├── .ackrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── backbone-state-tracking.js ├── default-context-model.js ├── filtered-rooms.js ├── index.js ├── limited-collection.js ├── live-collection.js ├── realtime-client.js ├── room-collection.js ├── room-model.js ├── simple-filtered-collection.js ├── sorted-array-index-search.js ├── sorts-filters.js ├── template-subscription.js └── wrap-extension.js ├── package-lock.json ├── package.json └── test ├── .eslintrc.json ├── limited-collection-test.js ├── room-collection-test.js ├── simple-filtered-collection-test.js ├── sorted-array-index-search-test.js └── sorts-filters.js /.ackrc: -------------------------------------------------------------------------------- 1 | --ignore-dir 2 | node_modules/ 3 | --type-set=hbs=.hbs 4 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #editorconfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [{**.js,**.json,**.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "node": true 5 | }, 6 | "plugins": [ 7 | "node" 8 | ], 9 | "extends": "eslint:recommended", 10 | "rules": { 11 | "indent": "off", 12 | "comma-dangle": "off", 13 | "quotes": "off", 14 | "eqeqeq": [ 15 | "warn", 16 | "allow-null" 17 | ], 18 | "strict": [ 19 | "error", 20 | "safe" 21 | ], 22 | "no-unused-vars": [ 23 | "warn" 24 | ], 25 | "no-extra-boolean-cast": [ 26 | "warn" 27 | ], 28 | "complexity": [ 29 | "error", 30 | { 31 | "max": 12 32 | } 33 | ], 34 | "max-statements-per-line": [ 35 | "error", 36 | { 37 | "max": 3 38 | } 39 | ], 40 | "no-debugger": "error", 41 | "no-dupe-keys": "error", 42 | "no-unsafe-finally": "error", 43 | "no-with": "error", 44 | "no-useless-call": "error", 45 | "no-spaced-func": "error", 46 | "max-statements": [ 47 | "warn", 48 | 30 49 | ], 50 | "max-depth": [ 51 | "error", 52 | 4 53 | ], 54 | "no-throw-literal": [ 55 | "error" 56 | ], 57 | "no-sequences": "error", 58 | "no-warning-comments": [ 59 | "warn", 60 | { 61 | "terms": [ 62 | "fixme", 63 | "xxx" 64 | ], 65 | "location": "anywhere" 66 | } 67 | ], 68 | "radix": "error", 69 | "yoda": "error", 70 | "no-nested-ternary": "warn", 71 | "no-whitespace-before-property": "error", 72 | "no-trailing-spaces": [ 73 | "error", 74 | { 75 | "skipBlankLines": true 76 | } 77 | ], 78 | "space-in-parens": [ 79 | "warn", 80 | "never" 81 | ], 82 | "max-nested-callbacks": [ 83 | "error", 84 | 6 85 | ], 86 | "eol-last": "warn", 87 | "no-mixed-spaces-and-tabs": "error", 88 | "no-negated-condition": "warn", 89 | "no-unneeded-ternary": "error", 90 | "no-multi-spaces": [ 91 | "warn", 92 | { 93 | "exceptions": { 94 | "Property": true 95 | } 96 | } 97 | ], 98 | "key-spacing": [ 99 | "warn", 100 | { 101 | "singleLine": { 102 | "beforeColon": false, 103 | "afterColon": true 104 | }, 105 | "multiLine": { 106 | "beforeColon": false, 107 | "afterColon": true, 108 | "mode": "minimum" 109 | } 110 | } 111 | ], 112 | "node/no-missing-require": "error", 113 | "node/no-unsupported-features": [ 114 | "error", 115 | { 116 | "version": 0.1 117 | } 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "disallowQuotedKeysInObjects": false 4 | } 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "lastsemic": true, 3 | "browser": true, 4 | "unused": "strict", 5 | "loopfunc": true, 6 | "globalstrict": true, 7 | "undef": true, 8 | "predef": [ 9 | "module", 10 | "require" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | git: 5 | depth: 10 6 | sudo: false 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2015, Troupe Technology Limited 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project moved to https://gitlab.com/gitlab-org/gitter/realtime-client 2 | -------------------------------------------------------------------------------- /lib/backbone-state-tracking.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* Inspired by https://github.com/hernantz/backbone.sos */ 4 | var LOADING_EVENTS = 'request'; 5 | var LOADED_EVENTS = 'sync error reset'; 6 | 7 | function loadingChange(obj, newState) { 8 | newState = !!newState; 9 | var current = !!obj.loading; 10 | obj.loading = newState; 11 | if (newState !== current) { 12 | obj.trigger(newState ? "loading" : "loaded", obj); 13 | obj.trigger('loading:change', obj, newState); 14 | } 15 | } 16 | 17 | function onLoading() { 18 | loadingChange(this, true); 19 | } 20 | 21 | function onLoaded() { 22 | loadingChange(this, false); 23 | } 24 | 25 | module.exports = { 26 | track: function(model) { 27 | model.loading = false; 28 | model.stateTracking = true; 29 | model.listenTo(model, LOADING_EVENTS, onLoading); 30 | model.listenTo(model, LOADED_EVENTS, onLoaded); 31 | }, 32 | untrack: function(model) { 33 | model.stateTracking = false; 34 | model.stopListening(model, LOADING_EVENTS, onLoading); 35 | model.stopListening(model, LOADED_EVENTS, onLoaded); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/default-context-model.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Backbone = require('backbone'); 4 | 5 | // Create a context model for backwards compatibility for clients 6 | // which have not supplied one. The default context model only binds 7 | // against userId, and only does so once 8 | module.exports = function(client, suppliedUserId) { 9 | var userId = suppliedUserId || client.getUserId(); 10 | var contextModel = new Backbone.Model({ userId: userId }); 11 | 12 | if (!userId) { 13 | client.on('change:userId', function(userId) { 14 | contextModel.set({ userId: userId }); 15 | }); 16 | } 17 | 18 | return contextModel; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/filtered-rooms.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var sortsFilters = require('./sorts-filters'); 4 | var SimpleFilteredCollection = require('./simple-filtered-collection'); 5 | 6 | function apply(collection, filter, sort) { 7 | return new SimpleFilteredCollection([], { 8 | model: collection.model, 9 | collection: collection, 10 | comparator: sort, 11 | filter: filter, 12 | autoResort: true 13 | }); 14 | } 15 | 16 | module.exports = { 17 | favourites: function(collection) { 18 | return apply(collection, sortsFilters.model.favourites.filter, sortsFilters.model.favourites.sort); 19 | }, 20 | recents: function(collection) { 21 | return apply(collection, sortsFilters.model.recents.filter, sortsFilters.model.recents.sort); 22 | }, 23 | unreads: function(collection) { 24 | return apply(collection, sortsFilters.model.unreads.filter, sortsFilters.model.unreads.sort); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | LiveCollection: require('./live-collection'), 5 | RealtimeClient: require('./realtime-client'), 6 | RoomCollection: require('./room-collection'), 7 | RoomModel: require('./room-model'), 8 | filteredRooms: require('./filtered-rooms'), 9 | sortsFilters: require('./sorts-filters'), 10 | wrapExtension: require('./wrap-extension') 11 | }; 12 | -------------------------------------------------------------------------------- /lib/limited-collection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Backbone = require('backbone'); 4 | var _ = require('underscore'); 5 | 6 | var LimitedCollection = Backbone.Collection.extend({ 7 | constructor: function(models, options) { 8 | if(!options || !options.collection) { 9 | throw new Error('A valid collection must be passed to a new instance of LimitedCollection'); 10 | } 11 | 12 | this._collection = options.collection; 13 | this._maxLength = options.maxLength || 10; 14 | 15 | var resetModels = this._collection.slice(0, this._maxLength); 16 | 17 | Backbone.Collection.call(this, resetModels, _.extend({ }, options, { 18 | comparator: this._collection.comparator 19 | })); 20 | 21 | this.listenTo(this._collection, 'add', this._onAddEvent); 22 | this.listenTo(this._collection, 'remove', this._onRemoveEvent); 23 | this.listenTo(this._collection, 'reset', this._resetFromBase); 24 | this.listenTo(this._collection, 'sort', this._resetFromSort); 25 | }, 26 | 27 | _onAddEvent: function(model /*, collection, options*/) { 28 | var index = this._collection.indexOf(model); 29 | if (index >= this._maxLength) { 30 | return; 31 | } 32 | 33 | return this.add(model); 34 | }, 35 | 36 | _onRemoveEvent: function(model/*, collection, options*/) { 37 | var didRemove = this.remove(model); 38 | if (didRemove) { 39 | this._resetFromSort(); 40 | } 41 | }, 42 | 43 | _resetFromBase: function() { 44 | var resetModels = this._collection.slice(0, this._maxLength); 45 | this.comparator = this._collection.comparator; 46 | 47 | return this.reset(resetModels); 48 | }, 49 | 50 | _resetFromSort: function() { 51 | var resetModels = this._collection.slice(0, this._maxLength); 52 | this.comparator = this._collection.comparator; 53 | this.set(resetModels, { 54 | add: true, 55 | remote: true, 56 | merge: false 57 | }) 58 | }, 59 | 60 | getUnderlying: function() { 61 | return this._collection; 62 | } 63 | }); 64 | 65 | 66 | 67 | module.exports = LimitedCollection; 68 | -------------------------------------------------------------------------------- /lib/live-collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('underscore'); 4 | var Backbone = require('backbone'); 5 | var backboneUrlResolver = require('backbone-url-resolver'); 6 | var defaultContextModel = require('./default-context-model'); 7 | var debug = require('debug-proxy')('grc:live-collection'); 8 | var backboneStateTracking = require('./backbone-state-tracking'); 9 | 10 | var PATCH_TIMEOUT = 2000; // 2000ms before a patch gives up 11 | 12 | function getOptionOrProperty(object, options, name) { 13 | if (options[name]) return _.result(options, name); 14 | return _.result(object, name); 15 | } 16 | 17 | module.exports = Backbone.Collection.extend({ 18 | modelName: '', 19 | /** 20 | * Indicates that when a subscribe occurs, the server 21 | * will return a snapshot. Defaults to true, but can 22 | * also be a function returning true or false. 23 | * TODO: allow this to be passed in `options` 24 | */ 25 | subscribeReturnsSnapshot: true, 26 | constructor: function(models, options) { 27 | var defaults = { snapshot: true }; 28 | options = _.extend(defaults, options); 29 | 30 | if (options.client) { 31 | this.client = options.client; 32 | } else { 33 | this.client = _.result(this, 'client', null); 34 | } 35 | 36 | if (!this.client) { 37 | throw new Error('LiveCollection requires a client to be passed in via options or via client property'); 38 | } 39 | 40 | // Call super constructor 41 | Backbone.Collection.prototype.constructor.call(this, models, options); 42 | 43 | // Setup the context-model 44 | var contextModel = getOptionOrProperty(this, options, 'contextModel'); 45 | if (!contextModel) { 46 | contextModel = defaultContextModel(this.client, this.userId); 47 | } 48 | this.contextModel = contextModel; 49 | 50 | this.urlModel = this._getUrlModel(options); 51 | backboneStateTracking.track(this); 52 | 53 | if(options && options.listen) { 54 | this.listen(); 55 | } 56 | 57 | }, 58 | 59 | addWaiter: function(id, callback, timeout) { 60 | debug('Waiting for id %s in collection', id); 61 | 62 | if(!id) return; 63 | 64 | var self = this; 65 | var idAttribute = this.model.prototype.idAttribute || 'id'; 66 | 67 | var actionPerformed = false; 68 | 69 | function done(model) { 70 | clearTimeout(timeoutRef); 71 | 72 | self.off('add', check, id); 73 | self.off('change:id', check, id); 74 | 75 | /* This check is probably not strictly neccessary */ 76 | if(actionPerformed) { 77 | debug('Warning: waiter function called twice.'); 78 | return; 79 | } 80 | actionPerformed = true; 81 | 82 | if(model) { 83 | callback.apply(self, [model]); 84 | } else { 85 | callback.apply(self, []); 86 | } 87 | } 88 | 89 | function check(model) { 90 | if(model && model[idAttribute] === id) { 91 | done(model); 92 | } 93 | } 94 | 95 | var timeoutRef = setTimeout(function() { 96 | done(); 97 | }, timeout); 98 | 99 | this.on('add', check, id); 100 | this.on('change:id', check, id); 101 | }, 102 | 103 | listen: function() { 104 | if (this.templateSubscription) throw new Error('Already subscribed'); 105 | 106 | this.templateSubscription = this.client.subscribeTemplate({ 107 | urlModel: this.urlModel, 108 | onMessage: this._onDataChange.bind(this), 109 | getSnapshotState: this.getSnapshotState && this.getSnapshotState.bind(this), 110 | handleSnapshot: this.handleSnapshot.bind(this) 111 | }); 112 | 113 | this.listenTo(this.templateSubscription, 'resubscribe', function() { 114 | debug('Resetting collection on resubscribe: %s', this.url()); 115 | this._resetOptional(); 116 | 117 | var subscribeReturnsSnapshot = _.result(this, 'subscribeReturnsSnapshot'); 118 | if (subscribeReturnsSnapshot) { 119 | // Triggering a "request" will let listeners know that 120 | // data is being loaded and the collection is in a 121 | // loading state 122 | this.trigger('request'); 123 | } 124 | }); 125 | 126 | this.listenTo(this.templateSubscription, 'unsubscribe', function() { 127 | debug('Resetting collection on unsubscribe: %s', this.url()); 128 | this._resetOptional(); 129 | }); 130 | 131 | this.listenTo(this.templateSubscription, 'subscriptionError', function(channel, error) { 132 | this.trigger('error', this, error, { reason: 'subscription_failed', channel: channel }); 133 | }); 134 | 135 | }, 136 | 137 | unlisten: function() { 138 | if (!this.templateSubscription) return; 139 | this.templateSubscription.cancel(); 140 | this.stopListening(this.templateSubscription); 141 | this.templateSubscription = null; 142 | }, 143 | 144 | // Note that this may be overridden by child classes 145 | url: function() { 146 | if (!this.urlTemplate) throw new Error('Please provide either a url or urlTemplate'); 147 | return this.urlModel.get('url'); 148 | }, 149 | 150 | _getUrlModel: function(options) { 151 | var url = getOptionOrProperty(this, options, 'urlTemplate') || 152 | getOptionOrProperty(this, options, 'channel') || /* channel is deprecated */ 153 | getOptionOrProperty(this, options, 'url'); 154 | 155 | if (!url) throw new Error('channel is required'); 156 | 157 | return backboneUrlResolver(url, this.contextModel); 158 | }, 159 | 160 | _resetOptional: function() { 161 | if (!this.length) return; 162 | 163 | // Performance tweak 164 | // Don't re-issue resets on empty collections 165 | // as this may cause a whole lot of unneccassary 166 | // dom manipulation 167 | this.reset(); 168 | }, 169 | 170 | handleSnapshot: function(snapshot) { 171 | /** 172 | * Don't remove items from the collections, as there is a greater 173 | * chance that they've been added on the client than that they've 174 | * been removed from the server. One day we may want to handle the 175 | * case that the server object has been removed, but it's not that 176 | * likely and doesn't warrant the extra complexity 177 | */ 178 | var options = { 179 | parse: true, /* parse the items */ 180 | remove: true, 181 | add: true, /* add new items */ 182 | merge: true /* merge into items that already exist */ 183 | }; 184 | 185 | if(this.length > 0) { 186 | debug('Performing merge on snapshot (current length is %s)', this.length); 187 | this.set(snapshot, options); 188 | } else { 189 | debug('Performing reset on snapshot'); 190 | // trash it and add all in one go 191 | this.reset(snapshot, options); 192 | } 193 | 194 | this.trigger('sync'); 195 | this.trigger('snapshot'); 196 | }, 197 | 198 | findExistingModel: function(id, newModel) { 199 | var existing = this.get(id); 200 | if(existing) return existing; 201 | 202 | if(this.findModelForOptimisticMerge) { 203 | existing = this.findModelForOptimisticMerge(newModel); 204 | } 205 | 206 | return existing; 207 | }, 208 | 209 | operationIsUpToDate: function(operation, existing, newModel) { 210 | var existingVersion = existing.get('v') ? existing.get('v') : 0; 211 | var incomingVersion = newModel.v ? newModel.v : 0; 212 | 213 | // Create operations are always allowed 214 | if(operation === 'create') return true; 215 | 216 | // Existing version is unversioned? Allow 217 | if(!existingVersion) return true; 218 | 219 | // New operation is unversioned? Dodgy. Only allow if the operation is a patch 220 | if(!incomingVersion) return operation === 'patch'; 221 | 222 | if(operation === 'patch') { 223 | return incomingVersion >= existingVersion; 224 | } 225 | 226 | return incomingVersion > existingVersion; 227 | }, 228 | 229 | // TODO: make this dude tighter 230 | applyUpdate: function(operation, existingModel, newAttributes, parsed, options) { 231 | if(this.operationIsUpToDate(operation, existingModel, newAttributes)) { 232 | debug('Performing %s: %j', operation, newAttributes); 233 | if (!options) options = {}; 234 | 235 | existingModel.set(parsed.attributes, options); 236 | existingModel.trigger('sync', existingModel, { live: options }); 237 | } else { 238 | debug('Ignoring out-of-date update. existing=%j, new=%j', existingModel.attributes, newAttributes); 239 | } 240 | }, 241 | 242 | /** 243 | * Patch an existing model. 244 | * If the model does not exist, will wait for a small period (`PATCH_TIMEOUT`) 245 | * in case the live collection has not yet updated. 246 | * 247 | * The optional callback has two parameters [id, found] 248 | * * `id` is the id of the model passed into the call 249 | * * `found` is true if the model was found in the collection 250 | */ 251 | patch: function(id, newModel, options, callback) { 252 | debug('Request to patch %s with %j', id, newModel); 253 | 254 | var self = this; 255 | 256 | if(this.transformModel) newModel = this.transformModel(newModel); 257 | var parsed = new this.model(newModel, { parse: true }); 258 | var existing = this.findExistingModel(id, parsed); 259 | if(existing) { 260 | this.applyUpdate('patch', existing, newModel, parsed, options); 261 | if (callback) callback(id, true); 262 | return; 263 | } 264 | 265 | /* Existing model does not exist */ 266 | this.addWaiter(id, function(existing) { 267 | if(!existing) { 268 | debug('Unable to find model %s', id); 269 | if (callback) callback(id, false); 270 | return; 271 | } 272 | 273 | self.applyUpdate('patch', existing, newModel, parsed, options); 274 | if (callback) callback(id, true); 275 | }, PATCH_TIMEOUT); 276 | }, 277 | 278 | _onDataChange: function(data) { 279 | var self = this; 280 | var operation = data.operation; 281 | var newModel = data.model; 282 | var idAttribute = this.model.prototype.idAttribute || 'id'; 283 | var id = newModel[idAttribute]; 284 | 285 | if(this.ignoreDataChange) { 286 | if(this.ignoreDataChange(data)) return; 287 | } 288 | 289 | if(this.transformModel) newModel = this.transformModel(newModel); 290 | var parsed = new this.model(newModel, { parse: true }); 291 | var existing = this.findExistingModel(id, parsed); 292 | 293 | switch(operation) { 294 | case 'create': 295 | case 'patch': 296 | case 'update': 297 | // There can be existing documents for create events if the doc was created on this 298 | // client and lazy-inserted into the collection 299 | if(existing) { 300 | this.applyUpdate(operation, existing, newModel, parsed); 301 | break; 302 | } else { 303 | /* Can't find an existing model */ 304 | if(operation === 'patch') { 305 | this.addWaiter(id, function(existing) { 306 | if(!existing) { 307 | debug('Unable to find model id=%s', id); 308 | return; 309 | } 310 | 311 | self.applyUpdate('patch', existing, newModel, parsed); 312 | }, PATCH_TIMEOUT); 313 | 314 | } else { 315 | this.add(parsed); 316 | } 317 | } 318 | 319 | break; 320 | 321 | case 'remove': 322 | if(existing) { 323 | this.remove(existing); 324 | } 325 | 326 | break; 327 | 328 | default: 329 | debug("Unknown operation %s, ignoring", operation); 330 | 331 | } 332 | } 333 | }); 334 | -------------------------------------------------------------------------------- /lib/realtime-client.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Halley = require('halley/backbone'); 4 | var log = require('loglevel'); 5 | var _ = require('underscore'); 6 | var Backbone = require('backbone'); 7 | var TemplateSubscription = require('./template-subscription'); 8 | var debug = require('debug-proxy')('grc:client'); 9 | var wrapExtension = require('./wrap-extension'); 10 | 11 | 12 | Halley.Promise.config({ 13 | warnings: false, 14 | longStackTraces: false, 15 | cancellation: true 16 | }); 17 | 18 | /* @const */ 19 | var FAYE_PREFIX = '/api'; 20 | var FAYE_PREFIX_RE = /^\/api/; 21 | var DEFAULT_FAYE_URL = 'https://ws.gitter.im/bayeux'; 22 | var PING_RESPONSE_TIMEOUT = 30000; 23 | 24 | var ErrorLogger = function() {}; 25 | ErrorLogger.prototype.incoming = wrapExtension(function(message, callback) { 26 | if(message.error) { 27 | debug('Bayeux error: %j', message); 28 | } 29 | 30 | callback(message); 31 | }); 32 | 33 | var ClientAuth = function(client, options) { 34 | this.client = client; 35 | if(options.authProvider) { 36 | this.authProvider = options.authProvider; 37 | } else { 38 | this.authProvider = function(callback) { 39 | return callback({ token: options.token }); 40 | }; 41 | } 42 | }; 43 | 44 | ClientAuth.prototype.outgoing = function(message, callback) { 45 | if(message.channel !== '/meta/handshake') return callback(message); 46 | 47 | var uniqueClientId = this.client.uniqueClientId; 48 | 49 | this.client.clientId = null; 50 | debug("Rehandshaking realtime connection"); 51 | 52 | this.authProvider(function(authInfo) { 53 | if(!message.ext) message.ext = {}; 54 | _.extend(message.ext, authInfo); 55 | message.ext.uniqueClientId = uniqueClientId; 56 | message.ext.realtimeLibrary = 'halley'; 57 | callback(message); 58 | }); 59 | }; 60 | 61 | 62 | ClientAuth.prototype.incoming = wrapExtension(function(message, callback) { 63 | if(message.channel !== '/meta/handshake') return callback(message); 64 | 65 | if(message.successful) { 66 | // New clientId? 67 | if(this.client.clientId !== message.clientId) { 68 | this.client.clientId = message.clientId; 69 | debug("Realtime reestablished. New id is %s", this.client.clientId); 70 | this.client.trigger('newConnectionEstablished'); 71 | } 72 | 73 | if (message.ext && message.ext.context) { 74 | if (message.ext.context.user) { 75 | this.client.user.set(message.ext.context.user); 76 | } else if(message.ext.context.userId) { 77 | this.client.user.set({ id: message.ext.context.userId }); 78 | } 79 | } 80 | 81 | // Clear any transport problem indicators 82 | this.client._transportUp(); 83 | } 84 | 85 | callback(message); 86 | }); 87 | 88 | var SequenceGapDetectorExtension = function(client) { 89 | var self = this; 90 | this.client = client; 91 | this._seq = 0; 92 | 93 | client.on('newConnectionEstablished', function() { 94 | self._seq = 0; 95 | }); 96 | }; 97 | 98 | /** 99 | * Only perform a sequence reset at most once every 5 minutes 100 | */ 101 | var MIN_PERIOD_BETWEEN_RESETS = 300 * 1000; 102 | 103 | SequenceGapDetectorExtension.prototype = { 104 | incoming: wrapExtension(function(message, callback) { 105 | var c = message.ext && message.ext.c; 106 | var channel = message.channel; 107 | 108 | if (this.lastResetTime) { 109 | var timeSinceLastReset = Date.now() - this.lastResetTime; 110 | if (timeSinceLastReset < MIN_PERIOD_BETWEEN_RESETS) { 111 | return callback(message); 112 | } 113 | } 114 | 115 | if(c && channel && channel.indexOf('/meta') !== 0) { 116 | if (c === 1) { 117 | this._seq = 1; 118 | this._seqStarted = true; 119 | return callback(message); 120 | } 121 | 122 | if (!this._seqStarted) return callback(message); 123 | 124 | var current = this._seq; 125 | this._seq = c; 126 | 127 | if (c !== current + 1) { 128 | // Stop listening to sequence messages until we get a `1` again... 129 | delete this._seqStarted; 130 | delete this._seq; 131 | this.lastResetTime = Date.now(); 132 | 133 | // Reset the connection 134 | log.warn('rtc: Message on channel ' + channel + ' out of sequence. Expected ' + (current + 1) + ' got ' + c + '. Resetting ' + this.client.clientId); 135 | this.client.trigger('sequence.error'); 136 | this.client.reset(this.client.clientId); 137 | } 138 | 139 | } 140 | callback(message); 141 | }) 142 | }; 143 | 144 | var SnapshotExtension = function(client) { 145 | this.client = client; 146 | this._listeners = {}; 147 | this._stateProvider = {}; 148 | this._subscribeTimers = {}; 149 | }; 150 | 151 | SnapshotExtension.prototype = { 152 | outgoing: function(message, callback) { 153 | if (message.channel !== '/meta/subscribe') return callback(message); 154 | var subscribeChannel = message.subscription.replace(FAYE_PREFIX_RE, ''); 155 | this._subscribeTimers[subscribeChannel] = Date.now(); // Record start time 156 | 157 | 158 | function first(array, iterator) { 159 | if (!array || !array.length) return; 160 | for (var i = 0; i < array.length; i++) { 161 | var value = iterator(array[i], i); 162 | if (value !== undefined) return value; 163 | } 164 | } 165 | 166 | // Generic listeners register with channel `null` and receive all snapshot requests 167 | var genericListeners = this._listeners[null]; 168 | 169 | /* NB: snapshot state can be 'false' so don't compare with falsy values */ 170 | var snapshotState = first(genericListeners, function(listener) { 171 | if(!listener.getSnapshotStateForChannel) return; 172 | 173 | return listener.getSnapshotStateForChannel(subscribeChannel); 174 | }); 175 | 176 | // Only try the non-generic listeners if the generic ones did not return results 177 | if (snapshotState === undefined) { 178 | var listeners = this._listeners[subscribeChannel]; 179 | snapshotState = first(listeners, function(listener) { 180 | if(!listener.getSnapshotState) return; 181 | return listener.getSnapshotState(); 182 | }); 183 | } 184 | 185 | if (snapshotState !== undefined) { 186 | if (!message.ext) message.ext = {}; 187 | message.ext.snapshot = snapshotState; 188 | } 189 | 190 | // Add generic subscribe options 191 | var subscribeOptions = first(genericListeners, function(listener) { 192 | if(!listener.getSubscribeOptions) return; 193 | 194 | return listener.getSubscribeOptions(subscribeChannel); 195 | }); 196 | 197 | // Subscribe options must be a hash. Graft the values 198 | // onto the ext object 199 | if (subscribeOptions) { 200 | if (!message.ext) message.ext = {}; 201 | _.extend(message.ext, subscribeOptions); 202 | } 203 | 204 | callback(message); 205 | }, 206 | 207 | incoming: function(message, callback) { 208 | if (message.channel !== '/meta/subscribe' || !message.ext || !message.ext.snapshot) return callback(message); 209 | // Add some statistics into the mix 210 | var startTime = this._subscribeTimers[message.subscription]; 211 | if (startTime) { 212 | delete this._subscribeTimers[message.subscription]; 213 | var totalTime = Date.now() - startTime; 214 | 215 | if (totalTime > 400) { 216 | var lastPart = message.subscription.split(/\//).pop(); 217 | this.client.trigger('stats', 'time', 'faye.subscribe.time.' + lastPart, totalTime); 218 | 219 | debug('Subscription to %s took %sms', message.subscription, totalTime); 220 | } 221 | } 222 | 223 | var channelListeners = this._listeners; 224 | var subscriptionChannel = message.subscription.replace(FAYE_PREFIX_RE, ''); 225 | 226 | function invokeHandleSnapshot(channel) { 227 | var listeners = channelListeners[channel]; 228 | var snapshot = message.ext.snapshot; 229 | 230 | if (!listeners) return; 231 | 232 | listeners.forEach(function(listener) { 233 | if(listener.handleSnapshot) { 234 | listener.handleSnapshot(snapshot, subscriptionChannel); 235 | } 236 | }); 237 | } 238 | 239 | invokeHandleSnapshot(null); 240 | invokeHandleSnapshot(subscriptionChannel); 241 | 242 | callback(message); 243 | }, 244 | 245 | registerSnapshotHandler: function(channel, snapshotHandler) { 246 | var list = this._listeners[channel]; 247 | if (list) { 248 | list.push(snapshotHandler); 249 | } else { 250 | list = [snapshotHandler]; 251 | this._listeners[channel] = list; 252 | } 253 | }, 254 | 255 | deregisterSnapshotHandler: function(channel, snapshotHandler) { 256 | var list = this._listeners[channel]; 257 | if (!list) return; 258 | 259 | // Remove the handler 260 | list = list.filter(function(handler) { return handler !== snapshotHandler; }); 261 | 262 | if (list.length) { 263 | this._listeners[channel] = list; 264 | } else { 265 | delete this._listeners[channel]; 266 | } 267 | } 268 | }; 269 | 270 | function getHalleyOptions(options) { 271 | var halleyOptions; 272 | if (options.fayeOptions) { 273 | halleyOptions = options.fayeOptions; 274 | 275 | /* Backwards compatibility for Faye */ 276 | if (halleyOptions.timeout) { 277 | halleyOptions.timeout = halleyOptions.timeout * 1000; 278 | } 279 | 280 | if (halleyOptions.interval) { 281 | halleyOptions.interval = halleyOptions.interval * 1000; 282 | } 283 | 284 | if (halleyOptions.retry) { 285 | halleyOptions.retry = halleyOptions.retry * 1000; 286 | } 287 | } 288 | 289 | if (options.halleyOptions) { 290 | halleyOptions = options.halleyOptions; 291 | } 292 | 293 | if (options.websocketsDisabled) { 294 | halleyOptions = halleyOptions || {}; 295 | if (!halleyOptions.disabled) halleyOptions.disabled = []; 296 | 297 | halleyOptions.disabled.push('websocket'); 298 | } 299 | 300 | return halleyOptions; 301 | } 302 | 303 | function RealtimeClient(options) { 304 | this.uniqueClientId = Math.floor(Math.random() * 100000); 305 | 306 | this.user = new Backbone.Model(); 307 | var halleyOptions = getHalleyOptions(options); 308 | var client = new Halley.Client(options.fayeUrl || DEFAULT_FAYE_URL, halleyOptions); 309 | 310 | client.addExtension(new ClientAuth(this, options)); 311 | client.addExtension(new SequenceGapDetectorExtension(this)); 312 | client.addExtension(new ErrorLogger(this)); 313 | 314 | this.snapshots = new SnapshotExtension(this); 315 | client.addExtension(this.snapshots); 316 | 317 | if(options.extensions) { 318 | options.extensions.forEach(function(extension) { 319 | client.addExtension(extension); 320 | }); 321 | } 322 | 323 | // Connect early in order to obtain the userId 324 | client.connect(); 325 | 326 | this.listenTo(this.user, 'change:id', function() { 327 | this.trigger('change:userId', this.user.id); 328 | }); 329 | 330 | // Initially, the transport is down 331 | this._transportDown(10 /* seconds */); 332 | 333 | // Deprecated in favour of connection:down 334 | this.listenTo(client, 'transport:down', function() { 335 | debug('Transport down'); 336 | this._transportDown(); 337 | }); 338 | 339 | // Deprecated in favour of connection:up 340 | this.listenTo(client, 'transport:up', function() { 341 | debug('Transport up'); 342 | this._transportUp(); 343 | }); 344 | 345 | this.listenTo(client, 'connection:down', function() { 346 | this.trigger('connection:down'); 347 | }); 348 | 349 | this.listenTo(client, 'connection:up', function() { 350 | this.trigger('connection:up'); 351 | }); 352 | 353 | this.client = client; 354 | } 355 | 356 | _.extend(RealtimeClient.prototype, Backbone.Events, { 357 | 358 | reset: function(clientIdOnPing) { 359 | if(clientIdOnPing !== this.clientId) { 360 | debug("Ignoring reset request as clientId has changed."); 361 | return; 362 | } 363 | 364 | debug("Client reset requested"); 365 | 366 | this.trigger('stats', 'event', 'faye.ping.reset'); 367 | this.clientId = null; 368 | this.client.reset(); 369 | }, 370 | 371 | subscribe: function(channel, callback, context) { 372 | var fayeChannel = FAYE_PREFIX + channel; 373 | debug('Subscribing to %s', channel); 374 | 375 | return this.client.subscribe(fayeChannel, callback, context); 376 | }, 377 | 378 | subscribeTemplate: function(options) { 379 | return new TemplateSubscription(this, options); 380 | }, 381 | 382 | publish: function(channel, message) { 383 | return this.client.publish(FAYE_PREFIX + channel, message); 384 | }, 385 | 386 | disconnect: function () { 387 | this.client.disconnect(); 388 | }, 389 | 390 | registerSnapshotHandler: function(channel, snapshotHandler) { 391 | return this.snapshots.registerSnapshotHandler(channel, snapshotHandler); 392 | }, 393 | 394 | deregisterSnapshotHandler: function(channel, snapshotHandler) { 395 | return this.snapshots.deregisterSnapshotHandler(channel, snapshotHandler); 396 | }, 397 | 398 | testConnection: function(reason, callback) { 399 | /* Wait until the connection is established before attempting the test */ 400 | var originalClientId = this.clientId; 401 | 402 | if (!originalClientId || this._pingOutstanding) { 403 | debug('Ignoring test connection request'); 404 | return callback && callback(); 405 | } 406 | 407 | debug('Testing connection: reason=%s, clientId=%s', reason, originalClientId); 408 | 409 | if (reason !== 'ping') { 410 | this.trigger('testConnection', reason); 411 | debug('Testing connection due to %s', reason); 412 | } 413 | 414 | this._pingOutstanding = true; 415 | 416 | return this.client.publish(FAYE_PREFIX + '/v1/ping2', { reason: reason }, { deadline: PING_RESPONSE_TIMEOUT }) 417 | .bind(this) 418 | .timeout(PING_RESPONSE_TIMEOUT + 1000, 'Ping timeout') 419 | .then(function() { 420 | debug('Server ping succeeded'); 421 | return true; 422 | }) 423 | .catch(function(error) { 424 | debug('Server ping error %j', error); 425 | this.reset(originalClientId); 426 | return false; 427 | }) 428 | .finally(function() { 429 | this._pingOutstanding = false; 430 | }) 431 | .asCallback(callback); 432 | }, 433 | 434 | getClientId: function() { 435 | return this.clientId; 436 | }, 437 | 438 | getUserId: function() { 439 | return this.user.id; 440 | }, 441 | 442 | _transportDown: function(persistentOutageTimeout) { 443 | var self = this; 444 | var timeout = persistentOutageTimeout || 60; 445 | 446 | if(!this._connectionFailureTimeout) { 447 | this._connectionFailureTimeout = setTimeout(function() { 448 | if(!self._persistentOutage) { 449 | self._persistentOutageStartTime = Date.now(); 450 | self._persistentOutage = true; 451 | debug('Persistent outage'); 452 | self.trigger('connectionFailure'); 453 | } 454 | }, timeout * 1000); 455 | } 456 | }, 457 | 458 | _transportUp: function () { 459 | if(this._connectionFailureTimeout) { 460 | clearTimeout(this._connectionFailureTimeout); 461 | this._connectionFailureTimeout = null; 462 | } 463 | 464 | if(this._persistentOutage) { 465 | this.trigger('stats', 'event', 'faye.outage.restored'); 466 | this.trigger('stats', 'time', 'faye.outage.restored.time', Date.now() - this._persistentOutageStartTime); 467 | delete this._persistentOutage; 468 | delete this._persistentOutageStartTime; 469 | 470 | debug('Persistent outage restored'); 471 | this.trigger('connectionRestored'); 472 | } 473 | } 474 | }); 475 | 476 | module.exports = RealtimeClient; 477 | -------------------------------------------------------------------------------- /lib/room-collection.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var LiveCollection = require('./live-collection'); 4 | var RoomModel = require('./room-model'); 5 | 6 | module.exports = LiveCollection.extend({ 7 | model: RoomModel, 8 | urlTemplate: '/v1/user/:userId/rooms', 9 | initialize: function(models, options) { // jshint unused:true 10 | this.userId = options.userId; 11 | this.listenTo(this, 'change:favourite', this.reorderFavs); 12 | this.listenTo(this, 'change:lastAccessTime change:lurk', this.resetActivity); 13 | }, 14 | 15 | resetActivity: function(model) { 16 | if(model.changed.lastAccessTime && model.get('lastAccessTime')) { 17 | model.unset('activity'); 18 | } 19 | 20 | if(model.changed.lurk && !model.get('lurk')) { 21 | model.unset('activity'); 22 | } 23 | }, 24 | 25 | reorderFavs: function(model) { 26 | /** 27 | * We need to do some special reordering in the model of a favourite being positioned 28 | * This is to mirror the changes happening on the server 29 | * @see recent-room-service.js@addTroupeAsFavouriteInPosition 30 | */ 31 | 32 | /* This only applies when a fav has been set */ 33 | if(!model.changed || !model.changed.favourite || this.reordering) { 34 | return; 35 | } 36 | 37 | this.reordering = true; 38 | 39 | var favourite = model.changed.favourite; 40 | 41 | var forUpdate = this 42 | .map(function(room) { 43 | return { id: room.id, favourite: room.get('favourite') }; 44 | }) 45 | .filter(function(room) { 46 | return room.favourite >= favourite && room.id !== model.id; 47 | }); 48 | 49 | forUpdate.sort(function(a, b) { 50 | return a.favourite - b.favourite; 51 | }); 52 | 53 | var next = favourite; 54 | for(var i = 0; i < forUpdate.length; i++) { 55 | var item = forUpdate[i]; 56 | 57 | if(item.favourite > next) { 58 | forUpdate.splice(i, forUpdate.length); 59 | break; 60 | } 61 | 62 | item.favourite++; 63 | next = item.favourite; 64 | } 65 | 66 | var self = this; 67 | for(var j = forUpdate.length - 1; j >= 0; j--) { 68 | var r = forUpdate[j]; 69 | var id = r.id; 70 | var value = r.favourite; 71 | var t = self.get(id); 72 | t.set('favourite', value, { silent: true }); 73 | } 74 | 75 | delete this.reordering; 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /lib/room-model.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Backbone = require('backbone'); 4 | var moment = require('moment'); 5 | 6 | module.exports = Backbone.Model.extend({ 7 | idAttribute: "id", 8 | initialize: function(options) { 9 | 10 | // this model will be cloned and destroyed with every update. 11 | // only the original will be created with every attribute in the options object. 12 | var isOriginalModel = !!options.url; 13 | 14 | if (isOriginalModel) { 15 | 16 | // we need to set these attributes when the original is created. 17 | // if we set them on every clone, then all these attributes would change when navigating to a room. 18 | // this would make the left menu room list reorder itself all the time. 19 | 20 | // we may not always have a lastAccessTime 21 | var time = this.get('lastAccessTime'); 22 | 23 | if (time) { 24 | this.set('lastAccessTimeOnLoad', time.clone()); 25 | } else if(this.get('unreadItems')) { 26 | // room has been added to the list but the user has never visited it. 27 | // probably a fresh mention in a room. 28 | // better escalate this room to be highest on the list. 29 | this.set('escalationTime', moment()); 30 | } 31 | 32 | if(this.get('unreadItems')) { 33 | this.set('hadUnreadItemsOnLoad', true); 34 | } 35 | 36 | if(this.get('mentions')) { 37 | this.set('hadMentionsOnLoad', true); 38 | } 39 | } 40 | 41 | this.listenTo(this, 'change:mentions', function (model, mentions) { // jshint unused:true 42 | if(!this.previous('mentions') && mentions) { 43 | // changed from 0 mentions to 1+, so escalate this room to be highest on the list 44 | this.set('escalationTime', moment()); 45 | } 46 | }); 47 | 48 | this.listenTo(this, 'change:unreadItems', function (model, unreadItems) { // jshint unused:true 49 | if(!this.previous('unreadItems') && unreadItems) { 50 | // changed from 0 unread to 1+, so escalate this room to be highest on the list 51 | this.set('escalationTime', moment()); 52 | } 53 | }); 54 | 55 | }, 56 | 57 | parse: function(message) { 58 | if(typeof message.lastAccessTime === 'string') { 59 | message.lastAccessTime = moment(message.lastAccessTime, moment.defaultFormat); 60 | } 61 | 62 | return message; 63 | } 64 | }, { modelType: 'room' }); 65 | -------------------------------------------------------------------------------- /lib/simple-filtered-collection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Backbone = require('backbone'); 4 | var sortedArrayIndexSearch = require('./sorted-array-index-search'); 5 | 6 | var nullPredicate = function() { 7 | return true; 8 | }; 9 | 10 | var SimpleFilteredCollection = Backbone.Collection.extend({ 11 | constructor: function(models, options) { 12 | Backbone.Collection.call(this, models, options); 13 | 14 | if(!options || !options.collection) { 15 | throw new Error('A valid collection must be passed to a new instance of SimpleFilteredCollection'); 16 | } 17 | 18 | this._collection = options.collection; 19 | this._filterFn = options.filter || this.filterFn; 20 | 21 | if (options.hasOwnProperty('autoResort')) { 22 | this.autoResort = options.autoResort; 23 | } 24 | 25 | this.listenTo(this._collection, 'add', this._onAddEvent); 26 | this.listenTo(this._collection, 'remove', this._onRemoveEvent); 27 | this.listenTo(this._collection, 'reset', this._resetFromBase); 28 | this.listenTo(this._collection, 'change', this._onChangeEvent); 29 | 30 | this._resetFromBase(); 31 | }, 32 | 33 | autoResort: false, 34 | 35 | /** 36 | * Default filter 37 | */ 38 | filterFn: nullPredicate, 39 | 40 | _applyFilter: function() { 41 | // Start off by indexing whats already in the collection 42 | var newIndex = { }; 43 | var filter = this._filterFn; 44 | 45 | var backingModels = this._collection.models; 46 | for(var i = 0; i < backingModels.length; i++) { 47 | var backingModel = backingModels[i]; 48 | var matches = filter(backingModel); 49 | if (matches) { 50 | newIndex[backingModel.id || backingModel.cid] = backingModel; 51 | } 52 | } 53 | 54 | // Now, figure out what we need to add and remove 55 | var idsToRemove = []; 56 | var currentModels = this.models; 57 | for(var j = currentModels.length - 1; j >= 0; j--) { 58 | var currentModel = currentModels[j]; 59 | var id = currentModel.id || currentModel.cid; 60 | 61 | if (newIndex.hasOwnProperty(id)) { 62 | // Was in the filtered collection and is still 63 | // in the collection 64 | delete newIndex[id]; 65 | } else { 66 | idsToRemove.push(id); 67 | } 68 | } 69 | 70 | if (idsToRemove.length) { 71 | this.remove(idsToRemove); 72 | } 73 | 74 | // Add any new items that are left over 75 | var idsToAdd = Object.keys(newIndex); 76 | if (idsToAdd.length) { 77 | var modelsToAdd = Array(idsToAdd.length); 78 | for (var k = 0; k < idsToAdd.length; k++) { 79 | modelsToAdd[k] = newIndex[idsToAdd[k]]; 80 | } 81 | 82 | // And a single add 83 | this.add(modelsToAdd); 84 | } 85 | 86 | this.trigger('filter-complete'); 87 | }, 88 | 89 | _onModelEvent: function(event, model, collection, options) { 90 | // Ignore change events from models that have recently been removed 91 | if (event === 'change' || event.indexOf('change:') === 0) { 92 | if (!this._filterFn(model)) return; 93 | } 94 | 95 | return Backbone.Collection.prototype._onModelEvent.call(this, event, model, collection, options); 96 | }, 97 | 98 | _onAddEvent: function(model/*, collection, options*/) { 99 | if (this._filterFn(model)) { 100 | return this.add(model); 101 | } 102 | }, 103 | 104 | _onRemoveEvent: function(model/*, collection, options*/) { 105 | return this.remove(model); 106 | }, 107 | 108 | _resetFromBase: function() { 109 | var resetModels = this._collection.filter(this._filterFn); 110 | return this.reset(resetModels); 111 | }, 112 | 113 | _onChangeEvent: function(model/*, options*/) { 114 | var cid = model.cid; 115 | var existsInCollection = this.get(cid); 116 | var matchesFilter = this._filterFn(model); 117 | if (matchesFilter) { 118 | if (existsInCollection) { 119 | // Matches filter and exists in collection... 120 | if (this.autoResort && this.comparator) { 121 | if(!this._modelIsSorted(model)) { 122 | this._readdModelForResort(model); 123 | } 124 | } 125 | } else { 126 | // Matches filter and does not exist in collection... 127 | this.add(model); 128 | } 129 | 130 | return; 131 | } 132 | 133 | if (!matchesFilter && existsInCollection) { 134 | this.remove(model); 135 | return; 136 | } 137 | 138 | }, 139 | 140 | /** 141 | * After a change, is the model still correctly sorted? 142 | */ 143 | _modelIsSorted: function(model) { 144 | var len = this.length; 145 | if (len < 2) return true; 146 | var index = this.indexOf(model); 147 | 148 | if (index <= 0) { 149 | return this.comparator(model, this.models[1]) <= 0; 150 | } 151 | 152 | if (index === len - 1) { 153 | return this.comparator(this.models[len - 2], model) <= 0; 154 | } 155 | 156 | return (this.comparator(this.models[index - 1], model) <= 0) && 157 | (this.comparator(model, this.models[index + 1]) <= 0); 158 | }, 159 | 160 | /** 161 | * Remove and readd the model in the correct position 162 | */ 163 | _readdModelForResort: function(model) { 164 | this.remove(model); 165 | var newIndex = sortedArrayIndexSearch(this.models, this.comparator, model); 166 | this.add(model, { at: newIndex }); 167 | }, 168 | 169 | setFilter: function(fn) { 170 | if (fn === this._filterFn) return; 171 | if (fn) this._filterFn = fn; 172 | this._applyFilter(); 173 | }, 174 | 175 | getUnderlying: function() { 176 | return this._collection; 177 | } 178 | 179 | }); 180 | 181 | module.exports = SimpleFilteredCollection; 182 | -------------------------------------------------------------------------------- /lib/sorted-array-index-search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function sortedArrayIndexSearch(array, comparator, value) { 4 | var low = 0, high = array.length 5 | while (low < high) { 6 | var mid = Math.floor((low + high) / 2); 7 | var current = array[mid]; 8 | 9 | if (comparator(current, value) < 0) { 10 | low = mid + 1; 11 | } else { 12 | high = mid; 13 | } 14 | } 15 | 16 | return low; 17 | } 18 | 19 | module.exports = sortedArrayIndexSearch; 20 | -------------------------------------------------------------------------------- /lib/sorts-filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function modelAdapter(fn) { 4 | return function(a, b) { 5 | return fn(a && a.attributes, b && b.attributes); 6 | }; 7 | } 8 | 9 | function natural(a, b) { 10 | if (a === b) return 0; 11 | return a > b ? 1 : -1; 12 | } 13 | 14 | /* Always sorts nulls after non-nulls */ 15 | function nullsLast(fn) { 16 | return function(a, b) { 17 | if (a && !b) return 1; 18 | if (b && !a) return -1; 19 | if (!a && !b) return 0; 20 | return fn(a, b); 21 | }; 22 | } 23 | 24 | /* Always filter nulls */ 25 | function filterNulls(fn) { 26 | return function(a) { 27 | if (!a) return false; 28 | return fn(a); 29 | }; 30 | } 31 | 32 | function getRank(room) { 33 | // hasHadMentionsAtSomePoint (and the equivalent for unreadItems) is used 34 | // to ensure that rooms dont jump around when mentions is updated after a 35 | // user visits a room and reads all the mentions. 36 | // hasHadMentionsAtSomePoint is not available on the server, so we have a failover. 37 | if (room.hadMentionsOnLoad || room.mentions) { 38 | return 0; 39 | } else if (room.hadUnreadItemsOnLoad || room.unreadItems) { 40 | return 1; 41 | } else { 42 | return 2; 43 | } 44 | } 45 | 46 | function timeDifference(a, b) { 47 | // lastAccessTimeNoSync is used to ensure that rooms dont jump around when 48 | // lastAccessTime is updated after a user visits a room 49 | // lastAccessTimeNoSync is not available on the server, so we have a failover. 50 | // new Date(x).valueOf converts Moments, Dates, Strings and unixtime ints to unixtime ints. 51 | var aTime = new Date(a.lastAccessTimeOnLoad || a.lastAccessTime || 0).valueOf(); 52 | var bTime = new Date(b.lastAccessTimeOnLoad || b.lastAccessTime || 0).valueOf(); 53 | 54 | return bTime - aTime; 55 | } 56 | 57 | function escalationDifference(a, b) { 58 | var aTime = a.escalationTime && a.escalationTime.valueOf() || 0; 59 | var bTime = b.escalationTime && b.escalationTime.valueOf() || 0; 60 | 61 | // most recently escalated is better 62 | return bTime - aTime; 63 | } 64 | 65 | var favouritesSort = nullsLast(function(a, b) { 66 | var isDifferent = natural(a.favourite, b.favourite); 67 | 68 | if (isDifferent) return isDifferent; // -1 or 1 69 | 70 | // both favourites of the same rank, order by name 71 | return natural(a.name, b.name); 72 | }); 73 | 74 | var favouritesFilter = filterNulls(function(room) { 75 | return !!room.favourite; 76 | }); 77 | 78 | var recentsSort = nullsLast(function(a, b) { 79 | var escDiff = escalationDifference(a, b); 80 | 81 | if (escDiff) return escDiff; 82 | 83 | var aRank = getRank(a); 84 | var bRank = getRank(b); 85 | 86 | if (aRank === bRank) { 87 | return timeDifference(a, b); 88 | } else { 89 | return aRank - bRank; 90 | } 91 | }); 92 | 93 | function requiresNotificationPredicate(room) { 94 | // If a one to one room is hidden, but has activity, we show it 95 | return !!(room.unreadItems || room.mentions || (room.oneToOne && room.activity)); 96 | } 97 | 98 | var recentsFilter = filterNulls(function(room) { 99 | return !room.favourite && !!(room.lastAccessTime || requiresNotificationPredicate(room)); 100 | }); 101 | 102 | var recentsLeftMenuFilter = filterNulls(function(room) { 103 | return !!(room.lastAccessTime || room.unreadItems || room.mentions); 104 | }); 105 | 106 | var unreadsSort = nullsLast(function (model) { 107 | return model.lastAccessTime; 108 | }); 109 | 110 | var unreadsFilter = filterNulls(requiresNotificationPredicate); 111 | 112 | 113 | // we want to sort in a descending order, thus the negative results 114 | module.exports = { 115 | pojo: { 116 | favourites: { 117 | sort: nullsLast(favouritesSort), 118 | filter: favouritesFilter 119 | }, 120 | recents: { 121 | sort: nullsLast(recentsSort), 122 | filter: recentsFilter 123 | }, 124 | unreads: { 125 | sort: unreadsSort, 126 | filter: unreadsFilter 127 | }, 128 | leftMenu: { 129 | sort: nullsLast(recentsSort), 130 | filter: recentsLeftMenuFilter 131 | } 132 | }, 133 | model: { 134 | favourites: { 135 | sort: modelAdapter(favouritesSort), 136 | filter: modelAdapter(favouritesFilter) 137 | }, 138 | recents: { 139 | sort: modelAdapter(recentsSort), 140 | filter: modelAdapter(recentsFilter) 141 | }, 142 | unreads: { 143 | sort: modelAdapter(unreadsSort), 144 | filter: modelAdapter(unreadsFilter) 145 | }, 146 | leftMenu: { 147 | sort: modelAdapter(recentsSort), 148 | filter: modelAdapter(recentsLeftMenuFilter) 149 | } 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /lib/template-subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var backboneUrlResolver = require('backbone-url-resolver'); 4 | var log = require('loglevel'); 5 | var _ = require('underscore'); 6 | var Backbone = require('backbone'); 7 | var defaultContextModel = require('./default-context-model'); 8 | var debug = require('debug-proxy')('grc:template-subscription'); 9 | 10 | function TemplateSubscription(client, options) { 11 | this.options = options; 12 | this.client = client; 13 | this._subscribed = false; 14 | this._subscription = null; 15 | 16 | if (!options.onMessage) throw new Error('onMessage is required'); 17 | 18 | if (options.urlModel) { 19 | this.urlModel = options.urlModel; 20 | } else { 21 | this.urlModel = this._getUrlModel(options); 22 | } 23 | 24 | this._subscribe(); 25 | } 26 | 27 | _.extend(TemplateSubscription.prototype, Backbone.Events, { 28 | _subscribe: function(/*options*/) { 29 | if (this._subscribed) return; 30 | this._subscribed = true; 31 | 32 | this._registerForSnapshots(); 33 | 34 | var url = this.url(); 35 | 36 | this.listenTo(this.urlModel, 'change:url', function(model, newChannel) { // jshint unused:true 37 | if (newChannel) { 38 | this._resubscribe(newChannel); 39 | } else { 40 | this._unsubscribe(); 41 | } 42 | }); 43 | 44 | if (!url) return; 45 | this._resubscribe(url); 46 | }, 47 | 48 | cancel: function() { 49 | if (!this._subscribed) return; 50 | this._subscribed = false; 51 | 52 | // Stop listening to model changes.... 53 | this.stopListening(this.contextModel); 54 | this._unsubscribe(); 55 | this._deregisterForSnapshots(); 56 | }, 57 | 58 | _unsubscribe: function() { 59 | var subscription = this._subscription; 60 | 61 | var channel = this._channel; 62 | this._channel = null; 63 | if (!subscription) return; 64 | 65 | debug('Unsubscribe: channel=%s', channel); 66 | subscription.unsubscribe() 67 | .catch(function(err) { 68 | debug('Error unsubscribing from %s: %s', channel, err && err.stack || err); 69 | }); 70 | this._subscription = null; 71 | 72 | this.trigger('unsubscribe'); 73 | }, 74 | 75 | _registerForSnapshots: function() { 76 | if (this._snapshotsRegistered) return; 77 | // Do we need snapshots? 78 | var options = this.options; 79 | if (!options.getSnapshotState && !options.handleSnapshot && !options.getSubscribeOptions) return; 80 | this._snapshotsRegistered = true; 81 | // Subscribe to snapshots to the `null` channel so that we 82 | // can handle snapshots when the channel changes name 83 | this.client.registerSnapshotHandler(null, this); 84 | }, 85 | 86 | _deregisterForSnapshots: function() { 87 | this.client.deregisterSnapshotHandler(null, this); 88 | this._snapshotsRegistered = false; 89 | }, 90 | 91 | _resubscribe: function(channel) { 92 | var oldChannel = this._channel; 93 | debug('Resubscribe %s (from %s)', channel, oldChannel); 94 | 95 | this._unsubscribe(); 96 | this._channel = channel; 97 | 98 | this.trigger('resubscribe', channel); 99 | 100 | var subscription = this._subscription = this.client.subscribe(channel, this.options.onMessage); 101 | 102 | this._subscription 103 | .bind(this) 104 | .catch(function(err) { 105 | log.error('template-subscription: Subscription error for ' + channel, err); 106 | this.trigger('subscriptionError', channel, err); 107 | 108 | if (subscription === this._subscription) { 109 | this._subscription = null; 110 | } 111 | }); 112 | }, 113 | 114 | // Note that this may be overridden by child classes 115 | url: function() { 116 | return this.urlModel.get('url'); 117 | }, 118 | 119 | _getUrlModel: function(options) { 120 | var url = _.result(options, 'urlTemplate'); 121 | var contextModel = _.result(options, 'contextModel'); 122 | 123 | if (!contextModel) { 124 | contextModel = defaultContextModel(this.client); 125 | } 126 | 127 | return backboneUrlResolver(url, contextModel); 128 | }, 129 | 130 | getSnapshotStateForChannel: function(snapshotChannel) { 131 | // Since we subscribed to all snapshots, we need to ensure that 132 | // the snapshot is for this channel 133 | if (snapshotChannel !== this.urlModel.get('url')) return; 134 | 135 | if (this.options.getSnapshotState) { 136 | return this.options.getSnapshotState(snapshotChannel); 137 | } 138 | }, 139 | 140 | getSubscribeOptions: function(snapshotChannel) { 141 | if (snapshotChannel !== this.urlModel.get('url')) return; 142 | 143 | if (this.options.getSubscribeOptions) { 144 | return this.options.getSubscribeOptions(snapshotChannel); 145 | } 146 | }, 147 | 148 | handleSnapshot: function(snapshot, snapshotChannel) { 149 | // Since we subscribed to all snapshots, we need to ensure that 150 | // the snapshot is for this channel 151 | if (snapshotChannel !== this.urlModel.get('url')) return; 152 | 153 | if (this.options.handleSnapshot) { 154 | return this.options.handleSnapshot(snapshot, snapshotChannel); 155 | } 156 | } 157 | }); 158 | 159 | module.exports = TemplateSubscription; 160 | -------------------------------------------------------------------------------- /lib/wrap-extension.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Halley = require('halley/backbone'); 4 | var Promise = Halley.Promise; 5 | var log = require('loglevel'); 6 | 7 | function wrapExtension(fn) { 8 | return function(message, callback) { 9 | var self = this; 10 | return Promise.try(function() { 11 | return new Promise(function(resolve) { 12 | fn.call(self, message, resolve); 13 | }); 14 | }) 15 | .catch(function(err) { 16 | log.error("Extension failed: ", (err.stack || err)); 17 | return message; 18 | }) 19 | .then(function(message) { 20 | callback(message); 21 | }); 22 | }; 23 | 24 | } 25 | 26 | module.exports = wrapExtension; 27 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-realtime-client", 3 | "version": "1.6.2", 4 | "lockfileVersion": 1, 5 | "dependencies": { 6 | "backbone": { 7 | "version": "1.3.3", 8 | "resolved": "http://beta-internal:4873/backbone/-/backbone-1.3.3.tgz", 9 | "integrity": "sha1-TMgOp8sWMaxHSInOQPL4vGg7KZk=" 10 | }, 11 | "backbone-events-standalone": { 12 | "version": "github:suprememoocow/backbone-events-standalone#f9b7ec37495f9ef62ebe563d6fdaec89882da0b0" 13 | }, 14 | "backbone-url-resolver": { 15 | "version": "0.1.1", 16 | "resolved": "http://beta-internal:4873/backbone-url-resolver/-/backbone-url-resolver-0.1.1.tgz", 17 | "integrity": "sha1-G3JVmSSUYsis1AE3UQ+BQUnmYKk=" 18 | }, 19 | "blocked": { 20 | "version": "1.2.1", 21 | "resolved": "http://beta-internal:4873/blocked/-/blocked-1.2.1.tgz", 22 | "integrity": "sha1-4i7+dnhjxlq4GX9iUpKRBOHsnOI=", 23 | "dev": true 24 | }, 25 | "bluebird": { 26 | "version": "3.5.0", 27 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", 28 | "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" 29 | }, 30 | "debug": { 31 | "version": "2.6.8", 32 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 33 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" 34 | }, 35 | "debug-proxy": { 36 | "version": "0.2.0", 37 | "resolved": "http://beta-internal:4873/debug-proxy/-/debug-proxy-0.2.0.tgz", 38 | "integrity": "sha1-1Z5FMYr3PVeBemxAE8OySrG4S1w=", 39 | "dependencies": { 40 | "debug": { 41 | "version": "2.2.0", 42 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 43 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 44 | "dependencies": { 45 | "ms": { 46 | "version": "0.7.1", 47 | "resolved": "http://beta-internal:4873/ms/-/ms-0.7.1.tgz", 48 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | "eslint": { 55 | "version": "3.2.2", 56 | "resolved": "http://beta-internal:4873/eslint/-/eslint-3.2.2.tgz", 57 | "integrity": "sha1-RyJvaw5wnyP2rNBsXMnpezlXTHw=", 58 | "dev": true, 59 | "dependencies": { 60 | "chalk": { 61 | "version": "1.1.3", 62 | "resolved": "http://beta-internal:4873/chalk/-/chalk-1.1.3.tgz", 63 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 64 | "dev": true, 65 | "dependencies": { 66 | "ansi-styles": { 67 | "version": "2.2.1", 68 | "resolved": "http://beta-internal:4873/ansi-styles/-/ansi-styles-2.2.1.tgz", 69 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 70 | "dev": true 71 | }, 72 | "escape-string-regexp": { 73 | "version": "1.0.5", 74 | "resolved": "http://beta-internal:4873/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 75 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 76 | "dev": true 77 | }, 78 | "has-ansi": { 79 | "version": "2.0.0", 80 | "resolved": "http://beta-internal:4873/has-ansi/-/has-ansi-2.0.0.tgz", 81 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 82 | "dev": true, 83 | "dependencies": { 84 | "ansi-regex": { 85 | "version": "2.0.0", 86 | "resolved": "http://beta-internal:4873/ansi-regex/-/ansi-regex-2.0.0.tgz", 87 | "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=", 88 | "dev": true 89 | } 90 | } 91 | }, 92 | "strip-ansi": { 93 | "version": "3.0.1", 94 | "resolved": "http://beta-internal:4873/strip-ansi/-/strip-ansi-3.0.1.tgz", 95 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 96 | "dev": true, 97 | "dependencies": { 98 | "ansi-regex": { 99 | "version": "2.0.0", 100 | "resolved": "http://beta-internal:4873/ansi-regex/-/ansi-regex-2.0.0.tgz", 101 | "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=", 102 | "dev": true 103 | } 104 | } 105 | }, 106 | "supports-color": { 107 | "version": "2.0.0", 108 | "resolved": "http://beta-internal:4873/supports-color/-/supports-color-2.0.0.tgz", 109 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 110 | "dev": true 111 | } 112 | } 113 | }, 114 | "concat-stream": { 115 | "version": "1.5.1", 116 | "resolved": "http://beta-internal:4873/concat-stream/-/concat-stream-1.5.1.tgz", 117 | "integrity": "sha1-87gKz54fSOOHXAaItBtsMWAu6hw=", 118 | "dev": true, 119 | "dependencies": { 120 | "inherits": { 121 | "version": "2.0.1", 122 | "resolved": "http://beta-internal:4873/inherits/-/inherits-2.0.1.tgz", 123 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 124 | "dev": true 125 | }, 126 | "readable-stream": { 127 | "version": "2.0.6", 128 | "resolved": "http://beta-internal:4873/readable-stream/-/readable-stream-2.0.6.tgz", 129 | "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", 130 | "dev": true, 131 | "dependencies": { 132 | "core-util-is": { 133 | "version": "1.0.2", 134 | "resolved": "http://beta-internal:4873/core-util-is/-/core-util-is-1.0.2.tgz", 135 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 136 | "dev": true 137 | }, 138 | "isarray": { 139 | "version": "1.0.0", 140 | "resolved": "http://beta-internal:4873/isarray/-/isarray-1.0.0.tgz", 141 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 142 | "dev": true 143 | }, 144 | "process-nextick-args": { 145 | "version": "1.0.7", 146 | "resolved": "http://beta-internal:4873/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 147 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", 148 | "dev": true 149 | }, 150 | "string_decoder": { 151 | "version": "0.10.31", 152 | "resolved": "http://beta-internal:4873/string_decoder/-/string_decoder-0.10.31.tgz", 153 | "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", 154 | "dev": true 155 | }, 156 | "util-deprecate": { 157 | "version": "1.0.2", 158 | "resolved": "http://beta-internal:4873/util-deprecate/-/util-deprecate-1.0.2.tgz", 159 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 160 | "dev": true 161 | } 162 | } 163 | }, 164 | "typedarray": { 165 | "version": "0.0.6", 166 | "resolved": "http://beta-internal:4873/typedarray/-/typedarray-0.0.6.tgz", 167 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 168 | "dev": true 169 | } 170 | } 171 | }, 172 | "debug": { 173 | "version": "2.2.0", 174 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 175 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 176 | "dev": true, 177 | "dependencies": { 178 | "ms": { 179 | "version": "0.7.1", 180 | "resolved": "http://beta-internal:4873/ms/-/ms-0.7.1.tgz", 181 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", 182 | "dev": true 183 | } 184 | } 185 | }, 186 | "doctrine": { 187 | "version": "1.2.2", 188 | "resolved": "http://beta-internal:4873/doctrine/-/doctrine-1.2.2.tgz", 189 | "integrity": "sha1-nphnIQFJVIuV7FFGna5MqtMSMI4=", 190 | "dev": true, 191 | "dependencies": { 192 | "esutils": { 193 | "version": "1.1.6", 194 | "resolved": "http://beta-internal:4873/esutils/-/esutils-1.1.6.tgz", 195 | "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", 196 | "dev": true 197 | }, 198 | "isarray": { 199 | "version": "1.0.0", 200 | "resolved": "http://beta-internal:4873/isarray/-/isarray-1.0.0.tgz", 201 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 202 | "dev": true 203 | } 204 | } 205 | }, 206 | "escope": { 207 | "version": "3.6.0", 208 | "resolved": "http://beta-internal:4873/escope/-/escope-3.6.0.tgz", 209 | "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", 210 | "dev": true, 211 | "dependencies": { 212 | "es6-map": { 213 | "version": "0.1.4", 214 | "resolved": "http://beta-internal:4873/es6-map/-/es6-map-0.1.4.tgz", 215 | "integrity": "sha1-o0sUe+IkdzpNfagHJ5TO+jYyuJc=", 216 | "dev": true, 217 | "dependencies": { 218 | "d": { 219 | "version": "0.1.1", 220 | "resolved": "http://beta-internal:4873/d/-/d-0.1.1.tgz", 221 | "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", 222 | "dev": true 223 | }, 224 | "es5-ext": { 225 | "version": "0.10.12", 226 | "resolved": "http://beta-internal:4873/es5-ext/-/es5-ext-0.10.12.tgz", 227 | "integrity": "sha1-qoRkHU23a2Krul5F/YBey6sUAEc=", 228 | "dev": true 229 | }, 230 | "es6-iterator": { 231 | "version": "2.0.0", 232 | "resolved": "http://beta-internal:4873/es6-iterator/-/es6-iterator-2.0.0.tgz", 233 | "integrity": "sha1-vZaFZ9YWNeM8C4BydhPJy0sJa6w=", 234 | "dev": true 235 | }, 236 | "es6-set": { 237 | "version": "0.1.4", 238 | "resolved": "http://beta-internal:4873/es6-set/-/es6-set-0.1.4.tgz", 239 | "integrity": "sha1-lRa2dhwpZLkv9HlFYjOiR9xwfOg=", 240 | "dev": true 241 | }, 242 | "es6-symbol": { 243 | "version": "3.1.0", 244 | "resolved": "http://beta-internal:4873/es6-symbol/-/es6-symbol-3.1.0.tgz", 245 | "integrity": "sha1-lEgcZV56fK2C66gy2X1UM0ltf/o=", 246 | "dev": true 247 | }, 248 | "event-emitter": { 249 | "version": "0.3.4", 250 | "resolved": "http://beta-internal:4873/event-emitter/-/event-emitter-0.3.4.tgz", 251 | "integrity": "sha1-jWPd+0z+H647MsomXExyAiIIC7U=", 252 | "dev": true 253 | } 254 | } 255 | }, 256 | "es6-weak-map": { 257 | "version": "2.0.1", 258 | "resolved": "http://beta-internal:4873/es6-weak-map/-/es6-weak-map-2.0.1.tgz", 259 | "integrity": "sha1-DSu9iCfrX7S6j5f7/qUNQ9sh6oE=", 260 | "dev": true, 261 | "dependencies": { 262 | "d": { 263 | "version": "0.1.1", 264 | "resolved": "http://beta-internal:4873/d/-/d-0.1.1.tgz", 265 | "integrity": "sha1-2hhMU10Y2O57oqoim5FACfrhEwk=", 266 | "dev": true 267 | }, 268 | "es5-ext": { 269 | "version": "0.10.12", 270 | "resolved": "http://beta-internal:4873/es5-ext/-/es5-ext-0.10.12.tgz", 271 | "integrity": "sha1-qoRkHU23a2Krul5F/YBey6sUAEc=", 272 | "dev": true 273 | }, 274 | "es6-iterator": { 275 | "version": "2.0.0", 276 | "resolved": "http://beta-internal:4873/es6-iterator/-/es6-iterator-2.0.0.tgz", 277 | "integrity": "sha1-vZaFZ9YWNeM8C4BydhPJy0sJa6w=", 278 | "dev": true 279 | }, 280 | "es6-symbol": { 281 | "version": "3.1.0", 282 | "resolved": "http://beta-internal:4873/es6-symbol/-/es6-symbol-3.1.0.tgz", 283 | "integrity": "sha1-lEgcZV56fK2C66gy2X1UM0ltf/o=", 284 | "dev": true 285 | } 286 | } 287 | }, 288 | "esrecurse": { 289 | "version": "4.1.0", 290 | "resolved": "http://beta-internal:4873/esrecurse/-/esrecurse-4.1.0.tgz", 291 | "integrity": "sha1-RxO2U2rffyrE8yfVWed1a/9kgiA=", 292 | "dev": true, 293 | "dependencies": { 294 | "estraverse": { 295 | "version": "4.1.1", 296 | "resolved": "http://beta-internal:4873/estraverse/-/estraverse-4.1.1.tgz", 297 | "integrity": "sha1-9srKcokzqFDvkGYdDheYK6RxEaI=", 298 | "dev": true 299 | }, 300 | "object-assign": { 301 | "version": "4.1.0", 302 | "resolved": "http://beta-internal:4873/object-assign/-/object-assign-4.1.0.tgz", 303 | "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", 304 | "dev": true 305 | } 306 | } 307 | } 308 | } 309 | }, 310 | "espree": { 311 | "version": "3.1.7", 312 | "resolved": "http://beta-internal:4873/espree/-/espree-3.1.7.tgz", 313 | "integrity": "sha1-/V3ux2qXpRIKnNOnyxF3oJI7EdI=", 314 | "dev": true, 315 | "dependencies": { 316 | "acorn": { 317 | "version": "3.3.0", 318 | "resolved": "http://beta-internal:4873/acorn/-/acorn-3.3.0.tgz", 319 | "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", 320 | "dev": true 321 | }, 322 | "acorn-jsx": { 323 | "version": "3.0.1", 324 | "resolved": "http://beta-internal:4873/acorn-jsx/-/acorn-jsx-3.0.1.tgz", 325 | "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", 326 | "dev": true 327 | } 328 | } 329 | }, 330 | "estraverse": { 331 | "version": "4.2.0", 332 | "resolved": "http://beta-internal:4873/estraverse/-/estraverse-4.2.0.tgz", 333 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 334 | "dev": true 335 | }, 336 | "esutils": { 337 | "version": "2.0.2", 338 | "resolved": "http://beta-internal:4873/esutils/-/esutils-2.0.2.tgz", 339 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 340 | "dev": true 341 | }, 342 | "file-entry-cache": { 343 | "version": "1.3.1", 344 | "resolved": "http://beta-internal:4873/file-entry-cache/-/file-entry-cache-1.3.1.tgz", 345 | "integrity": "sha1-RMYepgeuS+nBQC9B9EJwy/4zT/g=", 346 | "dev": true, 347 | "dependencies": { 348 | "flat-cache": { 349 | "version": "1.2.1", 350 | "resolved": "http://beta-internal:4873/flat-cache/-/flat-cache-1.2.1.tgz", 351 | "integrity": "sha1-bIN9YiWn3lZZMjdAs21TYfcWkf8=", 352 | "dev": true, 353 | "dependencies": { 354 | "circular-json": { 355 | "version": "0.3.1", 356 | "resolved": "http://beta-internal:4873/circular-json/-/circular-json-0.3.1.tgz", 357 | "integrity": "sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0=", 358 | "dev": true 359 | }, 360 | "del": { 361 | "version": "2.2.1", 362 | "resolved": "http://beta-internal:4873/del/-/del-2.2.1.tgz", 363 | "integrity": "sha1-9nYwJkciCcTwNJERxawoCGi+xP4=", 364 | "dev": true, 365 | "dependencies": { 366 | "globby": { 367 | "version": "5.0.0", 368 | "resolved": "http://beta-internal:4873/globby/-/globby-5.0.0.tgz", 369 | "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", 370 | "dev": true, 371 | "dependencies": { 372 | "array-union": { 373 | "version": "1.0.2", 374 | "resolved": "http://beta-internal:4873/array-union/-/array-union-1.0.2.tgz", 375 | "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", 376 | "dev": true, 377 | "dependencies": { 378 | "array-uniq": { 379 | "version": "1.0.3", 380 | "resolved": "http://beta-internal:4873/array-uniq/-/array-uniq-1.0.3.tgz", 381 | "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", 382 | "dev": true 383 | } 384 | } 385 | }, 386 | "arrify": { 387 | "version": "1.0.1", 388 | "resolved": "http://beta-internal:4873/arrify/-/arrify-1.0.1.tgz", 389 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 390 | "dev": true 391 | } 392 | } 393 | }, 394 | "is-path-cwd": { 395 | "version": "1.0.0", 396 | "resolved": "http://beta-internal:4873/is-path-cwd/-/is-path-cwd-1.0.0.tgz", 397 | "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", 398 | "dev": true 399 | }, 400 | "is-path-in-cwd": { 401 | "version": "1.0.0", 402 | "resolved": "http://beta-internal:4873/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", 403 | "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", 404 | "dev": true, 405 | "dependencies": { 406 | "is-path-inside": { 407 | "version": "1.0.0", 408 | "resolved": "http://beta-internal:4873/is-path-inside/-/is-path-inside-1.0.0.tgz", 409 | "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", 410 | "dev": true 411 | } 412 | } 413 | }, 414 | "pify": { 415 | "version": "2.3.0", 416 | "resolved": "http://beta-internal:4873/pify/-/pify-2.3.0.tgz", 417 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 418 | "dev": true 419 | }, 420 | "pinkie-promise": { 421 | "version": "2.0.1", 422 | "resolved": "http://beta-internal:4873/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 423 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 424 | "dev": true, 425 | "dependencies": { 426 | "pinkie": { 427 | "version": "2.0.4", 428 | "resolved": "http://beta-internal:4873/pinkie/-/pinkie-2.0.4.tgz", 429 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 430 | "dev": true 431 | } 432 | } 433 | }, 434 | "rimraf": { 435 | "version": "2.5.4", 436 | "resolved": "http://beta-internal:4873/rimraf/-/rimraf-2.5.4.tgz", 437 | "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", 438 | "dev": true 439 | } 440 | } 441 | }, 442 | "graceful-fs": { 443 | "version": "4.1.5", 444 | "resolved": "http://beta-internal:4873/graceful-fs/-/graceful-fs-4.1.5.tgz", 445 | "integrity": "sha1-9HRejK7V4N0u8hu14tIpoy6Ak8A=", 446 | "dev": true 447 | }, 448 | "write": { 449 | "version": "0.2.1", 450 | "resolved": "http://beta-internal:4873/write/-/write-0.2.1.tgz", 451 | "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", 452 | "dev": true 453 | } 454 | } 455 | }, 456 | "object-assign": { 457 | "version": "4.1.0", 458 | "resolved": "http://beta-internal:4873/object-assign/-/object-assign-4.1.0.tgz", 459 | "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", 460 | "dev": true 461 | } 462 | } 463 | }, 464 | "glob": { 465 | "version": "7.0.5", 466 | "resolved": "http://beta-internal:4873/glob/-/glob-7.0.5.tgz", 467 | "integrity": "sha1-tCAqaQmbu00pKnwblbZoK2fr3JU=", 468 | "dev": true, 469 | "dependencies": { 470 | "fs.realpath": { 471 | "version": "1.0.0", 472 | "resolved": "http://beta-internal:4873/fs.realpath/-/fs.realpath-1.0.0.tgz", 473 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 474 | "dev": true 475 | }, 476 | "inflight": { 477 | "version": "1.0.5", 478 | "resolved": "http://beta-internal:4873/inflight/-/inflight-1.0.5.tgz", 479 | "integrity": "sha1-2zIEzVqd4ubNiQuFxuL2a89PYgo=", 480 | "dev": true, 481 | "dependencies": { 482 | "wrappy": { 483 | "version": "1.0.2", 484 | "resolved": "http://beta-internal:4873/wrappy/-/wrappy-1.0.2.tgz", 485 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 486 | "dev": true 487 | } 488 | } 489 | }, 490 | "inherits": { 491 | "version": "2.0.1", 492 | "resolved": "http://beta-internal:4873/inherits/-/inherits-2.0.1.tgz", 493 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 494 | "dev": true 495 | }, 496 | "minimatch": { 497 | "version": "3.0.3", 498 | "resolved": "http://beta-internal:4873/minimatch/-/minimatch-3.0.3.tgz", 499 | "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", 500 | "dev": true, 501 | "dependencies": { 502 | "brace-expansion": { 503 | "version": "1.1.6", 504 | "resolved": "http://beta-internal:4873/brace-expansion/-/brace-expansion-1.1.6.tgz", 505 | "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", 506 | "dev": true, 507 | "dependencies": { 508 | "balanced-match": { 509 | "version": "0.4.2", 510 | "resolved": "http://beta-internal:4873/balanced-match/-/balanced-match-0.4.2.tgz", 511 | "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", 512 | "dev": true 513 | }, 514 | "concat-map": { 515 | "version": "0.0.1", 516 | "resolved": "http://beta-internal:4873/concat-map/-/concat-map-0.0.1.tgz", 517 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 518 | "dev": true 519 | } 520 | } 521 | } 522 | } 523 | }, 524 | "once": { 525 | "version": "1.3.3", 526 | "resolved": "http://beta-internal:4873/once/-/once-1.3.3.tgz", 527 | "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", 528 | "dev": true, 529 | "dependencies": { 530 | "wrappy": { 531 | "version": "1.0.2", 532 | "resolved": "http://beta-internal:4873/wrappy/-/wrappy-1.0.2.tgz", 533 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 534 | "dev": true 535 | } 536 | } 537 | }, 538 | "path-is-absolute": { 539 | "version": "1.0.0", 540 | "resolved": "http://beta-internal:4873/path-is-absolute/-/path-is-absolute-1.0.0.tgz", 541 | "integrity": "sha1-Jj2tpmqz8vsQv3+dJN2PPlcO+RI=", 542 | "dev": true 543 | } 544 | } 545 | }, 546 | "globals": { 547 | "version": "9.9.0", 548 | "resolved": "http://beta-internal:4873/globals/-/globals-9.9.0.tgz", 549 | "integrity": "sha1-TF/8NZ+yHtyD/tuHscC0FNwk1VI=", 550 | "dev": true 551 | }, 552 | "ignore": { 553 | "version": "3.1.3", 554 | "resolved": "http://beta-internal:4873/ignore/-/ignore-3.1.3.tgz", 555 | "integrity": "sha1-nokMBlJRkRWulCfaR1Fr1U0daZk=", 556 | "dev": true 557 | }, 558 | "imurmurhash": { 559 | "version": "0.1.4", 560 | "resolved": "http://beta-internal:4873/imurmurhash/-/imurmurhash-0.1.4.tgz", 561 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 562 | "dev": true 563 | }, 564 | "inquirer": { 565 | "version": "0.12.0", 566 | "resolved": "http://beta-internal:4873/inquirer/-/inquirer-0.12.0.tgz", 567 | "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", 568 | "dev": true, 569 | "dependencies": { 570 | "ansi-escapes": { 571 | "version": "1.4.0", 572 | "resolved": "http://beta-internal:4873/ansi-escapes/-/ansi-escapes-1.4.0.tgz", 573 | "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", 574 | "dev": true 575 | }, 576 | "ansi-regex": { 577 | "version": "2.0.0", 578 | "resolved": "http://beta-internal:4873/ansi-regex/-/ansi-regex-2.0.0.tgz", 579 | "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=", 580 | "dev": true 581 | }, 582 | "cli-cursor": { 583 | "version": "1.0.2", 584 | "resolved": "http://beta-internal:4873/cli-cursor/-/cli-cursor-1.0.2.tgz", 585 | "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", 586 | "dev": true, 587 | "dependencies": { 588 | "restore-cursor": { 589 | "version": "1.0.1", 590 | "resolved": "http://beta-internal:4873/restore-cursor/-/restore-cursor-1.0.1.tgz", 591 | "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", 592 | "dev": true, 593 | "dependencies": { 594 | "exit-hook": { 595 | "version": "1.1.1", 596 | "resolved": "http://beta-internal:4873/exit-hook/-/exit-hook-1.1.1.tgz", 597 | "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", 598 | "dev": true 599 | }, 600 | "onetime": { 601 | "version": "1.1.0", 602 | "resolved": "http://beta-internal:4873/onetime/-/onetime-1.1.0.tgz", 603 | "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", 604 | "dev": true 605 | } 606 | } 607 | } 608 | } 609 | }, 610 | "cli-width": { 611 | "version": "2.1.0", 612 | "resolved": "http://beta-internal:4873/cli-width/-/cli-width-2.1.0.tgz", 613 | "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", 614 | "dev": true 615 | }, 616 | "figures": { 617 | "version": "1.7.0", 618 | "resolved": "http://beta-internal:4873/figures/-/figures-1.7.0.tgz", 619 | "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", 620 | "dev": true, 621 | "dependencies": { 622 | "escape-string-regexp": { 623 | "version": "1.0.5", 624 | "resolved": "http://beta-internal:4873/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 625 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 626 | "dev": true 627 | }, 628 | "object-assign": { 629 | "version": "4.1.0", 630 | "resolved": "http://beta-internal:4873/object-assign/-/object-assign-4.1.0.tgz", 631 | "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", 632 | "dev": true 633 | } 634 | } 635 | }, 636 | "readline2": { 637 | "version": "1.0.1", 638 | "resolved": "http://beta-internal:4873/readline2/-/readline2-1.0.1.tgz", 639 | "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", 640 | "dev": true, 641 | "dependencies": { 642 | "code-point-at": { 643 | "version": "1.0.0", 644 | "resolved": "http://beta-internal:4873/code-point-at/-/code-point-at-1.0.0.tgz", 645 | "integrity": "sha1-9psZLT99keOC5Lcb3bd4eGGasMY=", 646 | "dev": true, 647 | "dependencies": { 648 | "number-is-nan": { 649 | "version": "1.0.0", 650 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 651 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 652 | "dev": true 653 | } 654 | } 655 | }, 656 | "is-fullwidth-code-point": { 657 | "version": "1.0.0", 658 | "resolved": "http://beta-internal:4873/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 659 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 660 | "dev": true, 661 | "dependencies": { 662 | "number-is-nan": { 663 | "version": "1.0.0", 664 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 665 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 666 | "dev": true 667 | } 668 | } 669 | }, 670 | "mute-stream": { 671 | "version": "0.0.5", 672 | "resolved": "http://beta-internal:4873/mute-stream/-/mute-stream-0.0.5.tgz", 673 | "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", 674 | "dev": true 675 | } 676 | } 677 | }, 678 | "run-async": { 679 | "version": "0.1.0", 680 | "resolved": "http://beta-internal:4873/run-async/-/run-async-0.1.0.tgz", 681 | "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", 682 | "dev": true, 683 | "dependencies": { 684 | "once": { 685 | "version": "1.3.3", 686 | "resolved": "http://beta-internal:4873/once/-/once-1.3.3.tgz", 687 | "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", 688 | "dev": true, 689 | "dependencies": { 690 | "wrappy": { 691 | "version": "1.0.2", 692 | "resolved": "http://beta-internal:4873/wrappy/-/wrappy-1.0.2.tgz", 693 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 694 | "dev": true 695 | } 696 | } 697 | } 698 | } 699 | }, 700 | "rx-lite": { 701 | "version": "3.1.2", 702 | "resolved": "http://beta-internal:4873/rx-lite/-/rx-lite-3.1.2.tgz", 703 | "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", 704 | "dev": true 705 | }, 706 | "string-width": { 707 | "version": "1.0.1", 708 | "resolved": "http://beta-internal:4873/string-width/-/string-width-1.0.1.tgz", 709 | "integrity": "sha1-ySEptvHX9SrPmvQkom44ZKBc6wo=", 710 | "dev": true, 711 | "dependencies": { 712 | "code-point-at": { 713 | "version": "1.0.0", 714 | "resolved": "http://beta-internal:4873/code-point-at/-/code-point-at-1.0.0.tgz", 715 | "integrity": "sha1-9psZLT99keOC5Lcb3bd4eGGasMY=", 716 | "dev": true, 717 | "dependencies": { 718 | "number-is-nan": { 719 | "version": "1.0.0", 720 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 721 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 722 | "dev": true 723 | } 724 | } 725 | }, 726 | "is-fullwidth-code-point": { 727 | "version": "1.0.0", 728 | "resolved": "http://beta-internal:4873/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 729 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 730 | "dev": true, 731 | "dependencies": { 732 | "number-is-nan": { 733 | "version": "1.0.0", 734 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 735 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 736 | "dev": true 737 | } 738 | } 739 | } 740 | } 741 | }, 742 | "strip-ansi": { 743 | "version": "3.0.1", 744 | "resolved": "http://beta-internal:4873/strip-ansi/-/strip-ansi-3.0.1.tgz", 745 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 746 | "dev": true 747 | }, 748 | "through": { 749 | "version": "2.3.8", 750 | "resolved": "http://beta-internal:4873/through/-/through-2.3.8.tgz", 751 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 752 | "dev": true 753 | } 754 | } 755 | }, 756 | "is-my-json-valid": { 757 | "version": "2.13.1", 758 | "resolved": "http://beta-internal:4873/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", 759 | "integrity": "sha1-1Vd4qC/rawlj/0vhEdXRaE6JBwc=", 760 | "dev": true, 761 | "dependencies": { 762 | "generate-function": { 763 | "version": "2.0.0", 764 | "resolved": "http://beta-internal:4873/generate-function/-/generate-function-2.0.0.tgz", 765 | "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", 766 | "dev": true 767 | }, 768 | "generate-object-property": { 769 | "version": "1.2.0", 770 | "resolved": "http://beta-internal:4873/generate-object-property/-/generate-object-property-1.2.0.tgz", 771 | "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", 772 | "dev": true, 773 | "dependencies": { 774 | "is-property": { 775 | "version": "1.0.2", 776 | "resolved": "http://beta-internal:4873/is-property/-/is-property-1.0.2.tgz", 777 | "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", 778 | "dev": true 779 | } 780 | } 781 | }, 782 | "jsonpointer": { 783 | "version": "2.0.0", 784 | "resolved": "http://beta-internal:4873/jsonpointer/-/jsonpointer-2.0.0.tgz", 785 | "integrity": "sha1-OvHdIP6FRjkQ1GmjheMwF9KgMNk=", 786 | "dev": true 787 | }, 788 | "xtend": { 789 | "version": "4.0.1", 790 | "resolved": "http://beta-internal:4873/xtend/-/xtend-4.0.1.tgz", 791 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", 792 | "dev": true 793 | } 794 | } 795 | }, 796 | "is-resolvable": { 797 | "version": "1.0.0", 798 | "resolved": "http://beta-internal:4873/is-resolvable/-/is-resolvable-1.0.0.tgz", 799 | "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", 800 | "dev": true, 801 | "dependencies": { 802 | "tryit": { 803 | "version": "1.0.2", 804 | "resolved": "http://beta-internal:4873/tryit/-/tryit-1.0.2.tgz", 805 | "integrity": "sha1-wZawBz5rHFldk8nIMIVbeswypFM=", 806 | "dev": true 807 | } 808 | } 809 | }, 810 | "js-yaml": { 811 | "version": "3.6.1", 812 | "resolved": "http://beta-internal:4873/js-yaml/-/js-yaml-3.6.1.tgz", 813 | "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", 814 | "dev": true, 815 | "dependencies": { 816 | "argparse": { 817 | "version": "1.0.7", 818 | "resolved": "http://beta-internal:4873/argparse/-/argparse-1.0.7.tgz", 819 | "integrity": "sha1-wolQZIBVeBDxSovGLXoG9j7X+VE=", 820 | "dev": true, 821 | "dependencies": { 822 | "sprintf-js": { 823 | "version": "1.0.3", 824 | "resolved": "http://beta-internal:4873/sprintf-js/-/sprintf-js-1.0.3.tgz", 825 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 826 | "dev": true 827 | } 828 | } 829 | }, 830 | "esprima": { 831 | "version": "2.7.2", 832 | "resolved": "http://beta-internal:4873/esprima/-/esprima-2.7.2.tgz", 833 | "integrity": "sha1-9DvlQ2CZhOrkTJM6xjNSpq818zk=", 834 | "dev": true 835 | } 836 | } 837 | }, 838 | "json-stable-stringify": { 839 | "version": "1.0.1", 840 | "resolved": "http://beta-internal:4873/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 841 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 842 | "dev": true, 843 | "dependencies": { 844 | "jsonify": { 845 | "version": "0.0.0", 846 | "resolved": "http://beta-internal:4873/jsonify/-/jsonify-0.0.0.tgz", 847 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", 848 | "dev": true 849 | } 850 | } 851 | }, 852 | "levn": { 853 | "version": "0.3.0", 854 | "resolved": "http://beta-internal:4873/levn/-/levn-0.3.0.tgz", 855 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 856 | "dev": true, 857 | "dependencies": { 858 | "prelude-ls": { 859 | "version": "1.1.2", 860 | "resolved": "http://beta-internal:4873/prelude-ls/-/prelude-ls-1.1.2.tgz", 861 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 862 | "dev": true 863 | }, 864 | "type-check": { 865 | "version": "0.3.2", 866 | "resolved": "http://beta-internal:4873/type-check/-/type-check-0.3.2.tgz", 867 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 868 | "dev": true 869 | } 870 | } 871 | }, 872 | "lodash": { 873 | "version": "4.15.0", 874 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.15.0.tgz", 875 | "integrity": "sha1-MWI5HY8BQKoiz49rPDTWt/Y9Oqk=", 876 | "dev": true 877 | }, 878 | "mkdirp": { 879 | "version": "0.5.1", 880 | "resolved": "http://beta-internal:4873/mkdirp/-/mkdirp-0.5.1.tgz", 881 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 882 | "dev": true, 883 | "dependencies": { 884 | "minimist": { 885 | "version": "0.0.8", 886 | "resolved": "http://beta-internal:4873/minimist/-/minimist-0.0.8.tgz", 887 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 888 | "dev": true 889 | } 890 | } 891 | }, 892 | "optionator": { 893 | "version": "0.8.1", 894 | "resolved": "http://beta-internal:4873/optionator/-/optionator-0.8.1.tgz", 895 | "integrity": "sha1-4xtJMs3V+4Yqiw0QvGPT7h7H14s=", 896 | "dev": true, 897 | "dependencies": { 898 | "deep-is": { 899 | "version": "0.1.3", 900 | "resolved": "http://beta-internal:4873/deep-is/-/deep-is-0.1.3.tgz", 901 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 902 | "dev": true 903 | }, 904 | "fast-levenshtein": { 905 | "version": "1.1.4", 906 | "resolved": "http://beta-internal:4873/fast-levenshtein/-/fast-levenshtein-1.1.4.tgz", 907 | "integrity": "sha1-5qdUzI8V5YmHqpy9J69m/W9OWvk=", 908 | "dev": true 909 | }, 910 | "prelude-ls": { 911 | "version": "1.1.2", 912 | "resolved": "http://beta-internal:4873/prelude-ls/-/prelude-ls-1.1.2.tgz", 913 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 914 | "dev": true 915 | }, 916 | "type-check": { 917 | "version": "0.3.2", 918 | "resolved": "http://beta-internal:4873/type-check/-/type-check-0.3.2.tgz", 919 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 920 | "dev": true 921 | }, 922 | "wordwrap": { 923 | "version": "1.0.0", 924 | "resolved": "http://beta-internal:4873/wordwrap/-/wordwrap-1.0.0.tgz", 925 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 926 | "dev": true 927 | } 928 | } 929 | }, 930 | "path-is-inside": { 931 | "version": "1.0.1", 932 | "resolved": "http://beta-internal:4873/path-is-inside/-/path-is-inside-1.0.1.tgz", 933 | "integrity": "sha1-mNjx0DC/BL167uShulSF1AMY/Yk=", 934 | "dev": true 935 | }, 936 | "pluralize": { 937 | "version": "1.2.1", 938 | "resolved": "http://beta-internal:4873/pluralize/-/pluralize-1.2.1.tgz", 939 | "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", 940 | "dev": true 941 | }, 942 | "progress": { 943 | "version": "1.1.8", 944 | "resolved": "http://beta-internal:4873/progress/-/progress-1.1.8.tgz", 945 | "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", 946 | "dev": true 947 | }, 948 | "require-uncached": { 949 | "version": "1.0.2", 950 | "resolved": "http://beta-internal:4873/require-uncached/-/require-uncached-1.0.2.tgz", 951 | "integrity": "sha1-Z9rTtzMInncDASRnikWVifr2p+w=", 952 | "dev": true, 953 | "dependencies": { 954 | "caller-path": { 955 | "version": "0.1.0", 956 | "resolved": "http://beta-internal:4873/caller-path/-/caller-path-0.1.0.tgz", 957 | "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", 958 | "dev": true, 959 | "dependencies": { 960 | "callsites": { 961 | "version": "0.2.0", 962 | "resolved": "http://beta-internal:4873/callsites/-/callsites-0.2.0.tgz", 963 | "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", 964 | "dev": true 965 | } 966 | } 967 | }, 968 | "resolve-from": { 969 | "version": "1.0.1", 970 | "resolved": "http://beta-internal:4873/resolve-from/-/resolve-from-1.0.1.tgz", 971 | "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", 972 | "dev": true 973 | } 974 | } 975 | }, 976 | "shelljs": { 977 | "version": "0.6.1", 978 | "resolved": "http://beta-internal:4873/shelljs/-/shelljs-0.6.1.tgz", 979 | "integrity": "sha1-7GIRvtGSBEIIj+D3Cyg3Iy7SyKg=", 980 | "dev": true 981 | }, 982 | "strip-bom": { 983 | "version": "3.0.0", 984 | "resolved": "http://beta-internal:4873/strip-bom/-/strip-bom-3.0.0.tgz", 985 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 986 | "dev": true 987 | }, 988 | "strip-json-comments": { 989 | "version": "1.0.4", 990 | "resolved": "http://beta-internal:4873/strip-json-comments/-/strip-json-comments-1.0.4.tgz", 991 | "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", 992 | "dev": true 993 | }, 994 | "table": { 995 | "version": "3.7.8", 996 | "resolved": "http://beta-internal:4873/table/-/table-3.7.8.tgz", 997 | "integrity": "sha1-tCRDPvWWhRkisv13IkppoZUWGOs=", 998 | "dev": true, 999 | "dependencies": { 1000 | "bluebird": { 1001 | "version": "3.4.1", 1002 | "resolved": "http://beta-internal:4873/bluebird/-/bluebird-3.4.1.tgz", 1003 | "integrity": "sha1-tzHd9I4t077awudeEhWhG8uR+gc=", 1004 | "dev": true 1005 | }, 1006 | "slice-ansi": { 1007 | "version": "0.0.4", 1008 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", 1009 | "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", 1010 | "dev": true 1011 | }, 1012 | "string-width": { 1013 | "version": "1.0.1", 1014 | "resolved": "http://beta-internal:4873/string-width/-/string-width-1.0.1.tgz", 1015 | "integrity": "sha1-ySEptvHX9SrPmvQkom44ZKBc6wo=", 1016 | "dev": true, 1017 | "dependencies": { 1018 | "code-point-at": { 1019 | "version": "1.0.0", 1020 | "resolved": "http://beta-internal:4873/code-point-at/-/code-point-at-1.0.0.tgz", 1021 | "integrity": "sha1-9psZLT99keOC5Lcb3bd4eGGasMY=", 1022 | "dev": true, 1023 | "dependencies": { 1024 | "number-is-nan": { 1025 | "version": "1.0.0", 1026 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 1027 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 1028 | "dev": true 1029 | } 1030 | } 1031 | }, 1032 | "is-fullwidth-code-point": { 1033 | "version": "1.0.0", 1034 | "resolved": "http://beta-internal:4873/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 1035 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 1036 | "dev": true, 1037 | "dependencies": { 1038 | "number-is-nan": { 1039 | "version": "1.0.0", 1040 | "resolved": "http://beta-internal:4873/number-is-nan/-/number-is-nan-1.0.0.tgz", 1041 | "integrity": "sha1-wCD1KcUoKt/dIz2R1LGBw9aG3Es=", 1042 | "dev": true 1043 | } 1044 | } 1045 | } 1046 | } 1047 | }, 1048 | "strip-ansi": { 1049 | "version": "3.0.1", 1050 | "resolved": "http://beta-internal:4873/strip-ansi/-/strip-ansi-3.0.1.tgz", 1051 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1052 | "dev": true, 1053 | "dependencies": { 1054 | "ansi-regex": { 1055 | "version": "2.0.0", 1056 | "resolved": "http://beta-internal:4873/ansi-regex/-/ansi-regex-2.0.0.tgz", 1057 | "integrity": "sha1-xQYbbg74qBd15Q9dZhUb9r83EQc=", 1058 | "dev": true 1059 | } 1060 | } 1061 | }, 1062 | "tv4": { 1063 | "version": "1.2.7", 1064 | "resolved": "http://beta-internal:4873/tv4/-/tv4-1.2.7.tgz", 1065 | "integrity": "sha1-vSk4mvxzreSa5fSBQrXVRL9o0SA=", 1066 | "dev": true 1067 | }, 1068 | "xregexp": { 1069 | "version": "3.1.1", 1070 | "resolved": "http://beta-internal:4873/xregexp/-/xregexp-3.1.1.tgz", 1071 | "integrity": "sha1-juGNde9cfLP5ln+NKUFKbKWxoYQ=", 1072 | "dev": true 1073 | } 1074 | } 1075 | }, 1076 | "text-table": { 1077 | "version": "0.2.0", 1078 | "resolved": "http://beta-internal:4873/text-table/-/text-table-0.2.0.tgz", 1079 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1080 | "dev": true 1081 | }, 1082 | "user-home": { 1083 | "version": "2.0.0", 1084 | "resolved": "http://beta-internal:4873/user-home/-/user-home-2.0.0.tgz", 1085 | "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", 1086 | "dev": true, 1087 | "dependencies": { 1088 | "os-homedir": { 1089 | "version": "1.0.1", 1090 | "resolved": "http://beta-internal:4873/os-homedir/-/os-homedir-1.0.1.tgz", 1091 | "integrity": "sha1-DWK99EuRb9O73PLKsZGUj7CU8Ac=", 1092 | "dev": true 1093 | } 1094 | } 1095 | } 1096 | } 1097 | }, 1098 | "eslint-plugin-mocha": { 1099 | "version": "4.3.0", 1100 | "resolved": "https://registry.npmjs.org/eslint-plugin-mocha/-/eslint-plugin-mocha-4.3.0.tgz", 1101 | "integrity": "sha1-hAdblvw3/pZ9KbhgIoU6FP8lLMM=", 1102 | "dev": true, 1103 | "dependencies": { 1104 | "ramda": { 1105 | "version": "0.21.0", 1106 | "resolved": "http://beta-internal:4873/ramda/-/ramda-0.21.0.tgz", 1107 | "integrity": "sha1-oAGr7bP/YQd9T/HVd9RN536NCjU=", 1108 | "dev": true 1109 | } 1110 | } 1111 | }, 1112 | "eslint-plugin-node": { 1113 | "version": "2.0.0", 1114 | "resolved": "http://beta-internal:4873/eslint-plugin-node/-/eslint-plugin-node-2.0.0.tgz", 1115 | "integrity": "sha1-1J3EJ87cDfQ2I4zcsGrNlh+jjeU=", 1116 | "dev": true, 1117 | "dependencies": { 1118 | "ignore": { 1119 | "version": "3.1.3", 1120 | "resolved": "http://beta-internal:4873/ignore/-/ignore-3.1.3.tgz", 1121 | "integrity": "sha1-nokMBlJRkRWulCfaR1Fr1U0daZk=", 1122 | "dev": true 1123 | }, 1124 | "minimatch": { 1125 | "version": "3.0.3", 1126 | "resolved": "http://beta-internal:4873/minimatch/-/minimatch-3.0.3.tgz", 1127 | "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", 1128 | "dev": true, 1129 | "dependencies": { 1130 | "brace-expansion": { 1131 | "version": "1.1.6", 1132 | "resolved": "http://beta-internal:4873/brace-expansion/-/brace-expansion-1.1.6.tgz", 1133 | "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", 1134 | "dev": true, 1135 | "dependencies": { 1136 | "balanced-match": { 1137 | "version": "0.4.2", 1138 | "resolved": "http://beta-internal:4873/balanced-match/-/balanced-match-0.4.2.tgz", 1139 | "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=", 1140 | "dev": true 1141 | }, 1142 | "concat-map": { 1143 | "version": "0.0.1", 1144 | "resolved": "http://beta-internal:4873/concat-map/-/concat-map-0.0.1.tgz", 1145 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 1146 | "dev": true 1147 | } 1148 | } 1149 | } 1150 | } 1151 | }, 1152 | "object-assign": { 1153 | "version": "4.1.0", 1154 | "resolved": "http://beta-internal:4873/object-assign/-/object-assign-4.1.0.tgz", 1155 | "integrity": "sha1-ejs9DpgGPUP0wD8uiubNUahog6A=", 1156 | "dev": true 1157 | }, 1158 | "resolve": { 1159 | "version": "1.1.7", 1160 | "resolved": "http://beta-internal:4873/resolve/-/resolve-1.1.7.tgz", 1161 | "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", 1162 | "dev": true 1163 | }, 1164 | "semver": { 1165 | "version": "5.2.0", 1166 | "resolved": "http://beta-internal:4873/semver/-/semver-5.2.0.tgz", 1167 | "integrity": "sha1-KBmVuAwUSCCUFd28TPUMJpzvVcU=", 1168 | "dev": true 1169 | } 1170 | } 1171 | }, 1172 | "faye-websocket": { 1173 | "version": "0.10.0", 1174 | "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", 1175 | "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=" 1176 | }, 1177 | "halley": { 1178 | "version": "0.4.8", 1179 | "resolved": "https://registry.npmjs.org/halley/-/halley-0.4.8.tgz", 1180 | "integrity": "sha512-RgR9cXQeEJoUIGTJrFfCMepOaeYmVjx0UVcbfPWxJkzDMBsGUvzdz3OJ1q4so51YI9Tc0PzuO96uSTPIPSAJXw==", 1181 | "dependencies": { 1182 | "backbone": { 1183 | "version": "1.2.3", 1184 | "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.2.3.tgz", 1185 | "integrity": "sha1-wiz9B/yG676uYdGJKe0RXpmdZbk=" 1186 | } 1187 | } 1188 | }, 1189 | "inherits": { 1190 | "version": "2.0.3", 1191 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 1192 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 1193 | }, 1194 | "lodash": { 1195 | "version": "3.10.1", 1196 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", 1197 | "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" 1198 | }, 1199 | "loglevel": { 1200 | "version": "1.4.1", 1201 | "resolved": "http://beta-internal:4873/loglevel/-/loglevel-1.4.1.tgz", 1202 | "integrity": "sha1-lbOD+Ro8J1b9SrCTZn5DCRYfK80=" 1203 | }, 1204 | "mocha": { 1205 | "version": "2.5.3", 1206 | "resolved": "http://beta-internal:4873/mocha/-/mocha-2.5.3.tgz", 1207 | "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", 1208 | "dev": true, 1209 | "dependencies": { 1210 | "commander": { 1211 | "version": "2.3.0", 1212 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", 1213 | "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", 1214 | "dev": true 1215 | }, 1216 | "debug": { 1217 | "version": "2.2.0", 1218 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 1219 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 1220 | "dev": true, 1221 | "dependencies": { 1222 | "ms": { 1223 | "version": "0.7.1", 1224 | "resolved": "http://beta-internal:4873/ms/-/ms-0.7.1.tgz", 1225 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", 1226 | "dev": true 1227 | } 1228 | } 1229 | }, 1230 | "diff": { 1231 | "version": "1.4.0", 1232 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", 1233 | "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", 1234 | "dev": true 1235 | }, 1236 | "escape-string-regexp": { 1237 | "version": "1.0.2", 1238 | "resolved": "http://beta-internal:4873/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", 1239 | "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", 1240 | "dev": true 1241 | }, 1242 | "glob": { 1243 | "version": "3.2.11", 1244 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 1245 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 1246 | "dev": true, 1247 | "dependencies": { 1248 | "inherits": { 1249 | "version": "2.0.1", 1250 | "resolved": "http://beta-internal:4873/inherits/-/inherits-2.0.1.tgz", 1251 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 1252 | "dev": true 1253 | }, 1254 | "minimatch": { 1255 | "version": "0.3.0", 1256 | "resolved": "http://beta-internal:4873/minimatch/-/minimatch-0.3.0.tgz", 1257 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 1258 | "dev": true, 1259 | "dependencies": { 1260 | "lru-cache": { 1261 | "version": "2.7.3", 1262 | "resolved": "http://beta-internal:4873/lru-cache/-/lru-cache-2.7.3.tgz", 1263 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 1264 | "dev": true 1265 | }, 1266 | "sigmund": { 1267 | "version": "1.0.1", 1268 | "resolved": "http://beta-internal:4873/sigmund/-/sigmund-1.0.1.tgz", 1269 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 1270 | "dev": true 1271 | } 1272 | } 1273 | } 1274 | } 1275 | }, 1276 | "growl": { 1277 | "version": "1.9.2", 1278 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 1279 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 1280 | "dev": true 1281 | }, 1282 | "jade": { 1283 | "version": "0.26.3", 1284 | "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", 1285 | "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", 1286 | "dev": true, 1287 | "dependencies": { 1288 | "commander": { 1289 | "version": "0.6.1", 1290 | "resolved": "http://beta-internal:4873/commander/-/commander-0.6.1.tgz", 1291 | "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", 1292 | "dev": true 1293 | }, 1294 | "mkdirp": { 1295 | "version": "0.3.0", 1296 | "resolved": "http://beta-internal:4873/mkdirp/-/mkdirp-0.3.0.tgz", 1297 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", 1298 | "dev": true 1299 | } 1300 | } 1301 | }, 1302 | "mkdirp": { 1303 | "version": "0.5.1", 1304 | "resolved": "http://beta-internal:4873/mkdirp/-/mkdirp-0.5.1.tgz", 1305 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1306 | "dev": true, 1307 | "dependencies": { 1308 | "minimist": { 1309 | "version": "0.0.8", 1310 | "resolved": "http://beta-internal:4873/minimist/-/minimist-0.0.8.tgz", 1311 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 1312 | "dev": true 1313 | } 1314 | } 1315 | }, 1316 | "supports-color": { 1317 | "version": "1.2.0", 1318 | "resolved": "http://beta-internal:4873/supports-color/-/supports-color-1.2.0.tgz", 1319 | "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", 1320 | "dev": true 1321 | }, 1322 | "to-iso-string": { 1323 | "version": "0.0.2", 1324 | "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", 1325 | "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", 1326 | "dev": true 1327 | } 1328 | } 1329 | }, 1330 | "moment": { 1331 | "version": "2.14.1", 1332 | "resolved": "http://beta-internal:4873/moment/-/moment-2.14.1.tgz", 1333 | "integrity": "sha1-s1snxH5X7S3ccAU9awe+zbKRdBw=" 1334 | }, 1335 | "ms": { 1336 | "version": "2.0.0", 1337 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1338 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1339 | }, 1340 | "underscore": { 1341 | "version": "1.8.3", 1342 | "resolved": "http://beta-internal:4873/underscore/-/underscore-1.8.3.tgz", 1343 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" 1344 | }, 1345 | "websocket-driver": { 1346 | "version": "0.6.5", 1347 | "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", 1348 | "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=" 1349 | }, 1350 | "websocket-extensions": { 1351 | "version": "0.1.1", 1352 | "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.1.tgz", 1353 | "integrity": "sha1-domUmcGEtu91Q3fC27DNbLVdKec=" 1354 | } 1355 | } 1356 | } 1357 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitter-realtime-client", 3 | "version": "1.6.2", 4 | "description": "Gitter Realtime Client", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha test/*" 8 | }, 9 | "keywords": [ 10 | "gitter", 11 | "realtime", 12 | "client", 13 | "faye" 14 | ], 15 | "author": "Andrew Newdigate", 16 | "license": "MIT", 17 | "dependencies": { 18 | "backbone": "^1.3.3", 19 | "backbone-url-resolver": "^0.1.1", 20 | "debug-proxy": "^0.2.0", 21 | "halley": "^0.4.8", 22 | "loglevel": "^1.2.0", 23 | "moment": "^2.9.0", 24 | "underscore": "^1.8.3" 25 | }, 26 | "devDependencies": { 27 | "blocked": "^1.1.0", 28 | "eslint": "^3.2.2", 29 | "eslint-plugin-mocha": "^4.3.0", 30 | "eslint-plugin-node": "^2.0.0", 31 | "mocha": "^2.1.0" 32 | }, 33 | "publishConfig": { 34 | "registry": "https://registry.npmjs.org" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "plugins": [ 8 | "mocha" 9 | ], 10 | "rules": { 11 | "mocha/no-exclusive-tests": "error", 12 | "no-console": "warn", 13 | "strict": [ 14 | "warn", 15 | "safe" 16 | ], 17 | "max-nested-callbacks": [ 18 | "error", 19 | 10 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /test/limited-collection-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Backbone = require('backbone'); 5 | var LimitedCollection = require('../lib/limited-collection'); 6 | 7 | describe('LimitedCollection', function() { 8 | 9 | var baseCollection; 10 | var limitedCollection; 11 | var events; 12 | 13 | beforeEach(function() { 14 | events = []; 15 | baseCollection = new Backbone.Collection([], { 16 | comparator: function(a, b) { 17 | return a.get('i') - b.get('i'); 18 | } 19 | }); 20 | 21 | limitedCollection = new LimitedCollection([], { 22 | collection: baseCollection, 23 | maxLength: 3 24 | }); 25 | 26 | limitedCollection.on('add', function(model) { 27 | events.push({ type: 'add', i: model.get('i') }); 28 | }); 29 | 30 | limitedCollection.on('remove', function(model) { 31 | events.push({ type: 'remove', i: model.get('i') }); 32 | }); 33 | 34 | limitedCollection.on('reset', function() { 35 | events.push({ type: 'reset' }); 36 | }); 37 | 38 | limitedCollection.on('sort', function() { 39 | events.push({ type: 'sort' }); 40 | }); 41 | 42 | limitedCollection.on('change', function(model) { 43 | events.push({ type: 'change', i: model.get('i'), prevI: model.previous('i') }); 44 | }); 45 | 46 | }); 47 | 48 | it('should handle an empty collection', function() { 49 | assert.strictEqual(limitedCollection.length, 0); 50 | assert.deepEqual(events, []); 51 | }); 52 | 53 | it('should handle adds', function() { 54 | assert.strictEqual(limitedCollection.length, 0); 55 | 56 | baseCollection.add({ i: 0 }); 57 | assert.strictEqual(limitedCollection.length, 1); 58 | assert.deepEqual(events, [ 59 | { type: 'add', i: 0 }, 60 | { type: 'sort' } 61 | ]); 62 | 63 | events = []; 64 | baseCollection.add({ i: 1 }); 65 | assert.strictEqual(limitedCollection.length, 2); 66 | assert.deepEqual(events, [ 67 | { type: 'add', i: 1 }, 68 | { type: 'sort' } 69 | ]); 70 | 71 | events = []; 72 | baseCollection.add({ i: 2 }); 73 | assert.strictEqual(limitedCollection.length, 3); 74 | assert.deepEqual(events, [ 75 | { type: 'add', i: 2 }, 76 | { type: 'sort' } 77 | ]); 78 | 79 | events = []; 80 | baseCollection.add({ i: 3 }); 81 | assert.strictEqual(limitedCollection.length, 3); 82 | assert.deepEqual(events, []); 83 | 84 | 85 | var limitedPluck = limitedCollection.pluck('i'); 86 | assert.deepEqual(limitedPluck, [0, 1, 2]); 87 | }); 88 | 89 | it('should handle duplicate adds', function() { 90 | var a1 = baseCollection.add({ i: 0 }); 91 | assert.strictEqual(limitedCollection.length, 1); 92 | assert.deepEqual(events, [ 93 | { type: 'add', i: 0 }, 94 | { type: 'sort' } 95 | ]); 96 | 97 | events = []; 98 | baseCollection.add(a1); 99 | assert.strictEqual(limitedCollection.length, 1); 100 | assert.deepEqual(events, []); 101 | 102 | var limitedPluck = limitedCollection.pluck('i'); 103 | assert.deepEqual(limitedPluck, [0]); 104 | }); 105 | 106 | it('should handle removes inside the limit with no more items', function() { 107 | baseCollection.add({ i: 0 }); 108 | baseCollection.add({ i: 1 }); 109 | var i2 = baseCollection.add({ i: 2 }); 110 | 111 | assert.strictEqual(limitedCollection.length, 3); 112 | 113 | events = []; 114 | baseCollection.remove(i2); 115 | assert.strictEqual(limitedCollection.length, 2); 116 | assert.deepEqual(events, [ 117 | { type: 'remove', i: 2 } 118 | ]); 119 | 120 | var limitedPluck = limitedCollection.pluck('i'); 121 | assert.deepEqual(limitedPluck, [0, 1]); 122 | }); 123 | 124 | it('should handle removes inside the limit when theres more items', function() { 125 | baseCollection.add({ i: 0 }); 126 | baseCollection.add({ i: 1 }); 127 | var i2 = baseCollection.add({ i: 2 }); 128 | baseCollection.add({ i: 3 }); 129 | 130 | assert.strictEqual(limitedCollection.length, 3); 131 | 132 | events = []; 133 | baseCollection.remove(i2); 134 | assert.strictEqual(limitedCollection.length, 3); 135 | assert.deepEqual(events, [ 136 | { type: 'remove', i: 2 }, 137 | { type: 'add', i: 3 }, 138 | { type: 'sort' } 139 | ]); 140 | 141 | var limitedPluck = limitedCollection.pluck('i'); 142 | assert.deepEqual(limitedPluck, [0, 1, 3]); 143 | }); 144 | 145 | it('should handle removes outside the limit', function() { 146 | baseCollection.add({ i: 0 }); 147 | baseCollection.add({ i: 1 }); 148 | baseCollection.add({ i: 2 }); 149 | var i3 = baseCollection.add({ i: 3 }); 150 | 151 | assert.strictEqual(limitedCollection.length, 3); 152 | 153 | events = []; 154 | baseCollection.remove(i3); 155 | assert.strictEqual(limitedCollection.length, 3); 156 | assert.deepEqual(events, []); 157 | 158 | var limitedPluck = limitedCollection.pluck('i'); 159 | assert.deepEqual(limitedPluck, [0, 1, 2]); 160 | }); 161 | 162 | it('should handle resets', function() { 163 | baseCollection.add({ i: 0 }); 164 | baseCollection.add({ i: 1 }); 165 | baseCollection.add({ i: 2 }); 166 | assert.strictEqual(limitedCollection.length, 3); 167 | 168 | events = []; 169 | baseCollection.reset(); 170 | assert.strictEqual(limitedCollection.length, 0); 171 | assert.deepEqual(events, [{ 172 | type: 'reset' 173 | }]); 174 | 175 | var limitedPluck = limitedCollection.pluck('i'); 176 | assert.deepEqual(limitedPluck, []); 177 | }); 178 | 179 | it('should handle populated resets', function() { 180 | baseCollection.add({ i: 0 }); 181 | baseCollection.add({ i: 1 }); 182 | baseCollection.add({ i: 2 }); 183 | assert.strictEqual(limitedCollection.length, 3); 184 | 185 | events = []; 186 | baseCollection.reset([ { i: 3 }, { i: 4 }, { i: 1 }, { i: 5 }]); 187 | assert.strictEqual(limitedCollection.length, 3); 188 | assert.deepEqual(events, [{ 189 | type: 'reset' 190 | }]); 191 | 192 | var limitedPluck = limitedCollection.pluck('i'); 193 | assert.deepEqual(limitedPluck, [1, 3, 4]); 194 | }); 195 | 196 | }); 197 | -------------------------------------------------------------------------------- /test/room-collection-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var realtimeClient = require('..'); 4 | require('loglevel').setLevel('debug'); 5 | 6 | var blocked = require('blocked'); 7 | blocked(function(ms){ 8 | if (ms > 0) { 9 | // console.log('BLOCKED FOR %sms', ms); 10 | } 11 | }); 12 | 13 | var client = new realtimeClient.RealtimeClient({ 14 | // fayeUrl: 'https://ws-beta.gitter.im/faye', 15 | token: process.env.GITTER_TOKEN, 16 | fayeOptions: { 17 | 18 | } 19 | }); 20 | 21 | var rooms = new realtimeClient.RoomCollection([], { client: client, listen: true }); 22 | 23 | var favs = realtimeClient.filteredRooms.favourites(rooms); 24 | 25 | /* Display all the favs, all the time */ 26 | favs.on('add remove reset change', function() { 27 | // console.log(favs.toJSON()); 28 | // console.log('change'); 29 | }); 30 | 31 | rooms.on('change:unreadItems', function(/*model*/) { 32 | // console.log(model.get('uri') + ' ' + model.get('unreadItems')) 33 | // console.log('unread'); 34 | }); 35 | -------------------------------------------------------------------------------- /test/simple-filtered-collection-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var Backbone = require('backbone'); 5 | var SimpleFilteredCollection = require('../lib/simple-filtered-collection'); 6 | 7 | function bindEvents(events, collection) { 8 | collection.on('add', function(model) { 9 | events.push({ type: 'add', i: model.get('i') }); 10 | }); 11 | 12 | collection.on('remove', function(model) { 13 | events.push({ type: 'remove', i: model.get('i') }); 14 | }); 15 | 16 | collection.on('reset', function() { 17 | events.push({ type: 'reset' }); 18 | }); 19 | 20 | collection.on('change', function(model) { 21 | events.push({ type: 'change', i: model.get('i'), prevI: model.previous('i') }); 22 | }); 23 | } 24 | 25 | describe('SimpleFilteredCollection', function() { 26 | 27 | describe('adding and removing', function() { 28 | 29 | var baseCollection; 30 | var filteredCollection; 31 | var events; 32 | 33 | beforeEach(function() { 34 | events = []; 35 | baseCollection = new Backbone.Collection([]); 36 | filteredCollection = new SimpleFilteredCollection([], { 37 | collection: baseCollection, 38 | filter: function(model) { 39 | return model.get('i') % 2 === 0; 40 | } 41 | }); 42 | 43 | bindEvents(events, filteredCollection); 44 | }); 45 | 46 | it('should handle an empty collection', function() { 47 | assert.strictEqual(filteredCollection.length, 0); 48 | assert.deepEqual(events, []); 49 | }); 50 | 51 | it('should handle adds that match', function() { 52 | baseCollection.add({ i: 0 }); 53 | assert.strictEqual(filteredCollection.length, 1); 54 | assert.deepEqual(events, [ 55 | { type: 'add', i: 0 } 56 | ]); 57 | }); 58 | 59 | it('should handle adds that do not match match', function() { 60 | baseCollection.add({ i: 1 }) 61 | assert.strictEqual(filteredCollection.length, 0); 62 | assert.deepEqual(events, []); 63 | }); 64 | 65 | it('should handle items being changed from a match to not a match', function() { 66 | var model = new Backbone.Model({ i: 1 }) 67 | baseCollection.add(model); 68 | assert.strictEqual(filteredCollection.length, 0); 69 | events.length = 0; 70 | 71 | model.set({ i: 2 }); 72 | assert.strictEqual(filteredCollection.length, 1); 73 | assert.deepEqual(events, [ 74 | { type: 'add', i: 2 } 75 | ]); 76 | }); 77 | 78 | it('should handle items being changed from a non-match to a match', function() { 79 | var model = new Backbone.Model({ i: 2 }) 80 | baseCollection.add(model); 81 | assert.strictEqual(filteredCollection.length, 1); 82 | events.length = 0; 83 | 84 | model.set({ i: 1 }); 85 | assert.strictEqual(filteredCollection.length, 0); 86 | assert.deepEqual(events, [ 87 | { type: 'remove', i: 1 } 88 | ]); 89 | }); 90 | 91 | it('should handle resets', function() { 92 | baseCollection.add({ i: 0 }); 93 | assert.strictEqual(filteredCollection.length, 1); 94 | events.length = 0; 95 | 96 | baseCollection.reset(); 97 | assert.strictEqual(filteredCollection.length, 0); 98 | 99 | assert.deepEqual(events, [ 100 | { type: 'reset' } 101 | ]); 102 | }) 103 | 104 | it('should handle populated resets', function() { 105 | baseCollection.add({ i: 0 }); 106 | assert.strictEqual(filteredCollection.length, 1); 107 | events.length = 0; 108 | 109 | baseCollection.reset([{ i: 0 }, { i: 1 }, { i: 2 }]); 110 | assert.strictEqual(filteredCollection.length, 2); 111 | assert.deepEqual(events, [ 112 | { type: 'reset' } 113 | ]); 114 | }); 115 | 116 | it('should handle filter changes ', function() { 117 | baseCollection.add({ i: 0 }); 118 | baseCollection.add({ i: 1 }); 119 | baseCollection.add({ i: 2 }); 120 | baseCollection.add({ i: 3 }); 121 | baseCollection.add({ i: 4 }); 122 | baseCollection.add({ i: 5 }); 123 | baseCollection.add({ i: 6 }); 124 | 125 | assert.strictEqual(filteredCollection.length, 4); 126 | events.length = 0; 127 | 128 | filteredCollection.setFilter(function() { 129 | return false; 130 | }); 131 | 132 | assert.strictEqual(filteredCollection.length, 0); 133 | 134 | assert.deepEqual(events, [ 135 | { type: 'remove', i: 6 }, 136 | { type: 'remove', i: 4 }, 137 | { type: 'remove', i: 2 }, 138 | { type: 'remove', i: 0 } 139 | ]); 140 | 141 | events.length = 0; 142 | 143 | filteredCollection.setFilter(function() { 144 | return true; 145 | }); 146 | 147 | assert.strictEqual(filteredCollection.length, 7); 148 | 149 | assert.deepEqual(events, [ 150 | { type: 'add', i: 0 }, 151 | { type: 'add', i: 1 }, 152 | { type: 'add', i: 2 }, 153 | { type: 'add', i: 3 }, 154 | { type: 'add', i: 4 }, 155 | { type: 'add', i: 5 }, 156 | { type: 'add', i: 6 }, 157 | ]); 158 | }) 159 | 160 | it('should handle partial filter changes ', function() { 161 | baseCollection.add({ i: 0 }); 162 | baseCollection.add({ i: 1 }); 163 | baseCollection.add({ i: 2 }); 164 | baseCollection.add({ i: 3 }); 165 | baseCollection.add({ i: 4 }); 166 | baseCollection.add({ i: 5 }); 167 | baseCollection.add({ i: 6 }); 168 | 169 | assert.strictEqual(filteredCollection.length, 4); 170 | events.length = 0; 171 | 172 | filteredCollection.setFilter(function(model) { 173 | return model.get('i') % 3 === 0; 174 | }); 175 | 176 | assert.strictEqual(filteredCollection.length, 3); 177 | 178 | assert.deepEqual(events, [ 179 | { type: 'remove', i: 4 }, 180 | { type: 'remove', i: 2 }, 181 | { type: 'add', i: 3 }, 182 | ]); 183 | }); 184 | }); 185 | 186 | 187 | describe('sorting', function() { 188 | describe('no autoResort', function() { 189 | var baseCollection; 190 | var filteredCollection; 191 | var items; 192 | 193 | beforeEach(function() { 194 | baseCollection = new Backbone.Collection(items, { 195 | comparator: function(a, b) { 196 | return a.get('i') - b.get('i'); 197 | } 198 | }); 199 | 200 | filteredCollection = new SimpleFilteredCollection([], { 201 | collection: baseCollection, 202 | filter: function(model) { 203 | return model.get('i') % 2 === 0; 204 | }, 205 | comparator: function(a, b) { 206 | return b.get('i') - a.get('i'); 207 | } 208 | }); 209 | }); 210 | 211 | describe('without preloaded', function() { 212 | before(function() { 213 | items = []; 214 | }); 215 | 216 | it('should allow alternative sorting to its parent', function() { 217 | baseCollection.add({ i: 3 }); 218 | baseCollection.add({ i: 5 }); 219 | baseCollection.add({ i: 4 }); 220 | baseCollection.add({ i: 2 }); 221 | baseCollection.add({ i: 1 }); 222 | 223 | assert.strictEqual(baseCollection.length, 5); 224 | assert.strictEqual(filteredCollection.length, 2); 225 | 226 | var basePluck = baseCollection.pluck('i'); 227 | assert.deepEqual(basePluck, [1, 2, 3, 4, 5]); 228 | 229 | var filteredPluck = filteredCollection.pluck('i'); 230 | assert.deepEqual(filteredPluck, [4, 2]); 231 | }); 232 | 233 | }); 234 | 235 | describe('preloaded', function() { 236 | before(function() { 237 | items = [{ i: 7 }, { i: 8 }, { i: 100 }]; 238 | }); 239 | 240 | it('should allow alternative sorting to its parent', function() { 241 | baseCollection.add({ i: 3 }); 242 | baseCollection.add({ i: 5 }); 243 | baseCollection.add({ i: 4 }); 244 | baseCollection.add({ i: 2 }); 245 | baseCollection.add({ i: 1 }); 246 | 247 | assert.strictEqual(baseCollection.length, 8); 248 | assert.strictEqual(filteredCollection.length, 4); 249 | 250 | var basePluck = baseCollection.pluck('i'); 251 | assert.deepEqual(basePluck, [1, 2, 3, 4, 5, 7, 8, 100]); 252 | 253 | var filteredPluck = filteredCollection.pluck('i'); 254 | assert.deepEqual(filteredPluck, [100, 8, 4, 2]); 255 | }); 256 | 257 | }); 258 | }); 259 | 260 | describe('with autoResort', function() { 261 | var baseCollection; 262 | var filteredCollection; 263 | var items; 264 | var events; 265 | 266 | beforeEach(function() { 267 | items = []; 268 | events = []; 269 | 270 | baseCollection = new Backbone.Collection(items, { 271 | comparator: function(a, b) { 272 | return a.get('i') - b.get('i'); 273 | } 274 | }); 275 | 276 | filteredCollection = new SimpleFilteredCollection([], { 277 | collection: baseCollection, 278 | autoResort: true, 279 | filter: function(model) { 280 | return model.get('i') % 2 === 0; 281 | }, 282 | comparator: function(a, b) { 283 | return b.get('i') - a.get('i'); 284 | } 285 | }); 286 | 287 | bindEvents(events, filteredCollection); 288 | }); 289 | 290 | it('should auto resort', function() { 291 | var i6 = baseCollection.add({ i: 6 }); 292 | baseCollection.add({ i: 10 }); 293 | baseCollection.add({ i: 8 }); 294 | baseCollection.add({ i: 4 }); 295 | baseCollection.add({ i: 2 }); 296 | 297 | var filteredPluck = filteredCollection.pluck('i'); 298 | assert.deepEqual(filteredPluck, [10, 8, 6, 4, 2]); 299 | 300 | events.length = 0; 301 | 302 | i6.set({ i: 12 }); 303 | 304 | assert.deepEqual(events, [{ 305 | "i": 12, 306 | "type": "remove" 307 | }, { 308 | "i": 12, 309 | "type": "add" 310 | }, { 311 | "i": 12, 312 | "prevI": 6, 313 | "type": "change" 314 | } 315 | ]); 316 | 317 | filteredPluck = filteredCollection.pluck('i'); 318 | assert.deepEqual(filteredPluck, [12, 10, 8, 4, 2]); 319 | 320 | events.length = 0; 321 | 322 | i6.set({ i: 0 }); 323 | filteredPluck = filteredCollection.pluck('i'); 324 | assert.deepEqual(filteredPluck, [10, 8, 4, 2, 0]); 325 | 326 | assert.deepEqual(events, [{ 327 | "i": 0, 328 | "type": "remove" 329 | }, { 330 | "i": 0, 331 | "type": "add" 332 | }, { 333 | "i": 0, 334 | "prevI": 12, 335 | "type": "change" 336 | } 337 | ]); 338 | 339 | }); 340 | 341 | it('should not move models when the change does not affect the comparator', function() { 342 | var i6 = baseCollection.add({ i: 6 }); 343 | baseCollection.add({ i: 10 }); 344 | baseCollection.add({ i: 8 }); 345 | baseCollection.add({ i: 4 }); 346 | baseCollection.add({ i: 2 }); 347 | 348 | events.length = 0; 349 | i6.set({ a: 1 }); 350 | 351 | var filteredPluck = filteredCollection.pluck('i'); 352 | assert.deepEqual(filteredPluck, [10, 8, 6, 4, 2]); 353 | 354 | assert.deepEqual(events, [{ 355 | "i": 6, 356 | "prevI": 6, 357 | "type": "change" 358 | } 359 | ]); 360 | 361 | }); 362 | 363 | it('should not move models when the change does not affect the position of the model, at end', function() { 364 | baseCollection.add({ i: 6 }); 365 | baseCollection.add({ i: 10 }); 366 | baseCollection.add({ i: 8 }); 367 | baseCollection.add({ i: 4 }); 368 | var i2 = baseCollection.add({ i: 2 }); 369 | 370 | events.length = 0; 371 | i2.set({ i: 0 }); 372 | 373 | var filteredPluck = filteredCollection.pluck('i'); 374 | assert.deepEqual(filteredPluck, [10, 8, 6, 4, 0]); 375 | 376 | assert.deepEqual(events, [{ 377 | "i": 0, 378 | "prevI": 2, 379 | "type": "change" 380 | } 381 | ]); 382 | }); 383 | 384 | it('should not move models when the change does not affect the position of the model, at start', function() { 385 | baseCollection.add({ i: 6 }); 386 | var i10 = baseCollection.add({ i: 10 }); 387 | baseCollection.add({ i: 8 }); 388 | baseCollection.add({ i: 4 }); 389 | baseCollection.add({ i: 2 }); 390 | 391 | events.length = 0; 392 | i10.set({ i: 20 }); 393 | 394 | var filteredPluck = filteredCollection.pluck('i'); 395 | assert.deepEqual(filteredPluck, [20, 8, 6, 4, 2]); 396 | 397 | assert.deepEqual(events, [{ 398 | "i": 20, 399 | "prevI": 10, 400 | "type": "change" 401 | } 402 | ]); 403 | 404 | }); 405 | 406 | it('should not move models when the change does not affect the position of the model, in middle', function() { 407 | baseCollection.add({ i: 6 }); 408 | baseCollection.add({ i: 20 }); 409 | var i10 = baseCollection.add({ i: 10 }); 410 | baseCollection.add({ i: 4 }); 411 | baseCollection.add({ i: 2 }); 412 | 413 | events.length = 0; 414 | i10.set({ i: 8 }); 415 | 416 | var filteredPluck = filteredCollection.pluck('i'); 417 | assert.deepEqual(filteredPluck, [20, 8, 6, 4, 2]); 418 | 419 | assert.deepEqual(events, [{ 420 | "i": 8, 421 | "prevI": 10, 422 | "type": "change" 423 | } 424 | ]); 425 | 426 | }); 427 | 428 | 429 | }); 430 | 431 | }); 432 | 433 | }); 434 | -------------------------------------------------------------------------------- /test/sorted-array-index-search-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var sortedArrayIndexSearch = require('../lib/sorted-array-index-search'); 5 | 6 | var intComparator = function(a, b) { 7 | return a - b; 8 | } 9 | var xComparator = function(a, b) { 10 | return a.x - b.x; 11 | } 12 | 13 | describe('sorted-array-index-search', function() { 14 | 15 | it('should work with int arrays', function() { 16 | var array = [2, 3, 4, 5, 6, 7, 8]; 17 | assert.strictEqual(sortedArrayIndexSearch(array, intComparator, 1), 0); 18 | assert.strictEqual(sortedArrayIndexSearch(array, intComparator, 2), 0); 19 | assert.strictEqual(sortedArrayIndexSearch(array, intComparator, 3), 1); 20 | assert.strictEqual(sortedArrayIndexSearch(array, intComparator, 8), 6); 21 | assert.strictEqual(sortedArrayIndexSearch(array, intComparator, 9), 7); 22 | }); 23 | 24 | it('should work with object arrays', function() { 25 | var array = [2, 3, 4, 5, 6, 7, 8].map(function(x) { 26 | return { x: x }; 27 | }); 28 | 29 | assert.strictEqual(sortedArrayIndexSearch(array, xComparator, { x: 1 }), 0); 30 | assert.strictEqual(sortedArrayIndexSearch(array, xComparator, { x: 2 }), 0); 31 | assert.strictEqual(sortedArrayIndexSearch(array, xComparator, { x: 3 }), 1); 32 | assert.strictEqual(sortedArrayIndexSearch(array, xComparator, { x: 8 }), 6); 33 | assert.strictEqual(sortedArrayIndexSearch(array, xComparator, { x: 9 }), 7); 34 | }); 35 | 36 | 37 | }) 38 | -------------------------------------------------------------------------------- /test/sorts-filters.js: -------------------------------------------------------------------------------- 1 | /*jslint node:true, unused:true*/ 2 | /*global describe:true, it:true */ 3 | 4 | 'use strict'; 5 | 6 | var roomSort = require('../lib/sorts-filters'); 7 | var Backbone = require('backbone'); 8 | var assert = require('assert'); 9 | 10 | var VERY_VERY_OLD = new Date('1066-10-29T12:00:20.250Z'); 11 | var VERY_OLD = new Date('1492-10-29T12:00:20.250Z'); 12 | var OLD = new Date('1985-10-29T12:00:20.250Z'); 13 | var NEW = new Date('2014-10-29T12:00:20.250Z'); 14 | 15 | describe('room-sort', function() { 16 | 17 | describe('favourites', function() { 18 | it('filters out non favourites', function() { 19 | var collection = new Backbone.Collection([ 20 | { id: 1, favourite: 1 }, 21 | { id: 2 } 22 | ]); 23 | 24 | var filteredCollection = collection.filter(roomSort.model.favourites.filter); 25 | 26 | assert.deepEqual(id(filteredCollection), [1]); 27 | }); 28 | 29 | it('sorts by favourite rank', function() { 30 | var collection = new Backbone.Collection([ 31 | { id: 1, favourite: 3 }, 32 | { id: 2, favourite: 1 }, 33 | { id: 3, favourite: 2 } 34 | ]); 35 | 36 | collection.comparator = roomSort.model.favourites.sort; 37 | 38 | var filteredCollection = collection.sort(); 39 | 40 | assert.deepEqual(id(filteredCollection), [2, 3, 1]); 41 | }); 42 | }); 43 | 44 | describe('recents', function() { 45 | it('filters out favourites', function() { 46 | var collection = new Backbone.Collection([ 47 | { id: 1, favourite: 1 }, 48 | { id: 2, unreadItems: 1 } 49 | ]); 50 | 51 | var filteredCollection = collection.filter(roomSort.model.recents.filter); 52 | 53 | assert.deepEqual(id(filteredCollection), [2]); 54 | }); 55 | 56 | describe('sort', function() { 57 | 58 | var RecentsCollection = Backbone.Collection.extend({ comparator: roomSort.model.recents.sort }); 59 | 60 | describe('@mentions', function() { 61 | 62 | it('puts them above unread rooms', function() { 63 | var collection = new RecentsCollection([ 64 | { id: 'unread', unreadItems: 2 }, 65 | { id: 'mentioned', unreadItems: 1, mentions: 1 } 66 | ]); 67 | 68 | collection.sort(); 69 | 70 | assert.deepEqual(id(collection), ['mentioned', 'unread']); 71 | }); 72 | 73 | it('sorts multiple @mentioned rooms by time of last access', function() { 74 | var collection = new RecentsCollection([ 75 | { id: 'old_mentions', unreadItems: 3, mentions: 5, lastAccessTime: OLD }, 76 | { id: 'new_mentions', unreadItems: 5, mentions: 3, lastAccessTime: NEW } 77 | ]); 78 | 79 | collection.sort(); 80 | 81 | assert.deepEqual(id(collection), ['new_mentions', 'old_mentions']); 82 | }); 83 | 84 | it('puts @mentioned rooms that havent been accessed at the bottom', function() { 85 | var collection = new RecentsCollection([ 86 | { id: 'never_accessed_mentions', unreadItems: 1, mentions: 1 }, 87 | { id: 'accessed_mentions', unreadItems: 1, mentions: 1, lastAccessTime: NEW } 88 | ]); 89 | 90 | collection.sort(); 91 | 92 | assert.deepEqual(id(collection), ['accessed_mentions', 'never_accessed_mentions']); 93 | }); 94 | 95 | it('doesnt move rooms once they have been accessed', function() { 96 | var room = new Backbone.Model({ 97 | id: 'room', 98 | unreadItems: 5, 99 | mentions: 3, 100 | hadUnreadItemsOnLoad: true, 101 | hadMentionsOnLoad: true, 102 | lastAccessTime: OLD, 103 | lastAccessTimeOnLoad: OLD 104 | }); 105 | var roomToUpdate = new Backbone.Model({ 106 | id: 'room_to_update', 107 | unreadItems: 3, 108 | mentions: 1, 109 | hadUnreadItemsOnLoad: true, 110 | hadMentionsOnLoad: true, 111 | lastAccessTime: VERY_OLD, 112 | lastAccessTimeOnLoad: VERY_OLD 113 | }); 114 | var collection = new RecentsCollection([room, roomToUpdate]); 115 | 116 | collection.sort(); 117 | 118 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 119 | 120 | roomToUpdate.set('lastAccessTime', NEW); 121 | collection.sort(); 122 | 123 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 124 | }); 125 | 126 | it('doesnt move rooms once they have been read', function() { 127 | var room = new Backbone.Model({ 128 | id: 'room', 129 | unreadItems: 1, 130 | mentions: 1, 131 | hadUnreadItemsOnLoad: true, 132 | hadMentionsOnLoad: true, 133 | lastAccessTime: OLD, 134 | lastAccessTimeOnLoad: OLD 135 | }); 136 | var roomToUpdate = new Backbone.Model({ 137 | id: 'room_to_update', 138 | unreadItems: 1, 139 | mentions: 1, 140 | hadUnreadItemsOnLoad: true, 141 | hadMentionsOnLoad: true, 142 | lastAccessTime: VERY_OLD, 143 | lastAccessTimeOnLoad: VERY_OLD 144 | }); 145 | var veryVeryOldRoom = new Backbone.Model({ 146 | id: 'very_very_old_room', 147 | unreadItems: 1, 148 | mentions: 1, 149 | hadUnreadItemsOnLoad: true, 150 | hadMentionsOnLoad: true, 151 | lastAccessTime: VERY_VERY_OLD, 152 | lastAccessTimeOnLoad: VERY_VERY_OLD 153 | }); 154 | var collection = new RecentsCollection([room, roomToUpdate, veryVeryOldRoom]); 155 | 156 | collection.sort(); 157 | 158 | assert.deepEqual(id(collection), ['room', 'room_to_update', 'very_very_old_room']); 159 | 160 | roomToUpdate.set('lastAccessTime', NEW); 161 | roomToUpdate.set('unreadItems', 0); 162 | roomToUpdate.set('mentions', 0); 163 | collection.sort(); 164 | 165 | assert.deepEqual(id(collection), ['room', 'room_to_update', 'very_very_old_room']); 166 | }); 167 | }); 168 | 169 | describe('unread', function() { 170 | it('puts them above regular rooms', function() { 171 | var collection = new RecentsCollection([ 172 | { id: 'regular' }, 173 | { id: 'unread', unreadItems: 1 } 174 | ]); 175 | 176 | collection.sort(); 177 | 178 | assert.deepEqual(id(collection), ['unread', 'regular']); 179 | }); 180 | 181 | it('puts new unreads above rooms that had unreads (issue troupe/gitter-webapp#368)', function() { 182 | var collection = new RecentsCollection([ 183 | { id: 'regular' }, 184 | { id: 'was_unread', hadUnreadItemsOnLoad: true, lastAccessTime: OLD }, 185 | { id: 'unread', unreadItems: 1, escalationTime: NEW } 186 | ]); 187 | 188 | collection.sort(); 189 | 190 | assert.deepEqual(id(collection), ['unread', 'was_unread', 'regular']); 191 | }); 192 | 193 | it('sorts multiple unread rooms by time of last access', function() { 194 | var collection = new RecentsCollection([ 195 | { id: 'old_unread', unreadItems: 5, lastAccessTime: OLD }, 196 | { id: 'new_unread', unreadItems: 1, lastAccessTime: NEW } 197 | ]); 198 | 199 | collection.sort(); 200 | 201 | assert.deepEqual(id(collection), ['new_unread', 'old_unread']); 202 | }); 203 | 204 | it('puts unread rooms that havent been accessed at the bottom', function() { 205 | var collection = new RecentsCollection([ 206 | { id: 'never_accessed_unread', unreadItems: 2 }, 207 | { id: 'accessed_unread', unreadItems: 1, lastAccessTime: NEW } 208 | ]); 209 | 210 | collection.sort(); 211 | 212 | assert.deepEqual(id(collection), ['accessed_unread', 'never_accessed_unread']); 213 | }); 214 | 215 | it('doesnt move rooms once they have been accessed', function() { 216 | var room = new Backbone.Model({ 217 | id: 'room', 218 | unreadItems: 5, 219 | hadUnreadItemsOnLoad: true, 220 | lastAccessTime: OLD, 221 | lastAccessTimeOnLoad: OLD 222 | }); 223 | var roomToUpdate = new Backbone.Model({ 224 | id: 'room_to_update', 225 | unreadItems: 3, 226 | hadUnreadItemsOnLoad: true, 227 | lastAccessTime: VERY_OLD, 228 | lastAccessTimeOnLoad: VERY_OLD 229 | }); 230 | var collection = new RecentsCollection([room, roomToUpdate]); 231 | 232 | collection.sort(); 233 | 234 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 235 | 236 | roomToUpdate.set('lastAccessTime', NEW); 237 | collection.sort(); 238 | 239 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 240 | }); 241 | 242 | it('doesnt move rooms once they have been read', function() { 243 | var room = new Backbone.Model({ 244 | id: 'room', 245 | unreadItems: 1, 246 | hadUnreadItemsOnLoad: true, 247 | lastAccessTime: OLD, 248 | lastAccessTimeOnLoad: OLD 249 | }); 250 | var roomToUpdate = new Backbone.Model({ 251 | id: 'room_to_update', 252 | unreadItems: 1, 253 | hadUnreadItemsOnLoad: true, 254 | lastAccessTime: VERY_OLD, 255 | lastAccessTimeOnLoad: VERY_OLD 256 | }); 257 | var veryVeryOldRoom = new Backbone.Model({ 258 | id: 'very_very_old_room', 259 | unreadItems: 1, 260 | hadUnreadItemsOnLoad: true, 261 | lastAccessTime: VERY_VERY_OLD, 262 | lastAccessTimeOnLoad: VERY_VERY_OLD 263 | }); 264 | var collection = new RecentsCollection([room, roomToUpdate, veryVeryOldRoom]); 265 | 266 | collection.sort(); 267 | 268 | assert.deepEqual(id(collection), ['room', 'room_to_update', 'very_very_old_room']); 269 | 270 | roomToUpdate.set('lastAccessTime', NEW); 271 | roomToUpdate.set('unreadItems', 0); 272 | roomToUpdate.set('lastUnreadItemTime', NEW); 273 | collection.sort(); 274 | 275 | assert.deepEqual(id(collection), ['room', 'room_to_update', 'very_very_old_room']); 276 | }); 277 | 278 | }); 279 | 280 | describe('regular', function() { 281 | it('sorts multiple rooms by time of last access', function() { 282 | var collection = new RecentsCollection([ 283 | { id: 'old_room', lastAccessTime: OLD }, 284 | { id: 'new_room', lastAccessTime: NEW } 285 | ]); 286 | 287 | collection.sort(); 288 | 289 | assert.deepEqual(id(collection), ['new_room', 'old_room']); 290 | }); 291 | 292 | it('puts rooms that havent been accessed at the bottom', function() { 293 | var collection = new RecentsCollection([ 294 | { id: 'never_accessed_room' }, 295 | { id: 'accessed_room', lastAccessTime: NEW } 296 | ]); 297 | 298 | collection.sort(); 299 | 300 | assert.deepEqual(id(collection), ['accessed_room', 'never_accessed_room']); 301 | }); 302 | 303 | it('doesnt move rooms if they are later accessed', function() { 304 | var room = new Backbone.Model({ id: 'room', lastAccessTime: OLD, lastAccessTimeOnLoad: OLD }); 305 | var roomToUpdate = new Backbone.Model({ id: 'room_to_update', lastAccessTime: VERY_OLD, lastAccessTimeOnLoad: VERY_OLD }); 306 | var collection = new RecentsCollection([room, roomToUpdate]); 307 | 308 | collection.sort(); 309 | 310 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 311 | 312 | roomToUpdate.set('lastAccessTime', NEW); 313 | collection.sort(); 314 | 315 | assert.deepEqual(id(collection), ['room', 'room_to_update']); 316 | }); 317 | 318 | it('promotes rooms if new unread messages arrive', function() { 319 | var room = new Backbone.Model({ id: 'room', lastAccessTime: OLD }); 320 | var roomToUpdate = new Backbone.Model({ id: 'room_to_update', lastAccessTime: VERY_OLD }); 321 | var collection = new RecentsCollection([room, roomToUpdate]); 322 | 323 | roomToUpdate.set('unreadItems', 1); 324 | roomToUpdate.set('lastUnreadItemTime', NEW); 325 | 326 | collection.sort(); 327 | 328 | assert.deepEqual(id(collection), ['room_to_update', 'room']); 329 | }); 330 | 331 | }); 332 | 333 | }); 334 | 335 | }); 336 | 337 | describe('left-menu', function(){ 338 | it('does not filter out favourites', function() { 339 | var collection = new Backbone.Collection([ 340 | { id: 1, favourite: 1, unreadItems: 1 }, 341 | { id: 2, unreadItems: 1 } 342 | ]); 343 | 344 | var filteredCollection = collection.filter(roomSort.model.leftMenu.filter); 345 | 346 | assert.deepEqual(id(filteredCollection), [1, 2]); 347 | }); 348 | 349 | }); 350 | 351 | }); 352 | 353 | function id(collection) { 354 | return collection.map(function(model) { 355 | return model.id; 356 | }); 357 | } 358 | --------------------------------------------------------------------------------