├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ └── actions.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── Changelog.md ├── dist └── radar_client.js ├── lib ├── backoff.js ├── client_version.js ├── index.js ├── radar_client.js ├── scope.js └── state.js ├── package-lock.json ├── package.json ├── readme.md ├── scripts ├── add_client_version.js └── run_radar_tests ├── tests ├── backoff.test.js ├── lib │ └── engine.js ├── radar_client.alloc.test.js ├── radar_client.connect.test.js ├── radar_client.events.test.js ├── radar_client.test.js ├── radar_client.unit.test.js └── state.test.js └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/* -diff 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Describe the original problem and the changes made on this PR. 4 | 5 | ### References 6 | 7 | * Jira: 8 | * Github issue: 9 | 10 | ### Risks 11 | 12 | * High | Medium | Low : How might failures be experienced? All code changes 13 | carry a minimum of risk of **Low**, and **None** should be a rare exception. 14 | * Rollback: 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | main: 5 | name: npm test 6 | runs-on: ubuntu-22.04 7 | strategy: 8 | matrix: 9 | version: 10 | - 20 11 | - 22 12 | - 24 13 | steps: 14 | - uses: zendesk/checkout@v3 15 | - uses: zendesk/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.version }} 18 | - name: install 19 | run: npm install 20 | - name: node_js ${{ matrix.version }} 21 | run: verbose=1 npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "$", "_" 4 | ], 5 | 6 | "bitwise": false, 7 | "newcap": false, 8 | "eqeqeq": false, 9 | "eqnull": true, 10 | "immed": true, 11 | "nomen": false, 12 | "onevar": false, 13 | "plusplus": false, 14 | "regexp": false, 15 | "strict": false, 16 | "undef": true, 17 | "white": false, 18 | 19 | "debug": false, 20 | "es5": false, 21 | "evil": false, 22 | "forin": false, 23 | "laxbreak": false, 24 | "sub": false, 25 | 26 | "maxlen": 0, 27 | "maxerr": 50, 28 | "passfail": false, 29 | "browser": true, 30 | "rhino": false, 31 | "devel": true, 32 | "lastsemic": false, 33 | "expr": true, 34 | 35 | "node": true 36 | } 37 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ### 0.17.2 2 | * [PR #109](https://github.com/zendesk/radar_client/pull/109) - upgrade engineio client 3 | 4 | ### 0.17.0 5 | * [PR #100](https://github.com/zendesk/radar_client/pull/100) - upgrade engineio client 6 | 7 | ### 0.16.10 8 | * [PR #97](https://github.com/zendesk/radar_client/pull/97) - Dev dependencies update 9 | * [PR #96](https://github.com/zendesk/radar_client/pull/96) - Replaces Gulp with Webpack 10 | 11 | ### 0.16.9 12 | * [PR #95](https://github.com/zendesk/radar_client/pull/95) - Replaces Travis with Github Actions 13 | * [PR #94](https://github.com/zendesk/radar_client/pull/94) - Package update 14 | 15 | ### 0.16.8 16 | * [PR #90](https://github.com/zendesk/radar_client/pull/90) - Updates some dev dependencies 17 | 18 | ### 0.16.7 19 | * [PR #87](https://github.com/zendesk/radar_client/pull/87) - Bumps serialize-javascript to close a vulnerability 20 | 21 | ### 0.16.6 22 | * [PR #84](https://github.com/zendesk/radar_client/pull/84) - Updates all dependencies to current latest versions 23 | * [PR #83](https://github.com/zendesk/radar_client/pull/83) - Updates stadard to v12 24 | 25 | ### 0.16.5 26 | * [PR #82](https://github.com/zendesk/radar_client/pull/82) - Upgrade dependencies and Node.JS version. 27 | 28 | ### 0.16.1 29 | * [PR #75](https://github.com/zendesk/radar_client/pull/75) - disconnect should clear any pending sockets 30 | 31 | ### 0.16.0 32 | * [PR #74](https://github.com/zendesk/radar_client/pull/74) - Add a random splay while backing off, also expose backoff events. 33 | 34 | ### 0.15.4 35 | * [PR #71](https://github.com/zendesk/radar_client/pull/71) - Second try: flexible minor, patch specifier for radar_message dependency 36 | 37 | ### 0.15.3 38 | * [PR #70](https://github.com/zendesk/radar_client/pull/70) - Loosen version range on radar_message dependency 39 | 40 | ### 0.15.2 41 | * [PR #67](https://github.com/zendesk/radar_client/pull/67) - Reformat to standardjs 42 | * [PR #66](https://github.com/zendesk/radar_client/pull/66) - Fix license metadata in package.json 43 | * [PR #69](https://github.com/zendesk/radar_client/pull/69) - Replace gulejs with gulp 44 | * [PR #65](https://github.com/zendesk/radar_client/pull/65) - Do not store presence offline on client state 45 | * [PR #64](https://github.com/zendesk/radar_client/pull/64) - fake test timers with sinon.js 46 | 47 | ### 0.15.1 48 | * fix: send nameSync only once per session. 49 | * Replace gluejs implementation of build script. 50 | 51 | ### 0.15.0 52 | * Use radar message library 53 | - significant refactor that changes most of the code underlying the public 54 | APIs, which have *not* been changed 55 | - explicitly extracted the message "library" code in the refactor above to a 56 | separate radar_message library 57 | - pin radar_message version (we'll specify versions of package moving forward) 58 | 59 | ### 0.14.5 60 | * Presence resource can set online and include client data (to be broadcasted 61 | as part of the client_online and client_updated events). 62 | 63 | ### 0.14.4 64 | * One change to the modfified set of build steps 65 | - use "npm version --no-git-tag-version patch" to bump the version locally 66 | but not create a git commit and tag 67 | - "npm run build" to update getClientVersion(), and update dist/ 68 | - commit changes to GH 69 | - git tag version && git push --tags 70 | - npm publish 71 | 72 | ### 0.14.3 73 | * No new code, but use a modified set of build steps 74 | - use "npm version --no-git-tag-version patch" to bump the version locally 75 | but not create a git commit and tag 76 | - "npm run version-build" to update getClientVersion() 77 | - commit changes to GH 78 | - git tag && git push --tags 79 | - npm publish 80 | 81 | ### 0.14.2 82 | * Update package.json version to 0.14.2 and tag 83 | - getClientVersion() still returns 0.14.1 84 | 85 | ### 0.14.1 86 | * Update package.json version to 0.14.1 and tag 87 | 88 | ### 0.14.0 89 | * Code changes to support server side client state 90 | - generate client UUID; send to server as client name 91 | - auto-generate getClientVersion() and its source file 92 | - add nameSync method and new *control* scope 93 | 94 | ### 0.13.1 95 | * Code cleanup 96 | - comment capitalization, comment line length, code line length 97 | - minor code standardization 98 | 99 | ### 0.13.0 100 | * server version bump 101 | 102 | ### 0.12.1 103 | * fix for bug created by using Work Offline mode in FF 104 | 105 | ### 0.12.0 106 | * engine.io-client updated to v1.4.2 107 | 108 | ### 0.11.0 109 | * Stream resource API added 110 | 111 | ### 0.10.0 112 | * emit message events in a new context to avoid errors 113 | 114 | ### 0.9.3 115 | * Perform a prepublish check for outdated dependencies and dirty working tree 116 | 117 | ### 0.9.1 118 | * server version bump 119 | 120 | ### 0.9.0 121 | * engine.io-client upgrade to v1.3.1 122 | 123 | ### 0.8.1 124 | - when using presence v1 (without message options) 125 | - internally use presence v2 for v1 126 | - translate v2 results to v1 format 127 | - alloc/ready logs only print when actually allocing 128 | 129 | ### 0.8.0 130 | - radar bump to v0.8.0 131 | 132 | ### 0.7.3 133 | - Update logging more 134 | 135 | ### v0.6.0 136 | - More logging fixes 137 | - Upgrade engine.io-client to 0.7.9 138 | 139 | ### v0.3.1 140 | - Update minilog to 2.0.5 141 | - Fix logging and reduce verbosity 142 | -------------------------------------------------------------------------------- /dist/radar_client.js: -------------------------------------------------------------------------------- 1 | var RadarClient;(()=>{var t={917:t=>{function e(){this.failures=0}e.durations=[1e3,2e3,4e3,8e3,16e3,32e3],e.fallback=6e4,e.maxSplay=5e3,e.prototype.get=function(){return Math.ceil(Math.random()*e.maxSplay)+(e.durations[this.failures]||e.fallback)},e.prototype.increment=function(){this.failures++},e.prototype.success=function(){this.failures=0},e.prototype.isUnavailable=function(){return e.durations.length<=this.failures},t.exports=e},653:t=>{t.exports=function(){return"0.17.2"}},509:(t,e,n)=>{const s=new(n(443)),i=n(917);s._log=n(838),s.Backoff=i,t.exports=s},443:(t,e,n)=>{const s=n(173);let i=n(442);const r=n(301),o=n(408),c="undefined"!=typeof setImmediate?setImmediate:function(t){setTimeout(t,1)},a=n(653),u=n(39).Request,h=n(39).Response;function p(t){this.logger=n(838)("radar_client"),this._ackCounter=1,this._channelSyncTimes={},this._uses={},this._presences={},this._subscriptions={},this._restoreRequired=!1,this._queuedRequests=[],this._identitySetRequired=!0,this._isConfigured=!1,this._createManager(),this.configure(!1),this._addListeners(),this.backend=t||i}s.mixin(p),p.prototype.alloc=function(t,e){const n=this;return this._uses[t]||(this.logger().info("alloc: ",t),this.once("ready",(function(){n.logger().info("ready: ",t)})),this._uses[t]=!0),e&&this.once("ready",(function(){Object.prototype.hasOwnProperty.call(n._uses,t)&&e()})),this._isConfigured?this.manager.start():this._waitingForConfigure=!0,this},p.prototype.dealloc=function(t){this.logger().info({op:"dealloc",useName:t}),delete this._uses[t];let e,n=!1;for(e in this._uses)if(Object.prototype.hasOwnProperty.call(this._uses,e)){n=!0;break}n||(this.logger().info("closing the connection"),this.manager.close())},p.prototype.currentState=function(){return this.manager.current},p.prototype.configure=function(t){const e=t||this._configuration||{accountName:"",userId:0,userType:0};return e.userType=e.userType||0,this._configuration=this._me=e,this._isConfigured=this._isConfigured||!!t,this._isConfigured&&this._waitingForConfigure&&(this._waitingForConfigure=!1,this.manager.start()),this},p.prototype.configuration=function(t){return t in this._configuration?JSON.parse(JSON.stringify(this._configuration[t])):null},p.prototype.attachStateMachineErrorHandler=function(t){this.manager.attachErrorHandler(t)},p.prototype.currentUserId=function(){return this._configuration&&this._configuration.userId},p.prototype.currentClientId=function(){return this._socket&&this._socket.id},p.prototype.message=function(t){return new r("message",t,this)},p.prototype.presence=function(t){return new r("presence",t,this)},p.prototype.status=function(t){return new r("status",t,this)},p.prototype.stream=function(t){return new r("stream",t,this)},p.prototype.control=function(t){return new r("control",t,this)},p.prototype.nameSync=function(t,e,n){const s=u.buildNameSync(t,e);return this._write(s,n)},p.prototype.push=function(t,e,n,s,i){const r=u.buildPush(t,e,n,s);return this._write(r,i)},p.prototype.set=function(t,e,n,s){s=l(n,s),n=f(n);const i=u.buildSet(t,e,this._configuration.userId,this._configuration.userType,n);return this._write(i,s)},p.prototype.publish=function(t,e,n){const s=u.buildPublish(t,e);return this._write(s,n)},p.prototype.subscribe=function(t,e,n){n=l(e,n),e=f(e);const s=u.buildSubscribe(t,e);return this._write(s,n)},p.prototype.unsubscribe=function(t,e){const n=u.buildUnsubscribe(t);return this._write(n,e)},p.prototype.sync=function(t,e,n){n=l(e,n),e=f(e);const s=u.buildSync(t,e),i=!e&&s.isPresence();return this.when("get",(function(t){const e=new h(t);return!(!e||!e.isFor(s)||(i&&e.forceV1Response(),n&&n(e.getMessage()),0))})),this._write(s)},p.prototype.get=function(t,e,n){n=l(e,n),e=f(e);const s=u.buildGet(t,e);return this.when("get",(function(t){const e=new h(t);return!(!e||!e.isFor(s)||(n&&n(e.getMessage()),0))})),this._write(s)};const l=function(t,e){return"function"==typeof t?t:e},f=function(t){return"function"==typeof t?null:t};p.prototype._addListeners=function(){this.on("authenticateMessage",(function(t){const e=new u(t);e.setAuthData(this._configuration),this.emit("messageAuthenticated",e.getMessage())})),this.on("messageAuthenticated",(function(t){const e=new u(t);this._sendMessage(e)}))},p.prototype._write=function(t,e){const n=this;return e&&(t.setAttr("ack",this._ackCounter++),this.when("ack",(function(s){const i=new h(s);return n.logger().debug("ack",i),!!i.isAckFor(t)&&(e(t.getMessage()),!0)}))),this.emit("authenticateMessage",t.getMessage()),this},p.prototype._batch=function(t){const e=t.getAttr("to"),n=t.getAttr("value");let s=t.getAttr("time");if(!t.isValid())return this.logger().info("response is invalid:",t.getMessage()),!1;let i,r=0;const o=n.length;let c=s;const a=this._channelSyncTimes[e]||0;for(;ra&&this.emitNext(e,i),s>c&&(c=s);this._channelSyncTimes[e]=c},p.prototype._createManager=function(){const t=this,e=this.manager=o.create();e.on("enterState",(function(e){t.emit(e)})),e.on("event",(function(e){t.emit(e)})),e.on("connect",(function(n){const s=t._socket=new t.backend.Socket(t._configuration);s.once("open",(function(){if(s!==t._socket)return s.removeAllListeners("message"),s.removeAllListeners("open"),s.removeAllListeners("close"),void s.close();t.logger().debug("socket open",s.id),e.established()})),s.once("close",(function(n,i){t.logger().debug("socket closed",s.id,n,i),s.removeAllListeners("message"),s.transport&&s.transport.close(),s===t._socket&&(t._socket=null,e.is("closed")||e.disconnect())})),s.on("message",(function(e){if(s!==t._socket)return s.removeAllListeners("message"),s.removeAllListeners("open"),s.removeAllListeners("close"),void s.close();t._messageReceived(e)})),s.on("error",(function(e){t.emit("socketError",e)})),e.removeAllListeners("close"),e.once("close",(function(){s.close()}))})),e.on("activate",(function(){null===t._socket?e.disconnect():(t._identitySet(),t._restore(),t.emit("ready"))})),e.on("authenticate",(function(){e.activate()})),e.on("disconnect",(function(){t._restoreRequired=!0,t._identitySetRequired=!0;const e=t._socket;e&&(e.removeAllListeners("message"),e.removeAllListeners("open"),e.removeAllListeners("close"),e.once("open",(function(){t.logger().debug("socket open, closing it",e.id),e.close()})),t._socket=null)})),e.on("backoff",(function(e,n){t.emit("backoff",e,n)}))},p.prototype._memorize=function(t){const e=t.getAttr("op"),n=t.getAttr("to"),s=t.getAttr("value");switch(e){case"unsubscribe":return this._subscriptions[n]&&delete this._subscriptions[n],!0;case"sync":case"subscribe":return"sync"!==this._subscriptions[n]&&(this._subscriptions[n]=e),!0;case"set":if(t.isPresence())return"offline"!==s?this._presences[n]=s:delete this._presences[n],!0}return!1},p.prototype._restore=function(){let t,e;const n={subscriptions:0,presences:0,messages:0};if(this._restoreRequired){for(e in this._restoreRequired=!1,this._subscriptions)Object.prototype.hasOwnProperty.call(this._subscriptions,e)&&(t=this._subscriptions[e],this[t](e),n.subscriptions+=1);for(e in this._presences)Object.prototype.hasOwnProperty.call(this._presences,e)&&(this.set(e,this._presences[e]),n.presences+=1);for(;this._queuedRequests.length;)this._write(this._queuedRequests.shift()),n.messages+=1;this.logger().debug("restore-subscriptions",n)}},p.prototype._sendMessage=function(t){const e=this._memorize(t),n=t.getAttr("ack");if(this.emit("message:out",t.getMessage()),this._socket&&this.manager.is("activated")){let e=t.payload();"string"!=typeof e&&(e=JSON.stringify(e)),this._socket.send(e)}else this._isConfigured&&(this._restoreRequired=!0,this._identitySetRequired=!0,e&&!n||this._queuedRequests.push(t),this.manager.connectWhenAble())},p.prototype._messageReceived=function(t){const e=new h(JSON.parse(t)),n=e.getAttr("op"),s=e.getAttr("to");switch(this.emit("message:in",e.getMessage()),n){case"err":case"ack":case"get":this.emitNext(n,e.getMessage());break;case"sync":this._batch(e);break;default:this.emitNext(s,e.getMessage())}},p.prototype.emitNext=function(){const t=this,e=Array.prototype.slice.call(arguments);c((function(){t.emit.apply(t,e)}))},p.prototype._identitySet=function(){if(this._identitySetRequired){this._identitySetRequired=!1,this.name||(this.name=this._uuidV4Generate());const t={association:{id:this._socket.id,name:this.name},clientVersion:a()},e=this;this.control("clientName").nameSync(t,(function(t){e.logger("nameSync message: "+JSON.stringify(t))}))}};const g=[];for(let t=0;t<256;t++)g[t]=(t<16?"0":"")+t.toString(16);p.prototype._uuidV4Generate=function(){const t=4294967295*Math.random()|0,e=4294967295*Math.random()|0,n=4294967295*Math.random()|0,s=4294967295*Math.random()|0;return g[255&t]+g[t>>8&255]+g[t>>16&255]+g[t>>24&255]+"-"+g[255&e]+g[e>>8&255]+"-"+g[e>>16&15|64]+g[e>>24&255]+"-"+g[63&n|128]+g[n>>8&255]+"-"+g[n>>16&255]+g[n>>24&255]+g[255&s]+g[s>>8&255]+g[s>>16&255]+g[s>>24&255]},p.setBackend=function(t){i=t},t.exports=p},301:t=>{function e(t,e,n){this.client=n,this.prefix=this._buildScopePrefix(t,e,n.configuration("accountName"))}const n=["set","get","subscribe","unsubscribe","publish","push","sync","on","once","when","removeListener","removeAllListeners","nameSync"],s=function(t){e.prototype[t]=function(){const e=Array.prototype.slice.apply(arguments);return e.unshift(this.prefix),this.client[t].apply(this.client,e),this}};for(let t=0;t{const s=n(838)("radar_state"),i=n(173),r=n(917),o=n(285);t.exports={create:function(){const t=new r,e=o.create({error:function(t,e,n,i,r,o,c){if(s.warn("state-machine-error",arguments),c){if(!this.errorHandler)throw c;this.errorHandler(t,e,n,i,r,o,c)}},events:[{name:"connect",from:["opened","disconnected"],to:"connecting"},{name:"established",from:"connecting",to:"connected"},{name:"authenticate",from:"connected",to:"authenticating"},{name:"activate",from:["authenticating","activated"],to:"activated"},{name:"disconnect",from:o.WILDCARD,to:"disconnected"},{name:"close",from:o.WILDCARD,to:"closed"},{name:"open",from:["none","closed"],to:"opened"}],callbacks:{onevent:function(t,e,n){s.debug("from "+e+" -> "+n+", event: "+t),this.emit("event",t),this.emit(t,arguments)},onstate:function(t,e,n){this.emit("enterState",n),this.emit(n,arguments)},onconnecting:function(){this.startGuard()},onestablished:function(){this.cancelGuard(),t.success(),this.authenticate()},onclose:function(){this.cancelGuard()},ondisconnected:function(n,i,r){this._timer&&(clearTimeout(this._timer),delete this._timer);const o=t.get();t.increment(),this.emit("backoff",o,t.failures),s.debug("reconnecting in "+o+"msec"),this._timer=setTimeout((function(){delete e._timer,e.is("disconnected")&&e.connect()}),o),t.isUnavailable()&&(s.info("unavailable"),this.emit("unavailable"))}}});e._backoff=t,e._connectTimeout=1e4;for(const t in i.prototype)Object.prototype.hasOwnProperty.call(i.prototype,t)&&(e[t]=i.prototype[t]);return e.open(),e.start=function(){this.is("closed")&&this.open(),this.is("activated")?this.activate():this.connectWhenAble()},e.startGuard=function(){e.cancelGuard(),e._guard=setTimeout((function(){s.info("startGuard: disconnect from timeout"),e.disconnect()}),e._connectTimeout)},e.cancelGuard=function(){e._guard&&(clearTimeout(e._guard),delete e._guard)},e.connectWhenAble=function(){this.is("connected")||this.is("activated")||(this.can("connect")?this.connect():this.once("enterState",(function(){e.connectWhenAble()})))},e.attachErrorHandler=function(t){"function"==typeof t?this.errorHandler=t:s.warn("errorHandler must be a function")},e}}},173:t=>{function e(){this._events={}}e.prototype={on:function(t,e){this._events||(this._events={});var n=this._events;return(n[t]||(n[t]=[])).push(e),this},removeListener:function(t,e){var n,s=this._events[t]||[];for(n=s.length-1;n>=0&&s[n];n--)s[n]!==e&&s[n].cb!==e||s.splice(n,1)},removeAllListeners:function(t){t?this._events[t]&&(this._events[t]=[]):this._events={}},listeners:function(t){return this._events&&this._events[t]||[]},emit:function(t){this._events||(this._events={});var e,n=Array.prototype.slice.call(arguments,1),s=this._events[t]||[];for(e=s.length-1;e>=0&&s[e];e--)s[e].apply(this,n);return this},when:function(t,e){return this.once(t,e,!0)},once:function(t,e,n){if(!e)return this;function s(){n||this.removeListener(t,s),e.apply(this,arguments)&&n&&this.removeListener(t,s)}return s.cb=e,this.on(t,s),this}},e.mixin=function(t){var n,s=e.prototype;for(n in s)s.hasOwnProperty(n)&&(t.prototype[n]=s[n])},t.exports=e},471:t=>{t.exports=class{constructor(){const t=[...arguments];this.value=t,this.op="batch"}add(t){this.value.push(t)}get length(){return this.value.length}toJSON(){return{op:this.op,length:this.length,value:this.value}}}},39:(t,e,n)=>{const s=n(374),i=n(520),r=n(471),o={};o.Batch=r,o.Request=s,o.Response=i,t.exports=o},374:(t,e,n)=>{const s=n(838)("message:request"),i={control:["nameSync","disconnect"],message:["publish","subscribe","sync","unsubscribe"],presence:["get","set","subscribe","sync","unsubscribe"],status:["get","set","subscribe","sync","unsubscribe"],stream:["get","push","subscribe","sync","unsubscribe"]},r=function(t){this.message=t,this._isValid()||(s.error("invalid request. op: "+this.message.op+"; to: "+this.message.to),this.message={})};r.buildGet=function(t,e,n={op:"get",to:t}){return new r(n).setOptions(e)},r.buildPublish=function(t,e,n={op:"publish",to:t}){const s=new r(n);return s.setAttr("value",e),s},r.buildPush=function(t,e,n,s,i={op:"push",to:t}){const o=new r(i);return o.setAttr("resource",e),o.setAttr("action",n),o.setAttr("value",s),o},r.buildNameSync=function(t,e,n={op:"nameSync",to:t}){return new r(n).setOptions(e)},r.buildSet=function(t,e,n,s,i,o={op:"set",to:t}){const c=new r(o);return c.setAttr("value",e),c.setAttr("key",n),c.setAttr("type",s),i&&c.setAttr("clientData",i),c},r.buildSync=function(t,e,n={op:"sync",to:t}){const s=new r(n).setOptions(e);return s.isPresence()&&s.forceV2Sync(e),s},r.buildSubscribe=function(t,e,n={op:"subscribe",to:t}){return new r(n).setOptions(e)},r.buildUnsubscribe=function(t,e={op:"unsubscribe",to:t}){return new r(e)},r.prototype.forceV2Sync=function(t={}){(t=t||{}).version=2,this.setAttr("options",t)},r.prototype.setAuthData=function(t){this.setAttr("userData",t.userData),t.auth&&(this.setAttr("auth",t.auth),this.setAttr("userId",t.userId),this.setAttr("userType",t.userType),this.setAttr("accountName",t.accountName))},r.prototype.getMessage=function(){return this.message},r.prototype.setOptions=function(t){return t&&this.setAttr("options",t),this},r.prototype.isPresence=function(){return"presence"===this.type},r.prototype.setAttr=function(t,e){this.message[t]=e},r.prototype.getAttr=function(t){return this.message[t]},r.prototype.payload=function(){return JSON.stringify(this.getMessage())},r.prototype.getType=function(){return this.type},r.prototype._isValid=function(){if(!this.message.op||!this.message.to)return!1;const t=this._getType();if(t){if(this._isValidType(t)&&this._isValidOperation(t))return this.type=t,!0}else s.error("missing type");return!1},r.prototype._isValidType=function(t){for(const e in i)if(Object.prototype.hasOwnProperty.call(i,e)&&e===t)return!0;return this.errMsg="invalid type: "+t,s.error(this.errMsg),!1},r.prototype._isValidOperation=function(t,e=i[t]){const n=e&&e.indexOf(this.message.op)>=0;return n||(this.errMsg="invalid operation: "+this.message.op+" for type: "+t,s.error(this.errMsg)),n},r.prototype._getType=function(){return this.message.to.substring(0,this.message.to.indexOf(":"))},t.exports=r},520:(t,e,n)=>{const s=n(838)("message:response");function i(t){this.message=t,this._validate()||(s.error("invalid response. message: "+JSON.stringify(t)),this.message={})}i.prototype.getMessage=function(){return this.message},i.prototype._validate=function(){if(!this.message.op)return this.errMsg="missing op",!1;if("ack"===this.message.op){if(!this.message.value)return this.errMsg="missing value",s.error(this.errMsg),!1}else if("err"!==this.message.op&&!this.message.to)return this.errMsg="missing to",s.error(this.errMsg),!1;return!0},i.prototype.isValid=function(){return!!this.message.to&&!!this.message.value&&!!this.message.time},i.prototype.isFor=function(t){return this.getAttr("to")===t.getAttr("to")},i.prototype.isAckFor=function(t){return this.getAttr("value")===t.getAttr("ack")},i.prototype.getAttr=function(t){return this.message[t]},i.prototype.forceV1Response=function(){const t=this.message,e={};for(const n in t.value)if(Object.prototype.hasOwnProperty.call(t.value,n)){if(!t.value[n])continue;e[n]=t.value[n].userType}t.value=e,t.op="online",this.message=t},t.exports=i},285:t=>{var e=e=t.exports={VERSION:"2.2.0",Result:{SUCCEEDED:1,NOTRANSITION:2,CANCELLED:3,PENDING:4},Error:{INVALID_TRANSITION:100,PENDING_TRANSITION:200,INVALID_CALLBACK:300},WILDCARD:"*",ASYNC:"async",create:function(t,n){var s,i="string"==typeof t.initial?{state:t.initial}:t.initial,r=t.terminal||t.final,o=n||t.target||{},c=t.events||[],a=t.callbacks||{},u={},h=function(t){var n=t.from instanceof Array?t.from:t.from?[t.from]:[e.WILDCARD];u[t.name]=u[t.name]||{};for(var s=0;s=0:this.current===t},o.can=function(t){return!this.transition&&(u[t].hasOwnProperty(this.current)||u[t].hasOwnProperty(e.WILDCARD))},o.cannot=function(t){return!this.can(t)},o.error=t.error||function(t,e,n,s,i,r,o){throw o||r},o.isFinished=function(){return this.is(r)},i&&!i.defer&&o[i.event](),o},doCallback:function(t,n,s,i,r,o){if(n)try{return n.apply(t,[s,i,r].concat(o))}catch(n){return t.error(s,i,r,o,e.Error.INVALID_CALLBACK,"an exception occurred in a caller-provided callback function",n)}},beforeAnyEvent:function(t,n,s,i,r){return e.doCallback(t,t.onbeforeevent,n,s,i,r)},afterAnyEvent:function(t,n,s,i,r){return e.doCallback(t,t.onafterevent||t.onevent,n,s,i,r)},leaveAnyState:function(t,n,s,i,r){return e.doCallback(t,t.onleavestate,n,s,i,r)},enterAnyState:function(t,n,s,i,r){return e.doCallback(t,t.onenterstate||t.onstate,n,s,i,r)},changeState:function(t,n,s,i,r){return e.doCallback(t,t.onchangestate,n,s,i,r)},beforeThisEvent:function(t,n,s,i,r){return e.doCallback(t,t["onbefore"+n],n,s,i,r)},afterThisEvent:function(t,n,s,i,r){return e.doCallback(t,t["onafter"+n]||t["on"+n],n,s,i,r)},leaveThisState:function(t,n,s,i,r){return e.doCallback(t,t["onleave"+s],n,s,i,r)},enterThisState:function(t,n,s,i,r){return e.doCallback(t,t["onenter"+i]||t["on"+i],n,s,i,r)},beforeEvent:function(t,n,s,i,r){if(!1===e.beforeThisEvent(t,n,s,i,r)||!1===e.beforeAnyEvent(t,n,s,i,r))return!1},afterEvent:function(t,n,s,i,r){e.afterThisEvent(t,n,s,i,r),e.afterAnyEvent(t,n,s,i,r)},leaveState:function(t,n,s,i,r){var o=e.leaveThisState(t,n,s,i,r),c=e.leaveAnyState(t,n,s,i,r);return!1!==o&&!1!==c&&(e.ASYNC===o||e.ASYNC===c?e.ASYNC:void 0)},enterState:function(t,n,s,i,r){e.enterThisState(t,n,s,i,r),e.enterAnyState(t,n,s,i,r)},buildEvent:function(t,n){return function(){var s=this.current,i=n[s]||n[e.WILDCARD]||s,r=Array.prototype.slice.call(arguments);if(this.transition)return this.error(t,s,i,r,e.Error.PENDING_TRANSITION,"event "+t+" inappropriate because previous transition did not complete");if(this.cannot(t))return this.error(t,s,i,r,e.Error.INVALID_TRANSITION,"event "+t+" inappropriate in current state "+this.current);if(!1===e.beforeEvent(this,t,s,i,r))return e.Result.CANCELLED;if(s===i)return e.afterEvent(this,t,s,i,r),e.Result.NOTRANSITION;var o=this;this.transition=function(){return o.transition=null,o.current=i,e.enterState(o,t,s,i,r),e.changeState(o,t,s,i,r),e.afterEvent(o,t,s,i,r),e.Result.SUCCEEDED},this.transition.cancel=function(){o.transition=null,e.afterEvent(o,t,s,i,r)};var c=e.leaveState(this,t,s,i,r);return!1===c?(this.transition=null,e.Result.CANCELLED):e.ASYNC===c?e.Result.PENDING:this.transition?this.transition():void 0}}}},838:t=>{"use strict";t.exports=Minilog},442:t=>{"use strict";t.exports=eio}},e={},n=function n(s){var i=e[s];if(void 0!==i)return i.exports;var r=e[s]={exports:{}};return t[s](r,r.exports,n),r.exports}(509);RadarClient=n})(); -------------------------------------------------------------------------------- /lib/backoff.js: -------------------------------------------------------------------------------- 1 | function Backoff () { 2 | this.failures = 0 3 | } 4 | 5 | Backoff.durations = [1000, 2000, 4000, 8000, 16000, 32000] // seconds (ticks) 6 | Backoff.fallback = 60000 7 | Backoff.maxSplay = 5000 8 | 9 | Backoff.prototype.get = function () { 10 | const splay = Math.ceil(Math.random() * Backoff.maxSplay) 11 | return splay + (Backoff.durations[this.failures] || Backoff.fallback) 12 | } 13 | 14 | Backoff.prototype.increment = function () { 15 | this.failures++ 16 | } 17 | 18 | Backoff.prototype.success = function () { 19 | this.failures = 0 20 | } 21 | 22 | Backoff.prototype.isUnavailable = function () { 23 | return Backoff.durations.length <= this.failures 24 | } 25 | 26 | module.exports = Backoff 27 | -------------------------------------------------------------------------------- /lib/client_version.js: -------------------------------------------------------------------------------- 1 | // Auto-generated file, overwritten by scripts/add_package_version.js 2 | 3 | function getClientVersion () { return '0.17.3' } 4 | 5 | module.exports = getClientVersion 6 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const Client = require('./radar_client') 2 | const instance = new Client() 3 | const Backoff = require('./backoff.js') 4 | 5 | instance._log = require('minilog') 6 | instance.Backoff = Backoff 7 | 8 | // This module makes radar_client a singleton to prevent multiple connections etc. 9 | 10 | module.exports = instance 11 | -------------------------------------------------------------------------------- /lib/radar_client.js: -------------------------------------------------------------------------------- 1 | /* globals setImmediate */ 2 | const MicroEE = require('microee') 3 | let eio = require('engine.io-client') 4 | const Scope = require('./scope.js') 5 | const StateMachine = require('./state.js') 6 | const immediate = typeof setImmediate !== 'undefined' ? setImmediate : function (fn) { setTimeout(fn, 1) } 7 | const getClientVersion = require('./client_version.js') 8 | const Request = require('radar_message').Request 9 | const Response = require('radar_message').Response 10 | 11 | function Client (backend) { 12 | this.logger = require('minilog')('radar_client') 13 | this._ackCounter = 1 14 | this._channelSyncTimes = {} 15 | this._uses = {} 16 | this._presences = {} 17 | this._subscriptions = {} 18 | this._restoreRequired = false 19 | this._queuedRequests = [] 20 | this._identitySetRequired = true 21 | this._isConfigured = false 22 | 23 | this._createManager() 24 | this.configure(false) 25 | this._addListeners() 26 | 27 | // Allow backend substitution for tests 28 | this.backend = backend || eio 29 | } 30 | 31 | MicroEE.mixin(Client) 32 | 33 | // Public API 34 | 35 | // Each use of the client is registered with "alloc", and a given use often 36 | // persists through many connects and disconnects. 37 | // The state machine - "manager" - handles connects and disconnects 38 | Client.prototype.alloc = function (useName, callback) { 39 | const self = this 40 | if (!this._uses[useName]) { 41 | this.logger().info('alloc: ', useName) 42 | this.once('ready', function () { 43 | self.logger().info('ready: ', useName) 44 | }) 45 | 46 | this._uses[useName] = true 47 | } 48 | 49 | callback && this.once('ready', function () { 50 | if (Object.prototype.hasOwnProperty.call(self._uses, useName)) { 51 | callback() 52 | } 53 | }) 54 | 55 | if (this._isConfigured) { 56 | this.manager.start() 57 | } else { 58 | this._waitingForConfigure = true 59 | } 60 | 61 | return this 62 | } 63 | 64 | // When done with a given use of the client, unregister the use 65 | // Only when all uses are unregistered do we disconnect the client 66 | Client.prototype.dealloc = function (useName) { 67 | this.logger().info({ op: 'dealloc', useName: useName }) 68 | 69 | delete this._uses[useName] 70 | 71 | let stillAllocated = false 72 | let key 73 | 74 | for (key in this._uses) { 75 | if (Object.prototype.hasOwnProperty.call(this._uses, key)) { 76 | stillAllocated = true 77 | break 78 | } 79 | } 80 | if (!stillAllocated) { 81 | this.logger().info('closing the connection') 82 | this.manager.close() 83 | } 84 | } 85 | 86 | Client.prototype.currentState = function () { 87 | return this.manager.current 88 | } 89 | 90 | Client.prototype.configure = function (hash) { 91 | const configuration = hash || this._configuration || { accountName: '', userId: 0, userType: 0 } 92 | configuration.userType = configuration.userType || 0 93 | this._configuration = this._me = configuration 94 | this._isConfigured = this._isConfigured || !!hash 95 | 96 | if (this._isConfigured && this._waitingForConfigure) { 97 | this._waitingForConfigure = false 98 | this.manager.start() 99 | } 100 | 101 | return this 102 | } 103 | 104 | Client.prototype.configuration = function (configKey) { 105 | if (configKey in this._configuration) { 106 | return JSON.parse(JSON.stringify(this._configuration[configKey])) 107 | } else { 108 | return null 109 | } 110 | } 111 | 112 | Client.prototype.attachStateMachineErrorHandler = function (errorHandler) { 113 | this.manager.attachErrorHandler(errorHandler) 114 | } 115 | 116 | Client.prototype.currentUserId = function () { 117 | return this._configuration && this._configuration.userId 118 | } 119 | 120 | Client.prototype.currentClientId = function () { 121 | return this._socket && this._socket.id 122 | } 123 | 124 | // Return the chainable scope object for a given message type 125 | 126 | Client.prototype.message = function (scope) { 127 | return new Scope('message', scope, this) 128 | } 129 | 130 | Client.prototype.presence = function (scope) { 131 | return new Scope('presence', scope, this) 132 | } 133 | 134 | Client.prototype.status = function (scope) { 135 | return new Scope('status', scope, this) 136 | } 137 | 138 | Client.prototype.stream = function (scope) { 139 | return new Scope('stream', scope, this) 140 | } 141 | 142 | Client.prototype.control = function (scope) { 143 | return new Scope('control', scope, this) 144 | } 145 | 146 | // Operations 147 | 148 | Client.prototype.nameSync = function (scope, options, callback) { 149 | const request = Request.buildNameSync(scope, options) 150 | return this._write(request, callback) 151 | } 152 | 153 | Client.prototype.push = function (scope, resource, action, value, callback) { 154 | const request = Request.buildPush(scope, resource, action, value) 155 | return this._write(request, callback) 156 | } 157 | 158 | Client.prototype.set = function (scope, value, clientData, callback) { 159 | callback = _chooseFunction(clientData, callback) 160 | clientData = _nullIfFunction(clientData) 161 | 162 | const request = Request.buildSet(scope, value, 163 | this._configuration.userId, this._configuration.userType, 164 | clientData) 165 | 166 | return this._write(request, callback) 167 | } 168 | 169 | Client.prototype.publish = function (scope, value, callback) { 170 | const request = Request.buildPublish(scope, value) 171 | return this._write(request, callback) 172 | } 173 | 174 | Client.prototype.subscribe = function (scope, options, callback) { 175 | callback = _chooseFunction(options, callback) 176 | options = _nullIfFunction(options) 177 | 178 | const request = Request.buildSubscribe(scope, options) 179 | 180 | return this._write(request, callback) 181 | } 182 | 183 | Client.prototype.unsubscribe = function (scope, callback) { 184 | const request = Request.buildUnsubscribe(scope) 185 | return this._write(request, callback) 186 | } 187 | 188 | // sync returns the actual value of the operation 189 | Client.prototype.sync = function (scope, options, callback) { 190 | callback = _chooseFunction(options, callback) 191 | options = _nullIfFunction(options) 192 | 193 | const request = Request.buildSync(scope, options) 194 | 195 | const v1Presence = !options && request.isPresence() 196 | const onResponse = function (message) { 197 | const response = new Response(message) 198 | if (response && response.isFor(request)) { 199 | if (v1Presence) { 200 | response.forceV1Response() 201 | } 202 | if (callback) { 203 | callback(response.getMessage()) 204 | } 205 | return true 206 | } 207 | return false 208 | } 209 | 210 | this.when('get', onResponse) 211 | 212 | // sync does not return ACK (it sends back a data message) 213 | return this._write(request) 214 | } 215 | 216 | // get returns the actual value of the operation 217 | Client.prototype.get = function (scope, options, callback) { 218 | callback = _chooseFunction(options, callback) 219 | options = _nullIfFunction(options) 220 | 221 | const request = Request.buildGet(scope, options) 222 | 223 | const onResponse = function (message) { 224 | const response = new Response(message) 225 | if (response && response.isFor(request)) { 226 | if (callback) { 227 | callback(response.getMessage()) 228 | } 229 | return true 230 | } 231 | return false 232 | } 233 | 234 | this.when('get', onResponse) 235 | 236 | // get does not return ACK (it sends back a data message) 237 | return this._write(request) 238 | } 239 | 240 | // Private API 241 | 242 | const _chooseFunction = function (options, callback) { 243 | return typeof (options) === 'function' ? options : callback 244 | } 245 | 246 | const _nullIfFunction = function (options) { 247 | if (typeof (options) === 'function') { 248 | return null 249 | } 250 | return options 251 | } 252 | 253 | Client.prototype._addListeners = function () { 254 | // Add authentication data to a request message; _write() emits authenticateMessage 255 | this.on('authenticateMessage', function (message) { 256 | const request = new Request(message) 257 | request.setAuthData(this._configuration) 258 | 259 | this.emit('messageAuthenticated', request.getMessage()) 260 | }) 261 | 262 | // Once the request is authenticated, send it to the server 263 | this.on('messageAuthenticated', function (message) { 264 | const request = new Request(message) 265 | this._sendMessage(request) 266 | }) 267 | } 268 | 269 | Client.prototype._write = function (request, callback) { 270 | const self = this 271 | 272 | if (callback) { 273 | request.setAttr('ack', this._ackCounter++) 274 | 275 | // Wait ack 276 | this.when('ack', function (message) { 277 | const response = new Response(message) 278 | self.logger().debug('ack', response) 279 | if (!response.isAckFor(request)) { return false } 280 | callback(request.getMessage()) 281 | 282 | return true 283 | }) 284 | } 285 | 286 | this.emit('authenticateMessage', request.getMessage()) 287 | 288 | return this 289 | } 290 | 291 | Client.prototype._batch = function (response) { 292 | const to = response.getAttr('to') 293 | const value = response.getAttr('value') 294 | let time = response.getAttr('time') 295 | 296 | if (!response.isValid()) { 297 | this.logger().info('response is invalid:', response.getMessage()) 298 | return false 299 | } 300 | 301 | let index = 0 302 | let data 303 | const length = value.length 304 | let newest = time 305 | const current = this._channelSyncTimes[to] || 0 306 | 307 | for (; index < length; index = index + 2) { 308 | data = JSON.parse(value[index]) 309 | time = value[index + 1] 310 | 311 | if (time > current) { 312 | this.emitNext(to, data) 313 | } 314 | if (time > newest) { 315 | newest = time 316 | } 317 | } 318 | this._channelSyncTimes[to] = newest 319 | } 320 | 321 | Client.prototype._createManager = function () { 322 | const self = this 323 | const manager = this.manager = StateMachine.create() 324 | 325 | manager.on('enterState', function (state) { 326 | self.emit(state) 327 | }) 328 | 329 | manager.on('event', function (event) { 330 | self.emit(event) 331 | }) 332 | 333 | manager.on('connect', function (data) { 334 | const socket = self._socket = new self.backend.Socket(self._configuration) 335 | 336 | socket.once('open', function () { 337 | if (socket !== self._socket) { 338 | socket.removeAllListeners('message') 339 | socket.removeAllListeners('open') 340 | socket.removeAllListeners('close') 341 | socket.close() 342 | return 343 | } 344 | 345 | self.logger().debug('socket open', socket.id) 346 | manager.established() 347 | }) 348 | 349 | socket.once('close', function (reason, description) { 350 | self.logger().debug('socket closed', socket.id, reason, description) 351 | socket.removeAllListeners('message') 352 | // Patch for polling-xhr continuing to poll after socket close (HTTP:POST 353 | // failure). socket.transport is in error but not closed, so if a subsequent 354 | // poll succeeds, the transport remains open and polling until server closes 355 | // the socket. 356 | if (socket.transport) { 357 | socket.transport.close() 358 | } 359 | 360 | if (socket === self._socket) { 361 | self._socket = null 362 | if (!manager.is('closed')) { 363 | manager.disconnect() 364 | } 365 | } 366 | }) 367 | 368 | socket.on('message', function (message) { 369 | if (socket !== self._socket) { 370 | socket.removeAllListeners('message') 371 | socket.removeAllListeners('open') 372 | socket.removeAllListeners('close') 373 | socket.close() 374 | return 375 | } 376 | self._messageReceived(message) 377 | }) 378 | 379 | socket.on('error', function (error) { 380 | self.emit('socketError', error) 381 | }) 382 | 383 | manager.removeAllListeners('close') 384 | manager.once('close', function () { 385 | socket.close() 386 | }) 387 | }) 388 | 389 | manager.on('activate', function () { 390 | if (self._socket === null) { 391 | manager.disconnect() 392 | } else { 393 | self._identitySet() 394 | self._restore() 395 | self.emit('ready') 396 | } 397 | }) 398 | 399 | manager.on('authenticate', function () { 400 | // Can be overridden in order to establish an authentication protocol 401 | manager.activate() 402 | }) 403 | 404 | manager.on('disconnect', function () { 405 | self._restoreRequired = true 406 | self._identitySetRequired = true 407 | 408 | const socket = self._socket 409 | if (socket) { 410 | // If you reach disconnect with a socket obj, 411 | // it might be from startGuard (open timeout reached) 412 | // Clear out the current attempt to get a socket 413 | // and close it if it opens 414 | socket.removeAllListeners('message') 415 | socket.removeAllListeners('open') 416 | socket.removeAllListeners('close') 417 | socket.once('open', function () { 418 | self.logger().debug('socket open, closing it', socket.id) 419 | socket.close() 420 | }) 421 | self._socket = null 422 | } 423 | }) 424 | 425 | manager.on('backoff', function (time, step) { 426 | self.emit('backoff', time, step) 427 | }) 428 | } 429 | 430 | // Memorize subscriptions and presence states; return "true" for a message that 431 | // adds to the memorized subscriptions or presences 432 | Client.prototype._memorize = function (request) { 433 | const op = request.getAttr('op') 434 | const to = request.getAttr('to') 435 | const value = request.getAttr('value') 436 | 437 | switch (op) { 438 | case 'unsubscribe': 439 | // Remove from queue 440 | if (this._subscriptions[to]) { 441 | delete this._subscriptions[to] 442 | } 443 | return true 444 | 445 | case 'sync': 446 | case 'subscribe': 447 | // A catch for when *subscribe* is called after *sync* 448 | if (this._subscriptions[to] !== 'sync') { 449 | this._subscriptions[to] = op 450 | } 451 | return true 452 | 453 | case 'set': 454 | if (request.isPresence()) { 455 | if (value !== 'offline') { 456 | this._presences[to] = value 457 | } else { 458 | delete this._presences[to] 459 | } 460 | return true 461 | } 462 | } 463 | 464 | return false 465 | } 466 | 467 | Client.prototype._restore = function () { 468 | let item 469 | let to 470 | const counts = { subscriptions: 0, presences: 0, messages: 0 } 471 | if (this._restoreRequired) { 472 | this._restoreRequired = false 473 | 474 | for (to in this._subscriptions) { 475 | if (Object.prototype.hasOwnProperty.call(this._subscriptions, to)) { 476 | item = this._subscriptions[to] 477 | this[item](to) 478 | counts.subscriptions += 1 479 | } 480 | } 481 | 482 | for (to in this._presences) { 483 | if (Object.prototype.hasOwnProperty.call(this._presences, to)) { 484 | this.set(to, this._presences[to]) 485 | counts.presences += 1 486 | } 487 | } 488 | 489 | while (this._queuedRequests.length) { 490 | this._write(this._queuedRequests.shift()) 491 | counts.messages += 1 492 | } 493 | 494 | this.logger().debug('restore-subscriptions', counts) 495 | } 496 | } 497 | 498 | Client.prototype._sendMessage = function (request) { 499 | const memorized = this._memorize(request) 500 | const ack = request.getAttr('ack') 501 | 502 | this.emit('message:out', request.getMessage()) 503 | 504 | if (this._socket && this.manager.is('activated')) { 505 | let payload = request.payload() 506 | if (typeof payload !== 'string') { 507 | payload = JSON.stringify(payload) 508 | } 509 | this._socket.send(payload) 510 | } else if (this._isConfigured) { 511 | this._restoreRequired = true 512 | this._identitySetRequired = true 513 | if (!memorized || ack) { 514 | this._queuedRequests.push(request) 515 | } 516 | this.manager.connectWhenAble() 517 | } 518 | } 519 | 520 | Client.prototype._messageReceived = function (msg) { 521 | const response = new Response(JSON.parse(msg)) 522 | const op = response.getAttr('op') 523 | const to = response.getAttr('to') 524 | 525 | this.emit('message:in', response.getMessage()) 526 | 527 | switch (op) { 528 | case 'err': 529 | case 'ack': 530 | case 'get': 531 | this.emitNext(op, response.getMessage()) 532 | break 533 | 534 | case 'sync': 535 | this._batch(response) 536 | break 537 | 538 | default: 539 | this.emitNext(to, response.getMessage()) 540 | } 541 | } 542 | 543 | Client.prototype.emitNext = function () { 544 | const self = this 545 | const args = Array.prototype.slice.call(arguments) 546 | immediate(function () { self.emit.apply(self, args) }) 547 | } 548 | 549 | Client.prototype._identitySet = function () { 550 | if (this._identitySetRequired) { 551 | this._identitySetRequired = false 552 | 553 | if (!this.name) { 554 | this.name = this._uuidV4Generate() 555 | } 556 | 557 | // Send msg that associates this.id with current name 558 | const association = { id: this._socket.id, name: this.name } 559 | const clientVersion = getClientVersion() 560 | const options = { association: association, clientVersion: clientVersion } 561 | const self = this 562 | 563 | this.control('clientName').nameSync(options, function (message) { 564 | self.logger('nameSync message: ' + JSON.stringify(message)) 565 | }) 566 | } 567 | } 568 | 569 | // Variant (by Jeff Ward) of code behind node-uuid, but avoids need for module 570 | const lut = [] 571 | for (let i = 0; i < 256; i++) { lut[i] = (i < 16 ? '0' : '') + (i).toString(16) } 572 | Client.prototype._uuidV4Generate = function () { 573 | const d0 = Math.random() * 0xffffffff | 0 574 | const d1 = Math.random() * 0xffffffff | 0 575 | const d2 = Math.random() * 0xffffffff | 0 576 | const d3 = Math.random() * 0xffffffff | 0 577 | return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' + 578 | lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' + 579 | lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] + 580 | lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff] 581 | } 582 | 583 | Client.setBackend = function (lib) { eio = lib } 584 | 585 | module.exports = Client 586 | -------------------------------------------------------------------------------- /lib/scope.js: -------------------------------------------------------------------------------- 1 | function Scope (typeName, scope, client) { 2 | this.client = client 3 | this.prefix = this._buildScopePrefix(typeName, scope, client.configuration('accountName')) 4 | } 5 | 6 | const props = ['set', 'get', 'subscribe', 'unsubscribe', 'publish', 'push', 'sync', 7 | 'on', 'once', 'when', 'removeListener', 'removeAllListeners', 'nameSync'] 8 | 9 | const init = function (name) { 10 | Scope.prototype[name] = function () { 11 | const args = Array.prototype.slice.apply(arguments) 12 | args.unshift(this.prefix) 13 | this.client[name].apply(this.client, args) 14 | return this 15 | } 16 | } 17 | 18 | for (let i = 0; i < props.length; i++) { 19 | init(props[i]) 20 | } 21 | 22 | Scope.prototype._buildScopePrefix = function (typeName, scope, accountName) { 23 | return typeName + ':/' + accountName + '/' + scope 24 | } 25 | 26 | module.exports = Scope 27 | -------------------------------------------------------------------------------- /lib/state.js: -------------------------------------------------------------------------------- 1 | const log = require('minilog')('radar_state') 2 | const MicroEE = require('microee') 3 | const Backoff = require('./backoff') 4 | const Machine = require('sfsm') 5 | 6 | function create () { 7 | const backoff = new Backoff() 8 | const machine = Machine.create({ 9 | error: function (name, from, to, args, type, message, err) { 10 | log.warn('state-machine-error', arguments) 11 | 12 | if (err) { 13 | if (this.errorHandler) { 14 | this.errorHandler(name, from, to, args, type, message, err) 15 | } else { 16 | throw err 17 | } 18 | } 19 | }, 20 | 21 | events: [ 22 | { name: 'connect', from: ['opened', 'disconnected'], to: 'connecting' }, 23 | { name: 'established', from: 'connecting', to: 'connected' }, 24 | { name: 'authenticate', from: 'connected', to: 'authenticating' }, 25 | { name: 'activate', from: ['authenticating', 'activated'], to: 'activated' }, 26 | { name: 'disconnect', from: Machine.WILDCARD, to: 'disconnected' }, 27 | { name: 'close', from: Machine.WILDCARD, to: 'closed' }, 28 | { name: 'open', from: ['none', 'closed'], to: 'opened' } 29 | ], 30 | 31 | callbacks: { 32 | onevent: function (event, from, to) { 33 | log.debug('from ' + from + ' -> ' + to + ', event: ' + event) 34 | 35 | this.emit('event', event) 36 | this.emit(event, arguments) 37 | }, 38 | 39 | onstate: function (event, from, to) { 40 | this.emit('enterState', to) 41 | this.emit(to, arguments) 42 | }, 43 | 44 | onconnecting: function () { 45 | this.startGuard() 46 | }, 47 | 48 | onestablished: function () { 49 | this.cancelGuard() 50 | backoff.success() 51 | this.authenticate() 52 | }, 53 | 54 | onclose: function () { 55 | this.cancelGuard() 56 | }, 57 | 58 | ondisconnected: function (event, from, to) { 59 | if (this._timer) { 60 | clearTimeout(this._timer) 61 | delete this._timer 62 | } 63 | 64 | const time = backoff.get() 65 | backoff.increment() 66 | 67 | this.emit('backoff', time, backoff.failures) 68 | log.debug('reconnecting in ' + time + 'msec') 69 | 70 | this._timer = setTimeout(function () { 71 | delete machine._timer 72 | if (machine.is('disconnected')) { 73 | machine.connect() 74 | } 75 | }, time) 76 | 77 | if (backoff.isUnavailable()) { 78 | log.info('unavailable') 79 | this.emit('unavailable') 80 | } 81 | } 82 | } 83 | }) 84 | 85 | // For testing 86 | machine._backoff = backoff 87 | machine._connectTimeout = 10000 88 | 89 | for (const property in MicroEE.prototype) { 90 | if (Object.prototype.hasOwnProperty.call(MicroEE.prototype, property)) { 91 | machine[property] = MicroEE.prototype[property] 92 | } 93 | } 94 | 95 | machine.open() 96 | 97 | machine.start = function () { 98 | if (this.is('closed')) { 99 | this.open() 100 | } 101 | 102 | if (this.is('activated')) { 103 | this.activate() 104 | } else { 105 | this.connectWhenAble() 106 | } 107 | } 108 | 109 | machine.startGuard = function () { 110 | machine.cancelGuard() 111 | machine._guard = setTimeout(function () { 112 | log.info('startGuard: disconnect from timeout') 113 | machine.disconnect() 114 | }, machine._connectTimeout) 115 | } 116 | 117 | machine.cancelGuard = function () { 118 | if (machine._guard) { 119 | clearTimeout(machine._guard) 120 | delete machine._guard 121 | } 122 | } 123 | 124 | machine.connectWhenAble = function () { 125 | if (!(this.is('connected') || this.is('activated'))) { 126 | if (this.can('connect')) { 127 | this.connect() 128 | } else { 129 | this.once('enterState', function () { 130 | machine.connectWhenAble() 131 | }) 132 | } 133 | } 134 | } 135 | 136 | machine.attachErrorHandler = function (errorHandler) { 137 | if (typeof errorHandler === 'function') { 138 | this.errorHandler = errorHandler 139 | } else { 140 | log.warn('errorHandler must be a function') 141 | } 142 | } 143 | 144 | return machine 145 | } 146 | 147 | module.exports = { create: create } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radar_client", 3 | "description": "Realtime apps with a high level API based on engine.io", 4 | "version": "0.18.0", 5 | "license": "Apache-2.0", 6 | "author": "Zendesk, Inc.", 7 | "contributors": [ 8 | "Mikito Takada ", 9 | { 10 | "name": "Sam Shull", 11 | "url": "http://github.com/samshull" 12 | }, 13 | { 14 | "name": "Vanchi Koduvayur", 15 | "url": "https://github.com/vanchi-zendesk" 16 | }, 17 | { 18 | "name": "Nicolas Herment", 19 | "url": "https://github.com/nherment" 20 | } 21 | ], 22 | "main": "lib/index.js", 23 | "keywords": [ 24 | "realtime", 25 | "socket.io", 26 | "engine.io", 27 | "comet", 28 | "ajax" 29 | ], 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/zendesk/radar_client.git" 33 | }, 34 | "dependencies": { 35 | "engine.io-client": "^6.6.0", 36 | "microee": "^0.0.6", 37 | "minilog": "^3.1.0", 38 | "radar_message": "^1.3.5", 39 | "sfsm": "^0.0.4" 40 | }, 41 | "devDependencies": { 42 | "mocha": "^11.3.0", 43 | "sinon": "^11.1.2", 44 | "standard": "^16.0.4", 45 | "webpack": "^5.92.1", 46 | "webpack-cli": "^4.10.0" 47 | }, 48 | "scripts": { 49 | "check-clean": "if [[ $(git diff --shortstat 2> /dev/null | tail -n1) != \"\" ]]; then npm run warn-dirty-tree && exit 1; fi", 50 | "check-modules": "if [ -z \"$SKIP_PACKAGE_CHECK\" ] && [ ./package.json -nt ./node_modules ]; then npm run warn-outdated && exit 1; else rm -rf ./node_modules/sfsm/demo; fi", 51 | "warn-outdated": "echo 'Your node_modules are out of date. Please run \"rm -rf node_modules && npm install\" in order to ensure you have the latest dependencies.'", 52 | "warn-dirty-tree": "echo 'Your repo tree is dirty.'", 53 | "lint": "standard", 54 | "pretest": "npm run build && npm run lint", 55 | "test": "ls ./tests/*.test.js | xargs -n 1 -t -I {} sh -c 'TEST=\"{}\" npm run test-one'", 56 | "test-one": "./node_modules/.bin/mocha --ui exports --reporter spec --slow 2000ms --bail \"$TEST\"", 57 | "test-one-solo": "./node_modules/.bin/mocha --ui exports --reporter spec --slow 2000ms --bail", 58 | "prebuild": "npm run check-modules", 59 | "build": "npm run version-build; webpack", 60 | "version-build": "node scripts/add_client_version.js" 61 | }, 62 | "standard": { 63 | "ignore": [ 64 | "dist/*" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/zendesk/radar_client/workflows/CI/badge.svg) 2 | 3 | # Radar Client 4 | 5 | High level API and backend for writing web apps that use real-time information. 6 | 7 | The radar server code is here: https://github.com/zendesk/radar 8 | 9 | ## Documentation 10 | 11 | See http://radar.zendesk.com/ for detailed documentation. 12 | 13 | ## How to contribute 14 | 15 | - Fork http://github.com/zendesk/radar_client, clone, make changes (including a Changelog update), commit, push, PR 16 | - Then fork http://github.com/zendesk/radar, clone, update and test with radar_client changes (including a Changelog update), commit, push, PR 17 | 18 | ## Copyright and License 19 | 20 | Copyright 2015, Zendesk Inc. 21 | Licensed under the Apache License Version 2.0, http://www.apache.org/licenses/LICENSE-2.0 22 | -------------------------------------------------------------------------------- /scripts/add_client_version.js: -------------------------------------------------------------------------------- 1 | // Add the package version to radar_client.js prior to building dist 2 | const version = require('../package.json').version 3 | const fs = require('fs') 4 | 5 | const FILEPATH = 'lib/client_version.js' 6 | 7 | // Create the string to write 8 | const fileContents = '// Auto-generated file, overwritten by' + 9 | ' scripts/add_package_version.js\n\n' + 10 | "function getClientVersion () { return '" + 11 | version + 12 | "' }\n\n" + 13 | 'module.exports = getClientVersion\n' 14 | 15 | // Rewrite the file lib/version.js to contain a current getVersion() 16 | if (fs.existsSync(FILEPATH)) { 17 | fs.unlinkSync(FILEPATH) 18 | } 19 | 20 | // Write the client version to a new instance of the file 21 | const stream = fs.createWriteStream(FILEPATH) 22 | stream.once('open', function () { 23 | stream.write(fileContents) 24 | stream.end() 25 | }) 26 | -------------------------------------------------------------------------------- /scripts/run_radar_tests: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # clone a radar repo 4 | git clone https://github.com/zendesk/radar.git 5 | # link this version of client 6 | npm link 7 | cd radar 8 | npm install 9 | # replace radar_client with our version 10 | npm link radar_client 11 | npm test 12 | if [ $? -ne 0 ] 13 | then exit 1 14 | fi 15 | cd .. 16 | # cleanup 17 | rm -rf radar 18 | npm unlink radar_client 19 | -------------------------------------------------------------------------------- /tests/backoff.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const Backoff = require('../lib/backoff.js') 3 | 4 | exports['given a backoff'] = { 5 | beforeEach: function (done) { 6 | this.b = new Backoff() 7 | done() 8 | }, 9 | 10 | /* 11 | 12 | Some properties of a nice backoff system: 13 | 14 | - Exponential 15 | - However, random connection errors should not force the user to wait for a long time. 16 | - Random error = an error that does not repeat itself within a short time. 17 | 18 | */ 19 | 20 | 'durations increase as failures increase': function (done) { 21 | const b = this.b 22 | Backoff.durations.forEach(function (duration) { 23 | const v = b.get() 24 | assert(duration <= v) 25 | assert(duration + Backoff.maxSplay > v) 26 | b.increment() 27 | }) 28 | done() 29 | }, 30 | 31 | 'successful connection resets durations so that a random error doesnt cause a long wait': function (done) { 32 | const b = this.b 33 | assert(b.get() >= 1000) 34 | b.increment() 35 | assert(b.get() >= 2000) 36 | b.success() 37 | const v = b.get() 38 | assert(v >= 1000 && v < 6000) 39 | done() 40 | }, 41 | 42 | 'seven failures should cause a permanent disconnect': function (done) { 43 | const b = this.b 44 | b.increment() 45 | b.increment() 46 | b.increment() 47 | b.increment() 48 | b.increment() 49 | b.increment() 50 | b.increment() 51 | assert(b.get() > 60000) 52 | done() 53 | } 54 | 55 | } 56 | 57 | // When this module is the script being run, run the tests: 58 | if (module === require.main) { 59 | const mocha = require('child_process').spawn('mocha', ['--colors', '--ui', 'exports', '--reporter', 'spec', __filename]) 60 | mocha.stdout.pipe(process.stdout) 61 | mocha.stderr.pipe(process.stderr) 62 | } 63 | -------------------------------------------------------------------------------- /tests/lib/engine.js: -------------------------------------------------------------------------------- 1 | const MicroEE = require('microee') 2 | const log = require('minilog')('test') 3 | 4 | function State () { 5 | this._written = [] 6 | } 7 | 8 | const current = new State() 9 | 10 | MicroEE.mixin(State) 11 | 12 | const sockets = [] 13 | function Socket () { 14 | sockets.push(this) 15 | } 16 | 17 | MicroEE.mixin(Socket) 18 | 19 | Socket.prototype.send = function (data) { 20 | const message = JSON.parse(data) 21 | current._written.push(message) 22 | log(message) 23 | if (message.op === 'get' || message.op === 'sync') { 24 | this.emit('message', data) 25 | } 26 | // ACKs should be returned immediately 27 | if (message.ack) { 28 | this.emit('message', JSON.stringify({ op: 'ack', value: message.ack })) 29 | } 30 | } 31 | 32 | Socket.prototype.close = function () { 33 | const self = this 34 | self._state = 'closing' 35 | setTimeout(function () { 36 | self._state = 'closed' 37 | self.emit('close') 38 | }, 5) 39 | } 40 | 41 | function wrap (delay) { 42 | const s = new Socket() 43 | s._state = 'opening' 44 | setTimeout(function () { 45 | s.emit('open') 46 | s._state = 'open' 47 | }, delay) 48 | return s 49 | } 50 | 51 | const MockEngine = function (openDelay) { 52 | openDelay = openDelay || 5 53 | return { 54 | Socket: function () { return wrap(openDelay) }, 55 | current: current, 56 | sockets: sockets 57 | } 58 | } 59 | 60 | module.exports = MockEngine 61 | -------------------------------------------------------------------------------- /tests/radar_client.alloc.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const MockEngine = require('./lib/engine.js')() 3 | const RadarClient = require('../lib/radar_client.js') 4 | const getClientVersion = require('../lib/client_version.js') 5 | let client 6 | 7 | RadarClient.setBackend(MockEngine) 8 | 9 | exports['given an instance of Radar client'] = { 10 | beforeEach: function () { 11 | client = new RadarClient() 12 | }, 13 | 14 | afterEach: function () { 15 | MockEngine.current._written = [] 16 | }, 17 | 18 | 'client version is always available': function (done) { 19 | assert.ok(getClientVersion()) 20 | done() 21 | }, 22 | 23 | 'calls to operations do not cause errors before the client is configured, but dont write either': function (done) { 24 | client.status('test/foo').set('bar') 25 | assert.strictEqual(MockEngine.current._written.length, 0) 26 | done() 27 | }, 28 | 29 | 'as long as the client is configured, nameSync is the first message sent': function (done) { 30 | client.configure({ userId: 123, accountName: 'dev' }) 31 | client.status('test/foo').set('bar') 32 | 33 | client.on('ready', function () { 34 | assert.strictEqual(MockEngine.current._written[0].op, 'nameSync') 35 | assert.strictEqual(MockEngine.current._written[0].to, 'control:/dev/clientName') 36 | done() 37 | }) 38 | }, 39 | 40 | 'as long as the client is configured, client name is set': function (done) { 41 | client.configure({ userId: 123, accountName: 'dev' }) 42 | client.status('test/foo').set('bar') 43 | 44 | client.on('ready', function () { 45 | assert.ok(client.name) 46 | done() 47 | }) 48 | }, 49 | 50 | 'as long as the client is configured, any operation that requires a send will automatically connect': function (done) { 51 | client.configure({ userId: 123, accountName: 'dev' }) 52 | client.status('test/foo').set('bar') 53 | 54 | client.on('ready', function () { 55 | assert.strictEqual(MockEngine.current._written.length, 2) 56 | done() 57 | }) 58 | }, 59 | 60 | 'alloc calls do not perform a connect if not connected and not configured': function (done) { 61 | client.alloc('foo') 62 | setTimeout(function () { 63 | // We use a setTimeout, because connecting with the fake backend 64 | // is also async, it just takes 5 ms rather than a real connect duration 65 | assert.ok(client.manager.is('opened')) 66 | assert.ok(client._waitingForConfigure) 67 | done() 68 | }, 6) 69 | }, 70 | 71 | 'alloc calls perform a connect if not connected': function (done) { 72 | client.configure({ userId: 123, accountName: 'dev' }) 73 | client.alloc('foo') 74 | setTimeout(function () { 75 | // We use a setTimeout, because connecting with the fake backend 76 | // is also async, it just takes 5 ms rather than a real connect duration 77 | assert.ok(client.manager.is('activated')) 78 | done() 79 | }, 9) 80 | }, 81 | 82 | 'configure calls perform a connect if waiting for configured': function (done) { 83 | client.alloc('foo') 84 | setTimeout(function () { 85 | // We use a setTimeout, because connecting with the fake backend 86 | // is also async, it just takes 5 ms rather than a real connect duration 87 | assert.ok(client.manager.is('opened')) 88 | assert.ok(client._waitingForConfigure) 89 | client.configure({ userId: 123, accountName: 'dev' }) 90 | setTimeout(function () { 91 | assert.ok(client.manager.is('activated')) 92 | done() 93 | }, 6) 94 | }, 6) 95 | }, 96 | 97 | 'multiple alloc calls just trigger the callback': function (done) { 98 | let readyCount = 0 99 | let allocDoneCount = 0 100 | client.on('ready', function () { 101 | readyCount++ 102 | }) 103 | client.configure({ userId: 123, accountName: 'dev' }) 104 | // Test that the callback param works 105 | function onAlloc () { 106 | allocDoneCount++ 107 | } 108 | client.alloc('foo', onAlloc) 109 | setTimeout(function () { 110 | // Ready state == 'activated' 111 | assert.ok(client.manager.is('activated')) 112 | assert.strictEqual(readyCount, 1) 113 | assert.strictEqual(allocDoneCount, 1) 114 | // If the connect code would trigger, then these would not run the 115 | // on('ready') action immediately. If the action is run immediately, we 116 | // know that the connection code was skipped. 117 | // Might rewrite this to be more explicit later. 118 | client.alloc('foo') 119 | client.alloc('foo', onAlloc) 120 | client.alloc('foo') 121 | assert.strictEqual(readyCount, 4) 122 | assert.strictEqual(allocDoneCount, 2) 123 | done() 124 | }, 10) 125 | }, 126 | 127 | 'dealloc has no effect until all the allocs have been performed': function (done) { 128 | client.configure({ userId: 123, accountName: 'dev' }) 129 | 130 | client.alloc('foo') 131 | client.alloc('bar') 132 | client.alloc('baz') 133 | client.dealloc('baz') 134 | setTimeout(function () { 135 | assert.ok(client.manager.is('activated')) // = Ready state 136 | client.dealloc('bar') 137 | setTimeout(function () { 138 | assert.ok(client.manager.is('activated')) // = Ready state 139 | client.dealloc('foo') 140 | setTimeout(function () { 141 | assert.ok(client.manager.is('closed')) // = Stopped state 142 | done() 143 | }, 10) 144 | }, 10) 145 | }, 10) 146 | } 147 | 148 | } 149 | 150 | // When this module is the script being run, run the tests: 151 | if (module === require.main) { 152 | const mocha = require('child_process').spawn('mocha', ['--colors', '--ui', 'exports', '--reporter', 'spec', __filename]) 153 | mocha.stdout.pipe(process.stdout) 154 | mocha.stderr.pipe(process.stderr) 155 | } 156 | -------------------------------------------------------------------------------- /tests/radar_client.connect.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const RadarClient = require('../lib/radar_client.js') 3 | // With a delay 4 | const MockEngine = require('./lib/engine.js')(150) 5 | const Backoff = require('../lib/backoff.js') 6 | const Minilog = require('minilog') 7 | let client 8 | 9 | if (process.env.verbose === '1') { 10 | Minilog.pipe(Minilog.backends.nodeConsole.formatWithStack).pipe(Minilog.backends.nodeConsole) 11 | } 12 | 13 | exports['before connecting'] = { 14 | before: function (done) { 15 | // make sure the first backoff will leave enough time 16 | Backoff.maxSplay = 100 17 | Backoff.durations = [200, 300, 400] 18 | RadarClient.setBackend(MockEngine) 19 | done() 20 | }, 21 | after: function (done) { 22 | RadarClient.setBackend({}) 23 | done() 24 | }, 25 | 26 | beforeEach: function (done) { 27 | client = new RadarClient() 28 | client.configure({ accountName: 'test', userId: 123, userType: 2 }) 29 | client.alloc('channel', done) 30 | }, 31 | 32 | afterEach: function (done) { 33 | MockEngine.current._written = [] 34 | done() 35 | }, 36 | 37 | 'socket opening late should not cause two open sockets': function (done) { 38 | // set wait time to 10, while it takes 150ms for a socket to open 39 | client.manager._connectTimeout = 10 40 | client.on('backoff', function (time, failures) { 41 | // second backoff: after first failure, we expand open timeout 42 | // to be > 150 43 | // This is the same as shrinking the socket open delay 44 | if (failures === 2) { 45 | client.manager._connectTimeout = 300 46 | } 47 | }) 48 | client._socket.close() 49 | client.on('ready', function () { 50 | // Wait for 150 so any pending sockets can open 51 | setTimeout(function () { 52 | let openSockets = 0 53 | for (let i = 0; i < MockEngine.sockets.length; i++) { 54 | if (MockEngine.sockets[i]._state === 'open' || MockEngine.sockets[i]._state === 'opening') { 55 | openSockets++ 56 | } 57 | } 58 | 59 | assert.strictEqual(openSockets, 1) 60 | done() 61 | }, 150) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/radar_client.events.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const RadarClient = require('../lib/radar_client.js') 3 | 4 | exports['given a new presence'] = { 5 | beforeEach: function (done) { 6 | this.client = new RadarClient() 7 | done() 8 | }, 9 | 10 | 'can set a event handler and receive messages': function (done) { 11 | const client = this.client 12 | client.once('presence:tickets:21', function (changes) { 13 | assert.deepStrictEqual([1], changes.online) 14 | assert.deepStrictEqual([], changes.offline) 15 | }) 16 | 17 | // Send an online message 18 | client.emit('presence:tickets:21', { online: [1], offline: [] }) 19 | 20 | client.once('presence:tickets:21', function (changes) { 21 | assert.deepStrictEqual([], changes.online) 22 | assert.deepStrictEqual([1], changes.offline) 23 | done() 24 | }) 25 | 26 | client.emit('presence:tickets:21', { online: [], offline: [1] }) 27 | }, 28 | 29 | 'can remove a single callback': function (done) { 30 | const client = this.client 31 | client.once('presence:tickets:21', function () { 32 | assert.ok(false) 33 | }) 34 | client.removeAllListeners('presence:tickets:21') 35 | client.emit('presence:tickets:21', { online: [1], offline: [2] }) 36 | setTimeout(function () { 37 | done() 38 | }, 10) 39 | }, 40 | 41 | 'can remove all listeners from an event by string': function (done) { 42 | const client = this.client 43 | client.once('presence:tickets:21', function () { 44 | assert.ok(false) 45 | }) 46 | client.once('presence:tickets:21', function () { 47 | assert.ok(false) 48 | }) 49 | client.removeAllListeners('presence:tickets:21') 50 | client.emit('presence:tickets:21', { online: [1], offline: [2] }) 51 | setTimeout(function () { 52 | done() 53 | }, 10) 54 | }, 55 | 56 | 'forwards backoff events from manager': function (done) { 57 | this.client.once('backoff', function (time, step) { 58 | done() 59 | }) 60 | this.client.manager.emit('backoff', 10, 1) 61 | } 62 | } 63 | 64 | // When this module is the script being run, run the tests: 65 | if (module === require.main) { 66 | const mocha = require('child_process').spawn('mocha', ['--colors', '--ui', 'exports', '--reporter', 'spec', __filename]) 67 | mocha.stdout.pipe(process.stdout) 68 | mocha.stderr.pipe(process.stderr) 69 | } 70 | -------------------------------------------------------------------------------- /tests/radar_client.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const RadarClient = require('../lib/radar_client.js') 3 | const MockEngine = require('./lib/engine.js')() 4 | const Response = require('radar_message').Response 5 | const Backoff = require('../lib/backoff.js') 6 | let client 7 | 8 | exports['before connecting'] = { 9 | before: function (done) { 10 | // speed up some tests 11 | Backoff.maxSplay = 100 12 | Backoff.durations = [100, 200, 400] 13 | RadarClient.setBackend(MockEngine) 14 | done() 15 | }, 16 | after: function (done) { 17 | RadarClient.setBackend({}) 18 | done() 19 | }, 20 | 21 | beforeEach: function (done) { 22 | client = new RadarClient() 23 | done() 24 | }, 25 | 26 | afterEach: function (done) { 27 | MockEngine.current._written = [] 28 | done() 29 | }, 30 | 31 | 'making set() calls should not cause errors when not connected': function () { 32 | client.presence('tickets/21').set('online') 33 | client.presence('tickets/21').subscribe() 34 | client.presence('tickets/21').unsubscribe() 35 | client.message('user/123').publish('hello world') 36 | client.presence('tickets/21').get(function () {}) 37 | client.status('user/123').set('foo', 'bar') 38 | client.on('foo', function () {}) 39 | }, 40 | 41 | 'calling alloc or dealloc before configure call should not cause errors': function (done) { 42 | client.dealloc('test') 43 | client.alloc('test', function () { 44 | // This should never be called because of the dealloc 45 | assert.ok(false) 46 | }) 47 | client.dealloc('test') 48 | client.dealloc('test2') 49 | client.alloc('test2', done) 50 | client.configure({ userId: 123, accountName: 'dev' }) 51 | } 52 | 53 | } 54 | 55 | exports['after reconnecting'] = { 56 | before: function () { 57 | RadarClient.setBackend(MockEngine) 58 | }, 59 | 60 | after: function () { 61 | RadarClient.setBackend({}) 62 | }, 63 | 64 | beforeEach: function (done) { 65 | client = new RadarClient() 66 | client.configure({ accountName: 'test', userId: 123, userType: 2 }) 67 | client.alloc('channel', done) 68 | }, 69 | 70 | afterEach: function () { 71 | MockEngine.current._written = [] 72 | }, 73 | 74 | 'should send queued messages': function (done) { 75 | let connected = false 76 | 77 | client.once('connect', function () { 78 | connected = true 79 | assert.strictEqual(MockEngine.current._written.length, 1) 80 | assert.strictEqual(client._queuedRequests.length, 1) 81 | assert.deepStrictEqual(client._queuedRequests[0].message, { 82 | op: 'set', 83 | to: 'status:/test/tickets/21', 84 | value: 'online', 85 | key: 123, 86 | type: 2, 87 | userData: undefined 88 | }) 89 | }) 90 | 91 | client.once('ready', function () { 92 | assert.ok(connected) 93 | assert.strictEqual(client._queuedRequests.length, 0) 94 | assert.ok( 95 | MockEngine.current._written.some(function (message) { 96 | return message.op === 'set' && 97 | message.to === 'status:/test/tickets/21' && 98 | message.value === 'online' 99 | }) 100 | ) 101 | done() 102 | }) 103 | 104 | client.manager.disconnect() 105 | assert.strictEqual(client.currentState(), 'disconnected') 106 | client.status('tickets/21').set('online') 107 | assert.strictEqual(client.currentState(), 'connecting') 108 | }, 109 | 110 | 'should resend queued presences': function (done) { 111 | let connected = false 112 | 113 | client.presence('tickets/21').set('online') 114 | 115 | client.once('connect', function () { 116 | connected = true 117 | assert.strictEqual(Object.keys(client._presences).length, 1) 118 | assert.strictEqual(client._presences['presence:/test/tickets/21'], 'online') 119 | }) 120 | 121 | client.once('ready', function () { 122 | assert.ok(connected) 123 | assert.strictEqual(Object.keys(client._presences).length, 1) 124 | assert.ok( 125 | MockEngine.current._written.some(function (message) { 126 | return message.op === 'set' && 127 | message.to === 'presence:/test/tickets/21' && 128 | message.value === 'online' 129 | }) 130 | ) 131 | done() 132 | }) 133 | 134 | client.manager.disconnect() 135 | assert.strictEqual(client.currentState(), 'disconnected') 136 | }, 137 | 138 | 'should resend queued subscriptions': function (done) { 139 | let connected = false 140 | 141 | client.presence('tickets/21').subscribe() 142 | 143 | client.once('connect', function () { 144 | connected = true 145 | assert.strictEqual(Object.keys(client._subscriptions).length, 1) 146 | assert.strictEqual(client._subscriptions['presence:/test/tickets/21'], 'subscribe') 147 | }) 148 | 149 | client.once('ready', function () { 150 | assert.ok(connected) 151 | assert.strictEqual(Object.keys(client._subscriptions).length, 1) 152 | assert.ok( 153 | MockEngine.current._written.some(function (message) { 154 | return message.op === 'subscribe' && 155 | message.to === 'presence:/test/tickets/21' 156 | }) 157 | ) 158 | done() 159 | }) 160 | 161 | client.manager.disconnect() 162 | assert.strictEqual(client.currentState(), 'disconnected') 163 | } 164 | } 165 | 166 | exports['given a new presence'] = { 167 | beforeEach: function (done) { 168 | client = new RadarClient(MockEngine).configure({ userId: '123', accountName: 'dev' }) 169 | .alloc('test', done) 170 | }, 171 | 172 | afterEach: function (done) { 173 | MockEngine.current._written = [] 174 | done() 175 | }, 176 | 177 | 'can configure my id': function (done) { 178 | assert.strictEqual('123', client._configuration.userId) 179 | done() 180 | }, 181 | 182 | 'can set presence online in a scope': function (done) { 183 | client.presence('tickets/21').set('online') 184 | 185 | setTimeout(function () { 186 | assert.ok( 187 | MockEngine.current._written.some(function (message) { 188 | return (message.op === 'set' && 189 | message.to === 'presence:/dev/tickets/21' && 190 | message.value === 'online') 191 | }) 192 | ) 193 | 194 | done() 195 | }, 5) 196 | }, 197 | 198 | 'can set presence offline in a scope': function (done) { 199 | client.presence('tickets/21').set('offline') 200 | setTimeout(function () { 201 | assert.ok( 202 | MockEngine.current._written.some(function (message) { 203 | return ( 204 | message.op === 'set' && 205 | message.to === 'presence:/dev/tickets/21' && 206 | message.value === 'offline' && 207 | message.key === '123' 208 | ) 209 | }) 210 | ) 211 | 212 | done() 213 | }, 5) 214 | }, 215 | 216 | 'can subscribe to a presence scope': function (done) { 217 | client.presence('tickets/21').subscribe() 218 | 219 | setTimeout(function () { 220 | assert.ok( 221 | MockEngine.current._written.some(function (message) { 222 | return (message.op === 'subscribe' && 223 | message.to === 'presence:/dev/tickets/21' 224 | ) 225 | }) 226 | ) 227 | 228 | done() 229 | }, 5) 230 | }, 231 | 232 | 'can unsubscribe from a presence scope': function (done) { 233 | client.presence('tickets/21').unsubscribe() 234 | 235 | setTimeout(function () { 236 | assert.ok( 237 | MockEngine.current._written.some(function (message) { 238 | return (message.op === 'unsubscribe' && 239 | message.to === 'presence:/dev/tickets/21' 240 | ) 241 | }) 242 | ) 243 | 244 | done() 245 | }, 5) 246 | }, 247 | 248 | 'can do a one time get for a scope': function (done) { 249 | client.presence('tickets/21').get(function (results) { 250 | done() 251 | }) 252 | }, 253 | 254 | 'can set options for a get operation': function (done) { 255 | client.presence('tickets/21').get({ version: 2 }, function (results) { 256 | assert.ok( 257 | MockEngine.current._written.some(function (message) { 258 | return (message.op === 'get' && 259 | message.to === 'presence:/dev/tickets/21' && 260 | message.options && 261 | message.options.version === 2 262 | ) 263 | }) 264 | ) 265 | done() 266 | }) 267 | }, 268 | 269 | 'can set options for a sync operation': function (done) { 270 | client.presence('tickets/21').sync({ version: 2 }) 271 | 272 | setTimeout(function () { 273 | assert.ok( 274 | MockEngine.current._written.some(function (message) { 275 | return (message.op === 'sync' && 276 | message.to === 'presence:/dev/tickets/21' && 277 | message.options && 278 | message.options.version === 2 279 | ) 280 | }) 281 | ) 282 | 283 | done() 284 | }, 5) 285 | }, 286 | 287 | 'can publish messages to a user': function (done) { 288 | client.message('user/123').publish('hello world', function () { 289 | assert.ok( 290 | MockEngine.current._written.some(function (message) { 291 | return (message.op === 'publish' && 292 | message.to === 'message:/dev/user/123' && 293 | message.value === 'hello world' 294 | ) 295 | }) 296 | ) 297 | 298 | done() 299 | }, 5) 300 | }, 301 | 302 | 'if a authentication token is set, it gets sent on each operation': function (done) { 303 | client.configure({ userId: 123, accountName: 'dev', auth: 'AUTH', userType: 2 }) 304 | client.message('user/123').publish('hello world') 305 | 306 | setTimeout(function () { 307 | assert.ok( 308 | MockEngine.current._written.some(function (message) { 309 | return (message.op === 'publish' && 310 | message.to === 'message:/dev/user/123' && 311 | message.value === 'hello world' && 312 | message.auth === 'AUTH' 313 | ) 314 | }) 315 | ) 316 | done() 317 | }, 10) 318 | }, 319 | 320 | 'synchronization batch filters out duplicate messages to the same channel by time': function (done) { 321 | const received = [] 322 | client.on('foo', function (msg) { 323 | received.push(msg) 324 | }) 325 | const msg1 = { 326 | op: 'subscribe', 327 | to: 'foo', 328 | value: [ 329 | JSON.stringify({ value: 'a' }), 330 | 1, 331 | JSON.stringify({ value: 'b' }), 332 | 2, 333 | JSON.stringify({ value: 'c' }), 334 | 3 335 | ], 336 | time: 1 337 | } 338 | const response1 = new Response(msg1) 339 | client._batch(response1) 340 | 341 | const msg2 = { 342 | op: 'subscribe', 343 | to: 'foo', 344 | value: [ 345 | JSON.stringify({ value: 'b' }), 346 | 2, 347 | JSON.stringify({ value: 'c' }), 348 | 3, 349 | JSON.stringify({ value: 'd' }), 350 | 600 351 | ], 352 | time: 2 353 | } 354 | const response2 = new Response(msg2) 355 | client._batch(response2) 356 | 357 | setTimeout(function () { 358 | assert.strictEqual(4, received.length) 359 | done() 360 | }, 10) 361 | } 362 | } 363 | 364 | // When this module is the script being run, run the tests: 365 | if (module === require.main) { 366 | const mocha = require('child_process').spawn('mocha', ['--colors', '--ui', 'exports', '--reporter', 'spec', __filename]) 367 | mocha.stdout.pipe(process.stdout) 368 | mocha.stderr.pipe(process.stderr) 369 | } 370 | -------------------------------------------------------------------------------- /tests/radar_client.unit.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const sinon = require('sinon') 3 | const RadarClient = require('../lib/radar_client.js') 4 | const MockEngine = require('./lib/engine.js')() 5 | const Request = require('radar_message').Request 6 | const Response = require('radar_message').Response 7 | const HOUR = 1000 * 60 * 60 8 | let client 9 | 10 | exports.RadarClient = { 11 | beforeEach: function () { 12 | client = new RadarClient(MockEngine) 13 | }, 14 | 15 | afterEach: function () { 16 | MockEngine.current._written = [] 17 | }, 18 | 19 | '.configuration': { 20 | 'should return a deep copy of the configuration value for the key provided': function () { 21 | const configuration = { userId: 123, userData: { test: 1 }, accountName: 'dev' } 22 | client.configure(configuration) 23 | assert.notStrictEqual(configuration.userData, client.configuration('userData')) 24 | }, 25 | 26 | 'should never allow the configuration to be altered by reference': function () { 27 | const configuration = { userId: 123, userData: { test: 1 }, accountName: 'dev' } 28 | client.configure(configuration) 29 | client.configuration('userData').test = 2 30 | assert.strictEqual(configuration.userData.test, 1) 31 | assert.strictEqual(client.configuration('userData').test, 1) 32 | } 33 | }, 34 | 35 | '.currentClientId': { 36 | 'should return the current socket id if a socket id is available': function (done) { 37 | client.configure({ userId: 123, accountName: 'dev' }) 38 | client.alloc('test', function () { 39 | assert.strictEqual(client.currentClientId(), client._socket.id) 40 | done() 41 | }) 42 | } 43 | }, 44 | 45 | '.alloc': { 46 | 'should start the manager': function () { 47 | let called = false 48 | client.manager.start = function () { called = true } 49 | client.configure({ userId: 123, accountName: 'dev' }) 50 | client.alloc('foo') 51 | assert.ok(called) 52 | }, 53 | 54 | 'should add the channel name to the hash of users': function () { 55 | assert.strictEqual(client._uses.foo, undefined) 56 | client.alloc('foo') 57 | assert.strictEqual(client._uses.foo, true) 58 | }, 59 | 60 | 'should add a callback for ready if a callback is passed': function () { 61 | let called = false 62 | 63 | client.on = function (name, callback) { 64 | called = true 65 | assert.strictEqual(name, 'ready') 66 | assert.strictEqual(typeof callback, 'function') 67 | } 68 | 69 | client.alloc('foo', function () {}) 70 | assert.ok(called) 71 | } 72 | }, 73 | 74 | '.dealloc': { 75 | 'should delete the _uses property for a given channel name': function () { 76 | client.alloc('foo') 77 | assert.strictEqual(client._uses.foo, true) 78 | client.dealloc('foo') 79 | assert.strictEqual(client._uses.foo, undefined) 80 | }, 81 | 82 | 'should call close() on the manager if the are no open channels': function () { 83 | let called = true 84 | client.manager.close = function () { called = true } 85 | client.alloc('foo') 86 | assert.strictEqual(client._uses.foo, true) 87 | client.dealloc('foo') 88 | assert.strictEqual(client._uses.foo, undefined) 89 | assert.ok(called) 90 | } 91 | }, 92 | 93 | '.configure': { 94 | 'should not change the configuration if nothing is passed': function () { 95 | assert.deepStrictEqual(client._configuration, { accountName: '', userId: 0, userType: 0 }) 96 | client.configure() 97 | assert.deepStrictEqual(client._configuration, { accountName: '', userId: 0, userType: 0 }) 98 | }, 99 | 100 | 'should store the passed hash as a configuration property': function () { 101 | client.configure({ accountName: 'test', userId: 123, userType: 2 }) 102 | assert.deepStrictEqual(client._configuration, { accountName: 'test', userId: 123, userType: 2 }) 103 | } 104 | }, 105 | 106 | '.attachStateMachineErrorHandler': { 107 | '.should attach error handler to the state manager': function () { 108 | const errorHandler = function () {} 109 | const attachErrorHandlerSpy = sinon.spy(client.manager, 'attachErrorHandler') 110 | 111 | client.attachStateMachineErrorHandler(errorHandler) 112 | assert.ok(attachErrorHandlerSpy.calledWith(errorHandler)) 113 | 114 | attachErrorHandlerSpy.restore() 115 | } 116 | }, 117 | 118 | scopes: { 119 | '.message should return a scope with the appropriate prefix': function () { 120 | client.configure({ accountName: 'test' }) 121 | const scope = client.message('chatter/1') 122 | assert.strictEqual(scope.prefix, 'message:/test/chatter/1') 123 | }, 124 | 125 | '.presence should return a scope with the appropriate prefix': function () { 126 | client.configure({ accountName: 'test' }) 127 | const scope = client.presence('chatter/1') 128 | assert.strictEqual(scope.prefix, 'presence:/test/chatter/1') 129 | }, 130 | 131 | '.status should return a scope with the appropriate prefix': function () { 132 | client.configure({ accountName: 'test' }) 133 | const scope = client.status('chatter/1') 134 | assert.strictEqual(scope.prefix, 'status:/test/chatter/1') 135 | } 136 | }, 137 | 138 | '.set': { 139 | 'should call _write() with a set operation definition hash': function () { 140 | let called = false 141 | const callback = function () {} 142 | 143 | client._write = function (request, fn) { 144 | called = true 145 | assert.deepStrictEqual(request.getMessage(), { 146 | op: 'set', 147 | to: 'status:/test/account/1', 148 | value: 'whatever', 149 | key: 123, 150 | type: 0 151 | }) 152 | assert.strictEqual(fn, callback) 153 | } 154 | 155 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 156 | client.set('status:/test/account/1', 'whatever', callback) 157 | assert.ok(called) 158 | }, 159 | 160 | 'should not queue a presence set, but require restore': function () { 161 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 162 | assert.ok(!client.manager.is('activated')) 163 | client.set('presence:/test/account/1', 'online') 164 | assert.ok(client._restoreRequired) 165 | assert.ok(!client.manager.is('activated')) 166 | assert.strictEqual(client._queuedRequests.length, 0) 167 | assert.deepStrictEqual(client._presences, { 'presence:/test/account/1': 'online' }) 168 | }, 169 | 170 | 'should queue a presence set and require restore if there is a callback': function () { 171 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 172 | assert.ok(!client.manager.is('activated')) 173 | client.set('presence:/test/account/1', 'online', function () {}) 174 | assert.ok(client._restoreRequired) 175 | assert.ok(!client.manager.is('activated')) 176 | assert.strictEqual(client._queuedRequests.length, 1) 177 | assert.deepStrictEqual(client._presences, { 'presence:/test/account/1': 'online' }) 178 | } 179 | }, 180 | 181 | '.publish': { 182 | 'should call _write() with a publish operation definition hash': function () { 183 | let called = false 184 | const callback = function () {} 185 | 186 | client._write = function (request, fn) { 187 | called = true 188 | assert.deepStrictEqual(request.getMessage(), { 189 | op: 'publish', 190 | to: 'message:/test/account/1', 191 | value: 'whatever' 192 | }) 193 | assert.strictEqual(fn, callback) 194 | } 195 | 196 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 197 | client.publish('message:/test/account/1', 'whatever', callback) 198 | assert.ok(called) 199 | } 200 | }, 201 | 202 | '.subscribe': { 203 | 'should call _write() with a subscribe operation definition hash': function () { 204 | let called = false 205 | const callback = function () {} 206 | 207 | client._write = function (request, fn) { 208 | called = true 209 | assert.deepStrictEqual(request.getMessage(), { 210 | op: 'subscribe', 211 | to: 'status:/test/account/1' 212 | }) 213 | assert.strictEqual(fn, callback) 214 | } 215 | 216 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 217 | client.subscribe('status:/test/account/1', callback) 218 | assert.ok(called) 219 | }, 220 | 221 | 'should not queue a subscribe operation if disconnected, but require restore': function () { 222 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 223 | assert.ok(!client.manager.is('activated')) 224 | client.subscribe('status:/test/account/1') 225 | assert.ok(client._restoreRequired) 226 | assert.ok(!client.manager.is('activated')) 227 | assert.strictEqual(client._queuedRequests.length, 0) 228 | assert.deepStrictEqual(client._subscriptions, { 'status:/test/account/1': 'subscribe' }) 229 | }, 230 | 231 | 'should queue a subscribe operation if disconnected and require restore if there is a callback': function () { 232 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 233 | assert.ok(!client.manager.is('activated')) 234 | client.subscribe('status:/test/account/1', function () {}) 235 | assert.ok(client._restoreRequired) 236 | assert.ok(!client.manager.is('activated')) 237 | assert.strictEqual(client._queuedRequests.length, 1) 238 | assert.deepStrictEqual(client._subscriptions, { 'status:/test/account/1': 'subscribe' }) 239 | }, 240 | 241 | 'should not queue a sync operation if disconnected, but require restore': function () { 242 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 243 | assert.ok(!client.manager.is('activated')) 244 | client.sync('presence:/test/account/1') 245 | assert.ok(client._restoreRequired) 246 | assert.ok(!client.manager.is('activated')) 247 | assert.strictEqual(client._queuedRequests.length, 0) 248 | assert.deepStrictEqual(client._subscriptions, { 'presence:/test/account/1': 'sync' }) 249 | } 250 | }, 251 | 252 | '.unsubscribe': { 253 | 'should call _write() with a unsubscribe operation definition hash': function () { 254 | let called = false 255 | const callback = function () {} 256 | 257 | client._write = function (request, fn) { 258 | called = true 259 | assert.deepStrictEqual(request.getMessage(), { 260 | op: 'unsubscribe', 261 | to: 'status:/test/account/1' 262 | }) 263 | assert.strictEqual(fn, callback) 264 | } 265 | 266 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 267 | client.unsubscribe('status:/test/account/1', callback) 268 | assert.ok(called) 269 | }, 270 | 271 | 'should not queue a message if the subscription was not in memory, but require restore': function () { 272 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 273 | assert.ok(!client.manager.is('activated')) 274 | client.unsubscribe('presence:/test/account/1') 275 | assert.ok(client._restoreRequired) 276 | assert.ok(!client.manager.is('activated')) 277 | assert.strictEqual(client._queuedRequests.length, 0) 278 | assert.deepStrictEqual(client._subscriptions, {}) 279 | }, 280 | 281 | 'should queue a message if the subscription was not in memory and require restore if there is a callback': function () { 282 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 283 | assert.ok(!client.manager.is('activated')) 284 | client.unsubscribe('presence:/test/account/1', function () {}) 285 | assert.ok(client._restoreRequired) 286 | assert.ok(!client.manager.is('activated')) 287 | assert.strictEqual(client._queuedRequests.length, 1) 288 | assert.deepStrictEqual(client._subscriptions, {}) 289 | } 290 | }, 291 | 292 | '.get': { 293 | 'should call _write() with a get operation definition hash': function () { 294 | let called = false 295 | 296 | client._write = function (request) { 297 | called = true 298 | assert.deepStrictEqual(request.getMessage(), { 299 | op: 'get', 300 | to: 'status:/test/account/1' 301 | }) 302 | } 303 | 304 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 305 | client.get('status:/test/account/1') 306 | assert.ok(called) 307 | }, 308 | 309 | 'should listen for the next get response operation': function () { 310 | let called = false 311 | 312 | client.when = function (operation, fn) { 313 | called = true 314 | } 315 | 316 | client.get('status:/test/account/1', function () {}) 317 | assert.ok(called) 318 | }, 319 | 320 | 'should pass a function that will call the callback function for the get response operation with the scope provided': function () { 321 | let called = false 322 | let passed = false 323 | const scope = 'status:/test/account/1' 324 | const message = { op: 'get', to: scope } 325 | const callback = function (msg) { 326 | passed = true 327 | assert.deepStrictEqual(msg, message) 328 | } 329 | 330 | client.when = function (operation, fn) { 331 | called = true 332 | fn(message) 333 | } 334 | 335 | client.get(scope, callback) 336 | assert.ok(called) 337 | assert.ok(passed) 338 | }, 339 | 340 | 'should pass a function that will not call the callback function for a get response operation with a different scope': function () { 341 | let called = false 342 | let passed = true 343 | const message = { op: 'get', to: 'status:/test/account/2' } 344 | const callback = function (msg) { 345 | passed = false 346 | } 347 | 348 | client.when = function (operation, fn) { 349 | called = true 350 | fn(message) 351 | } 352 | 353 | client.get('status:/test/account/1', callback) 354 | assert.ok(called) 355 | assert.ok(passed) 356 | } 357 | }, 358 | 359 | '.sync': { 360 | 'should call _write() with a sync operation definition hash': function () { 361 | let called = false 362 | 363 | client._write = function (request) { 364 | called = true 365 | assert.deepStrictEqual(request.getMessage(), { 366 | op: 'sync', 367 | to: 'status:/test/account/1' 368 | }) 369 | } 370 | 371 | client.configure({ accountName: 'test', userId: 123, userType: 0 }) 372 | client.sync('status:/test/account/1') 373 | assert.ok(called) 374 | }, 375 | 376 | 'with options': { 377 | 'should listen for the next get response operation': function () { 378 | let called = false 379 | 380 | client.when = function (operation, fn) { 381 | called = true 382 | } 383 | 384 | client.sync('presence:/test/account/1', { version: 2 }, function () {}) 385 | assert.ok(called) 386 | }, 387 | 388 | 'should pass a function that will call the callback function for the get response operation with the scope provided': function () { 389 | let called = false 390 | let passed = false 391 | const scope = 'presence:/test/account/1' 392 | const message = { op: 'sync', to: scope } 393 | const callback = function (msg) { 394 | passed = true 395 | assert.deepStrictEqual(msg, message) 396 | } 397 | 398 | client.when = function (operation, fn) { 399 | called = true 400 | fn(message) 401 | } 402 | 403 | client.sync(scope, { version: 2 }, callback) 404 | assert.ok(called) 405 | assert.ok(passed) 406 | }, 407 | 408 | 'should pass a function that will not call the callback function for a get response operation with a different scope': function () { 409 | let called = false 410 | let passed = true 411 | const message = { op: 'sync', to: 'presence:/test/account/2' } 412 | const response = new Response(message) 413 | const callback = function (msg) { 414 | passed = false 415 | } 416 | 417 | client.when = function (operation, fn) { 418 | called = true 419 | fn(response) 420 | } 421 | 422 | client.sync('presence:/test/account/1', { version: 2 }, callback) 423 | assert.ok(called) 424 | assert.ok(passed) 425 | } 426 | }, 427 | 428 | 'without options on a presence': { 429 | 'should force v2, translate result from v2 to v1': function (done) { 430 | const scope = 'presence:/test/account/1' 431 | 432 | client.sync(scope, function (m) { 433 | assert.deepStrictEqual({ 434 | op: 'online', 435 | to: scope, 436 | value: { 437 | 100: 2, 438 | 200: 0 439 | } 440 | }, m) 441 | done() 442 | }) 443 | 444 | // Previous online emits should not affect the callback 445 | client.emit(scope, { op: 'online', to: scope, value: { 100: 2 } }) 446 | 447 | const message = { 448 | op: 'get', 449 | to: scope, 450 | value: { 451 | 100: { userType: 2, clients: {} }, 452 | 200: { userType: 0, clients: {} } 453 | } 454 | } 455 | 456 | client.emit('get', message) 457 | } 458 | } 459 | }, 460 | 461 | 'internal methods': { 462 | _memorize: { 463 | 'memorizing a sync/subscribe should work': function (done) { 464 | let request 465 | 466 | assert.strictEqual(0, Object.keys(client._subscriptions).length) 467 | request = Request.buildSubscribe('presence:/test/ticket/1') 468 | client._memorize(request) 469 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 470 | 471 | request = Request.buildSync('status:/test/ticket/1') 472 | client._memorize(request) 473 | assert.strictEqual(2, Object.keys(client._subscriptions).length) 474 | 475 | request = Request.buildGet('status:/test/ticket/1') 476 | client._memorize(request) 477 | // Should be a no-op 478 | assert.strictEqual(2, Object.keys(client._subscriptions).length) 479 | 480 | done() 481 | }, 482 | 483 | 'memorizing a set(online) and unmemorizing a set(offline) should work': function (done) { 484 | let request 485 | 486 | assert.strictEqual(0, Object.keys(client._presences).length) 487 | request = Request.buildSet('presence:/foo/bar', 'online') 488 | client._memorize(request) 489 | assert.strictEqual('online', client._presences['presence:/foo/bar']) 490 | assert.strictEqual(1, Object.keys(client._presences).length) 491 | // Duplicate should be ignored 492 | client._memorize(request) 493 | assert.strictEqual(1, Object.keys(client._presences).length) 494 | 495 | request = Request.buildSet('presence:/foo/bar', 'offline') 496 | client._memorize(request) 497 | assert.strictEqual(0, Object.keys(client._presences).length) 498 | done() 499 | }, 500 | 501 | 'memorizing a unsubscribe should remove any sync/subscribe': function (done) { 502 | // Set up 503 | let request = Request.buildSubscribe('status:/test/ticket/1') 504 | client._memorize(request) 505 | request = Request.buildSync('status:/test/ticket/2') 506 | client._memorize(request) 507 | assert.strictEqual(2, Object.keys(client._subscriptions).length) 508 | 509 | // Unsubscribe 510 | request = Request.buildUnsubscribe('status:/test/ticket/1') 511 | client._memorize(request) 512 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 513 | request = Request.buildUnsubscribe('status:/test/ticket/2') 514 | client._memorize(request) 515 | assert.strictEqual(0, Object.keys(client._subscriptions).length) 516 | done() 517 | }, 518 | 519 | 'duplicated subscribes and syncs should only be stored once and sync is more important than subscribe': function (done) { 520 | // Simple duplicates 521 | let request = Request.buildSubscribe('status:/test/ticket/1') 522 | client._memorize(request) 523 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 524 | client._memorize(request) 525 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 526 | 527 | client._subscriptions = {} 528 | // Simple duplicates 529 | request = Request.buildSync('status:/test/ticket/2') 530 | client._memorize(request) 531 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 532 | client._memorize(request) 533 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 534 | 535 | client._subscriptions = {} 536 | // Sync after subscribe 537 | request = Request.buildSubscribe('status:/test/ticket/3') 538 | client._memorize(request) 539 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 540 | request = Request.buildSync('status:/test/ticket/3') 541 | client._memorize(request) 542 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 543 | assert.strictEqual('sync', client._subscriptions['status:/test/ticket/3']) 544 | 545 | client._subscriptions = {} 546 | // Subscribe after sync 547 | request = Request.buildSync('status:/test/ticket/4') 548 | client._memorize(request) 549 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 550 | assert.strictEqual('sync', client._subscriptions['status:/test/ticket/4']) 551 | // When we sync and subscribe, it means just sync 552 | request = Request.buildSubscribe('status:/test/ticket/4') 553 | client._memorize(request) 554 | assert.strictEqual(1, Object.keys(client._subscriptions).length) 555 | assert.strictEqual('sync', client._subscriptions['status:/test/ticket/4']) 556 | 557 | done() 558 | } 559 | }, 560 | 561 | _restore: { 562 | 'restore presences': function (done) { 563 | MockEngine.current._written = [] 564 | let request = Request.buildSet('presence:/foo/bar', 'online') 565 | client._memorize(request) 566 | request = Request.buildSet('presence:/foo/bar2', 'offline') 567 | client._memorize(request) 568 | client._restoreRequired = true 569 | client.configure({ accountName: 'foo', userId: 123, userType: 2 }) 570 | client.alloc('test', function () { 571 | assert.strictEqual(MockEngine.current._written.length, 2) 572 | assert.ok(MockEngine.current._written.some(function (message) { 573 | return (message.op === 'set' && 574 | message.to === 'presence:/foo/bar' && 575 | message.value === 'online') 576 | })) 577 | done() 578 | }) 579 | }, 580 | 581 | 'restore subscriptions': function (done) { 582 | MockEngine.current._written = [] 583 | let request = Request.buildSubscribe('status:/foo/bar') 584 | client._memorize(request) 585 | request = Request.buildSubscribe('message:/foo/bar2') 586 | client._memorize(request) 587 | client._restoreRequired = true 588 | client.configure({ accountName: 'foo', userId: 123, userType: 2 }) 589 | client.alloc('test', function () { 590 | assert.strictEqual(MockEngine.current._written.length, 3) 591 | assert.ok(MockEngine.current._written.some(function (message) { 592 | return (message.op === 'subscribe' && 593 | message.to === 'status:/foo/bar') 594 | })) 595 | assert.ok(MockEngine.current._written.some(function (message) { 596 | return (message.op === 'subscribe' && 597 | message.to === 'message:/foo/bar2') 598 | })) 599 | done() 600 | }) 601 | } 602 | }, 603 | '._write': { 604 | 'should emit an authenticateMessage event': function () { 605 | let called = false 606 | const message = { op: 'subscribe', to: 'status:/account/scope/1' } 607 | const request = Request.buildSubscribe(message.to) 608 | 609 | client.emit = function (name, data) { 610 | called = true 611 | assert.strictEqual(name, 'authenticateMessage') 612 | assert.deepStrictEqual(data, message) 613 | } 614 | 615 | // client._write(request.getMessage()) 616 | client._write(request) 617 | assert.ok(called) 618 | }, 619 | 620 | 'should register an ack event handler that calls the callback function once the appropriate ack message has been received': function () { 621 | let called = false 622 | let passed = false 623 | const request = Request.buildSubscribe('status:/account/scope/1') 624 | const ackMessage = { value: -2 } 625 | const callback = function (msg) { 626 | passed = true 627 | assert.deepStrictEqual(msg, request.getMessage()) 628 | } 629 | 630 | client.when = function (name, fn) { 631 | called = true 632 | assert.strictEqual(name, 'ack') 633 | ackMessage.op = 'ack' 634 | ackMessage.value = request.getAttr('ack') 635 | fn(ackMessage) 636 | } 637 | 638 | client._write(request, callback) 639 | assert.ok(called) 640 | assert.ok(passed) 641 | }, 642 | 643 | 'should register an ack event handler that does not call the callback function for ack messages with a different value': function () { 644 | let called = false 645 | let passed = true 646 | const request = Request.buildSubscribe('status:/account/scope/1') 647 | const ackMessage = { op: 'ack', value: -2 } 648 | const response = new Response(ackMessage) 649 | const callback = function (msg) { passed = false } 650 | 651 | client.when = function (name, fn) { 652 | called = true 653 | assert.strictEqual(name, 'ack') 654 | fn(response) 655 | } 656 | 657 | client._write(request, callback) 658 | assert.ok(called) 659 | assert.ok(passed) 660 | } 661 | }, 662 | 663 | '._batch': { 664 | 'should ignore messages without the appropriate properties': { 665 | op: function () { 666 | const message = { to: 'status:/dev/ticket/1', value: 'x', time: new Date() / 1000 } 667 | 668 | var response = new Response(message) // eslint-disable-line 669 | assert.deepStrictEqual(client._channelSyncTimes, {}) 670 | }, 671 | 672 | to: function () { 673 | const message = { op: 'subscribe', value: 'x', time: new Date() / 1000 } 674 | 675 | var response = new Response(message) // eslint-disable-line 676 | assert.deepStrictEqual(client._channelSyncTimes, {}) 677 | }, 678 | 679 | value: function () { 680 | const message = { op: 'subscribe', to: 'you', value: 'x' } 681 | const response = new Response(message) 682 | 683 | assert.strictEqual(client._channelSyncTimes.you, undefined) 684 | assert.ok(!client._batch(response)) 685 | assert.strictEqual(client._channelSyncTimes.you, undefined) 686 | }, 687 | 688 | time: function () { 689 | const message = { op: 'subscribe', to: 'you', value: 'x' } 690 | const response = new Response(message) 691 | 692 | assert.strictEqual(client._channelSyncTimes.you, undefined) 693 | assert.ok(!client._batch(response)) 694 | assert.strictEqual(client._channelSyncTimes.you, undefined) 695 | } 696 | }, 697 | 698 | 'should not ignore messages that have all the appropriate properties': function () { 699 | const now = new Date() 700 | const message = { 701 | op: 'subscribe', 702 | to: 'you', 703 | value: ['{}', now], 704 | time: now 705 | } 706 | const response = new Response(message) 707 | 708 | assert.strictEqual(client._channelSyncTimes.you, undefined) 709 | assert.notStrictEqual(client._batch(response), false) 710 | assert.strictEqual(client._channelSyncTimes.you, now) 711 | }, 712 | 713 | 'should emit an event named for the "to" property value if there is a time that is greater than the current channelSyncTime': function () { 714 | let called = false 715 | const now = new Date() 716 | const message = { 717 | op: 'subscribe', 718 | to: 'you', 719 | value: ['{ "something": 1 }', now], 720 | time: now 721 | } 722 | const response = new Response(message) 723 | 724 | client._channelSyncTimes.you = now - HOUR 725 | 726 | client.emitNext = function (name, data) { 727 | called = true 728 | assert.strictEqual(name, response.getAttr('to')) 729 | assert.deepStrictEqual(data, JSON.parse(response.getAttr('value')[0])) 730 | } 731 | 732 | assert.notStrictEqual(client._batch(response), false) 733 | assert.strictEqual(client._channelSyncTimes.you, now) 734 | assert.ok(called) 735 | } 736 | }, 737 | 738 | '._createManager': { 739 | 'should create a manager that cannot open the same socket twice': function () { 740 | let neverCalledBefore = true 741 | let called = false 742 | 743 | client._createManager() 744 | 745 | client.manager.established = function () { 746 | called = true 747 | assert(neverCalledBefore) 748 | neverCalledBefore = false 749 | } 750 | 751 | client.manager.emit('connect') 752 | 753 | client._socket.emit('open') 754 | client._socket.emit('open') 755 | 756 | assert(called) 757 | }, 758 | 759 | 'should create a manager that stops listening to messages from a socket when the socket emits the close event': function () { 760 | let called = false 761 | 762 | client._createManager() 763 | 764 | client.manager.emit('connect') 765 | 766 | client._socket.emit('open') 767 | 768 | client._socket.on('message', function (data) { 769 | const json = JSON.parse(data) 770 | called = json.open 771 | assert(json.open) 772 | }) 773 | 774 | client._socket.emit('message', '{"open":1}') 775 | 776 | client._socket.emit('close') 777 | 778 | assert(!client._socket) 779 | 780 | assert(called) 781 | }, 782 | 783 | 'should create a manager that listens for the appropriate events': { 784 | enterState: function () { 785 | const state = 'test' 786 | let called = false 787 | 788 | client.emit = function (name) { 789 | called = true 790 | assert.strictEqual(name, state) 791 | } 792 | 793 | client._createManager() 794 | client.manager.emit('enterState', state) 795 | assert.ok(called) 796 | }, 797 | 798 | event: function () { 799 | const event = 'test' 800 | let called = false 801 | 802 | client.emit = function (name) { 803 | called = true 804 | assert.strictEqual(name, event) 805 | } 806 | 807 | client._createManager() 808 | client.manager.emit('event', event) 809 | assert.ok(called) 810 | }, 811 | 812 | 'connect and create a socket with the appropriate listeners': { 813 | open: function () { 814 | let called = false 815 | 816 | client._createManager() 817 | 818 | client.manager.can = function (name) { 819 | return name === 'established' 820 | } 821 | 822 | client.manager.established = function () { 823 | called = true 824 | } 825 | 826 | client.manager.emit('connect') 827 | 828 | client._socket.emit('open') 829 | assert.ok(called) 830 | }, 831 | 832 | close: function () { 833 | let called = false 834 | 835 | client._createManager() 836 | 837 | client.manager.disconnect = function () { 838 | called = true 839 | } 840 | 841 | client.manager.emit('connect') 842 | 843 | client._socket.emit('close') 844 | 845 | assert.ok(called) 846 | }, 847 | 848 | message: function () { 849 | let called = false 850 | const message = { test: 1 } 851 | 852 | client._createManager() 853 | 854 | client._messageReceived = function (msg) { 855 | called = true 856 | assert.strictEqual(msg, message) 857 | } 858 | 859 | client.manager.emit('connect') 860 | 861 | client._socket.emit('message', message) 862 | 863 | assert.ok(called) 864 | } 865 | }, 866 | 867 | activate: { 868 | 'and emits "authenticateMessage", "ready"': function () { 869 | let called = false 870 | let count = 0 871 | 872 | client.emit = function (name) { 873 | called = true 874 | 875 | count++ 876 | if (count === 1) { 877 | assert.strictEqual(name, 'authenticateMessage') 878 | } 879 | 880 | if (count === 2) { 881 | assert.strictEqual(name, 'ready') 882 | } 883 | } 884 | 885 | client._createManager() 886 | client.manager.emit('connect') 887 | client.manager.emit('activate') 888 | assert.ok(called) 889 | }, 890 | 891 | 'and _write()s the messages asynchronously': function (done) { 892 | let count = 0 893 | let called = 0 894 | 895 | while (count < 10) { 896 | client._queuedRequests.push({ test: count++ }) 897 | } 898 | 899 | client._restoreRequired = true 900 | 901 | client._write = function (message) { 902 | called += 1 903 | if (called === count) { 904 | done() 905 | } 906 | } 907 | 908 | client._createManager() 909 | client.manager.emit('connect') 910 | client.manager.emit('activate') 911 | } 912 | }, 913 | 914 | authenticate: function () { 915 | let called = false 916 | 917 | client._createManager() 918 | 919 | client.manager.activate = function () { 920 | called = true 921 | } 922 | 923 | client.manager.emit('authenticate') 924 | assert.ok(called) 925 | } 926 | } 927 | }, 928 | 929 | '._sendMessage': { 930 | 'should call send() on the _socket if the manager is activated': function () { 931 | let called = false 932 | const request = Request.buildSubscribe('status:/test/ticket/1') 933 | 934 | client.manager.is = function (state) { return state === 'activated' } 935 | 936 | client._socket = { 937 | send: function (data) { 938 | called = true 939 | assert.strictEqual(data, JSON.stringify(request.getMessage())) 940 | } 941 | } 942 | 943 | client._sendMessage(request) 944 | assert.ok(called) 945 | }, 946 | 947 | 'should queue the message if the client has been configured, but is not activated': function () { 948 | const request = Request.buildSet('status:/test/ticket/1', 'any_value') 949 | 950 | client.configure({}) 951 | client._sendMessage(request) 952 | assert.deepStrictEqual(request, client._queuedRequests[0]) 953 | }, 954 | 955 | 'should ignore the message if the client has not been configured': function () { 956 | const request = Request.buildSet('status:/test/ticket/1', 'any_value') 957 | 958 | assert.ok(!client._isConfigured) 959 | client._sendMessage(request) 960 | assert.strictEqual(client._queuedRequests.length, 0) 961 | } 962 | }, 963 | 964 | '._messageReceived': { 965 | 'handles incoming messages from the socket connection for': { 966 | err: function () { 967 | let called = false 968 | const message = { 969 | op: 'err' 970 | } 971 | const json = JSON.stringify(message) 972 | 973 | client.emitNext = function (name, data) { 974 | if (name === 'message:in') return 975 | called = true 976 | assert.strictEqual(name, message.op) 977 | assert.deepStrictEqual(data, message) 978 | } 979 | 980 | client._messageReceived(json) 981 | assert.ok(called) 982 | }, 983 | 984 | ack: function () { 985 | let called = false 986 | const message = { 987 | op: 'ack', 988 | value: 1 989 | } 990 | const json = JSON.stringify(message) 991 | 992 | client.emitNext = function (name, data) { 993 | if (name === 'message:in') return 994 | called = true 995 | assert.strictEqual(name, message.op) 996 | assert.deepStrictEqual(data, message) 997 | } 998 | 999 | client._messageReceived(json) 1000 | assert.ok(called) 1001 | }, 1002 | 1003 | get: function () { 1004 | let called = false 1005 | const message = { 1006 | op: 'get', 1007 | to: 'staus:/test/ticket/1' 1008 | } 1009 | const json = JSON.stringify(message) 1010 | 1011 | client.emitNext = function (name, data) { 1012 | if (name === 'message:in') return 1013 | called = true 1014 | assert.strictEqual(name, message.op) 1015 | assert.deepStrictEqual(data, message) 1016 | } 1017 | 1018 | client._messageReceived(json) 1019 | assert.ok(called) 1020 | }, 1021 | 1022 | sync: function () { 1023 | let called = false 1024 | const message = { 1025 | op: 'sync', 1026 | to: 'staus:/test/ticket/1' 1027 | } 1028 | const json = JSON.stringify(message) 1029 | 1030 | client._batch = function (msg) { 1031 | called = true 1032 | assert.deepStrictEqual(msg.message, message) 1033 | } 1034 | 1035 | client._messageReceived(json) 1036 | assert.ok(called) 1037 | }, 1038 | 1039 | 'everything else': function () { 1040 | let called = false 1041 | const message = { 1042 | op: 'something', 1043 | to: 'wherever' 1044 | } 1045 | const json = JSON.stringify(message) 1046 | 1047 | client.emitNext = function (name, data) { 1048 | if (name === 'message:in') return 1049 | 1050 | called = true 1051 | assert.strictEqual(name, message.to) 1052 | assert.deepStrictEqual(data, message) 1053 | } 1054 | 1055 | client._messageReceived(json) 1056 | assert.ok(called) 1057 | } 1058 | } 1059 | } 1060 | } 1061 | } 1062 | -------------------------------------------------------------------------------- /tests/state.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const StateMachine = require('../lib/state.js') 3 | let machine 4 | const Backoff = require('../lib/backoff.js') 5 | const sinon = require('sinon') 6 | let clock 7 | 8 | function maxBackoffStep (num) { 9 | if (num < Backoff.durations.length) { 10 | return Backoff.durations[num] + Backoff.maxSplay 11 | } else { 12 | return Backoff.fallback + Backoff.maxSplay 13 | } 14 | } 15 | 16 | exports['given a state machine'] = { 17 | beforeEach: function () { 18 | // create puts it in opened state. 19 | machine = StateMachine.create() 20 | machine._backoff.success() 21 | clock = sinon.useFakeTimers() 22 | }, 23 | 24 | afterEach: function () { 25 | clock.restore() 26 | }, 27 | 28 | 'calling start twice should not cause two connections': function (done) { 29 | let connecting = false 30 | 31 | machine.on('connect', function () { 32 | assert.ok(!connecting) 33 | connecting = true 34 | }) 35 | machine.start() 36 | assert.ok(machine.is('connecting')) 37 | machine.on('established', function () { 38 | assert.ok(machine.is('authenticating')) 39 | machine.activate() 40 | machine.start() 41 | assert.ok(machine.is('activated')) 42 | done() 43 | }) 44 | machine.established() 45 | }, 46 | 47 | 'if the user calls disconnect the machine will reconnect after a delay': function (done) { 48 | machine.connect() 49 | assert.ok(machine.is('connecting')) 50 | machine.once('connect', function () { 51 | machine.close() 52 | done() 53 | }) 54 | machine.disconnect() 55 | clock.tick(maxBackoffStep(0)) 56 | }, 57 | 58 | 'the first connection should begin connecting, after disconnected it should automatically reconnect': function (done) { 59 | machine.connect() 60 | assert.ok(machine.is('connecting')) 61 | 62 | let disconnected = false 63 | 64 | machine.once('disconnected', function () { 65 | disconnected = true 66 | }) 67 | 68 | machine.once('connect', function () { 69 | assert.ok(disconnected) 70 | done() 71 | }) 72 | 73 | machine.disconnect() 74 | clock.tick(maxBackoffStep(0)) 75 | }, 76 | 77 | 'connections that hang should be detected after connect timeout': function (done) { 78 | machine.disconnect = function () { 79 | done() 80 | } 81 | 82 | machine.connect() 83 | clock.tick(machine._connectTimeout + 1) 84 | }, 85 | 86 | 'should not get caught by timeout if connect takes too long': function (done) { 87 | let once = true 88 | let disconnects = 0 89 | 90 | machine.on('disconnect', function () { 91 | disconnects++ 92 | }) 93 | 94 | setTimeout(function () { 95 | // Only 1 disconnect due to manager.disconnect() 96 | assert.strictEqual(disconnects, 1) 97 | done() 98 | }, maxBackoffStep(0) + machine._connectTimeout) 99 | 100 | machine.on('connect', function () { 101 | if (once) { 102 | machine.disconnect() 103 | once = false 104 | } else { 105 | machine.established() 106 | } 107 | }) 108 | machine.connect() 109 | 110 | clock.tick(1 + maxBackoffStep(0) + machine._connectTimeout) 111 | }, 112 | 113 | 'connections that fail should cause exponential backoff, emit backoff times, finally emit unavailable': function (done) { 114 | let available = true 115 | let tries = Backoff.durations.length + 1 116 | const backoffs = [] 117 | 118 | machine.on('backoff', function (time, failures) { 119 | backoffs.push(failures) 120 | const step = failures - 1 121 | 122 | assert(failures > 0) 123 | assert(time > 0) 124 | assert(time > (maxBackoffStep(step) - Backoff.maxSplay)) 125 | assert(time < (maxBackoffStep(step) + Backoff.maxSplay)) 126 | }) 127 | 128 | machine.once('unavailable', function () { 129 | available = false 130 | 131 | assert(backoffs.length > 1) 132 | assert(backoffs.length === machine._backoff.failures) 133 | assert(backoffs.length === Backoff.durations.length) 134 | done() 135 | }) 136 | 137 | machine.on('connecting', function () { 138 | if (available && --tries) { 139 | machine.disconnect() 140 | } 141 | }) 142 | 143 | machine.connect() 144 | 145 | // Wait for all the backoffs + splay, then wait for fallback + splay as well 146 | let totalTimeToWait = 0 147 | for (let i = 0; i < Backoff.durations.length; i++) { 148 | totalTimeToWait += maxBackoffStep(i) 149 | } 150 | totalTimeToWait += Backoff.fallback + Backoff.maxSplay 151 | 152 | clock.tick(1 + totalTimeToWait) 153 | }, 154 | 155 | 'closing will cancel the guard timer': function () { 156 | assert(!machine._guard) 157 | machine.connect() 158 | assert(machine._guard) 159 | machine.close() 160 | assert(!machine._guard) 161 | }, 162 | 163 | 'should be able to attach a custom errorHandler': function () { 164 | const handler = function () {} 165 | machine.attachErrorHandler(handler) 166 | assert.strictEqual(machine.errorHandler, handler) 167 | }, 168 | 169 | 'should be able to override the custom errorHandler': function () { 170 | const handler1 = function () {} 171 | const handler2 = function () {} 172 | 173 | machine.attachErrorHandler(handler1) 174 | machine.attachErrorHandler(handler2) 175 | assert.strictEqual(machine.errorHandler, handler2) 176 | }, 177 | 178 | 'should only allow attaching a function as a custom state machine error handler': function () { 179 | assert(!machine.errorHandler) 180 | 181 | machine.attachErrorHandler(23) 182 | assert(!machine.errorHandler) 183 | 184 | machine.attachErrorHandler({}) 185 | assert(!machine.errorHandler) 186 | 187 | machine.attachErrorHandler('error') 188 | assert(!machine.errorHandler) 189 | 190 | machine.attachErrorHandler(function () {}) 191 | assert(machine.errorHandler) 192 | } 193 | } 194 | 195 | // When this module is the script being run, run the tests: 196 | if (module === require.main) { 197 | const mocha = require('child_process').spawn('mocha', ['--colors', '--ui', 'exports', '--reporter', 'spec', __filename]) 198 | mocha.stdout.pipe(process.stdout) 199 | mocha.stderr.pipe(process.stderr) 200 | } 201 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './lib/index.js', 5 | externals: { 6 | minilog: 'Minilog', 7 | 'engine.io-client': 'eio' 8 | }, 9 | mode: 'production', 10 | output: { 11 | filename: 'radar_client.js', 12 | library: 'RadarClient', 13 | libraryTarget: 'var', 14 | path: path.resolve(__dirname, 'dist') 15 | } 16 | } 17 | --------------------------------------------------------------------------------