PageRenderTime 62ms CodeModel.GetById 3ms app.highlight 49ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/hooks/pubsub/index.js

https://github.com/carlolee/sails
JavaScript | 1458 lines | 749 code | 298 blank | 411 comment | 205 complexity | 74db6bf3e4963808ee244687e0c7fb38 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/**
   2 * Module dependencies.
   3 */
   4var util = require('util')
   5  , _ = require('lodash')
   6  , getSDKMetadata = require('../sockets/lib/getSDKMetadata')
   7  , STRINGFILE = require('sails-stringfile');
   8
   9
  10/**
  11 * Module errors
  12 */
  13var Err = {
  14  dependency: function (dependent, dependency) {
  15    return new Error( '\n' +
  16      'Cannot use `' + dependent + '` hook ' +
  17      'without the `' + dependency + '` hook enabled!'
  18    );
  19  }
  20};
  21
  22
  23module.exports = function(sails) {
  24
  25
  26  /**
  27   * Expose Hook definition
  28   */
  29
  30  return {
  31
  32
  33    initialize: function(cb) {
  34
  35      var self = this;
  36
  37      // If `views` and `http` hook is not enabled, complain and respond w/ error
  38      if (!sails.hooks.sockets) {
  39        return cb( Err.dependency('pubsub', 'sockets') );
  40      }
  41
  42      // Add low-level, generic socket methods.  These are mostly just wrappers
  43
  44      // around socket.io, to enforce a little abstraction.
  45      addLowLevelSocketMethods();
  46
  47      if (!sails.hooks.orm) {
  48        return cb( Err.dependency('pubsub', 'orm') );
  49      }
  50
  51      // Wait for `hook:orm:loaded`
  52      sails.on('hook:orm:loaded', function() {
  53
  54        // Do the heavy lifting
  55        self.augmentModels();
  56
  57        // Indicate that the hook is fully loaded
  58        cb();
  59
  60      });
  61
  62      // When the orm is reloaded, re-apply all of the pubsub methods to the
  63      // models
  64      sails.on('hook:orm:reloaded', function() {
  65        self.augmentModels();
  66
  67        // Trigger an event in case something needs to respond to the pubsub reload
  68        sails.emit('hook:pubsub:reloaded');
  69      });
  70
  71    },
  72
  73    augmentModels: function() {
  74      // Augment models with room/socket logic (& bind context)
  75      for (var identity in sails.models) {
  76
  77        var AugmentedModel = _.defaults(sails.models[identity], getPubsubMethods(), {autosubscribe: true} );
  78        _.bindAll(AugmentedModel,
  79          'subscribe',
  80          'watch',
  81          'introduce',
  82          'retire',
  83          'unwatch',
  84          'unsubscribe',
  85          'publish',
  86          'room',
  87          'publishCreate',
  88          'publishUpdate',
  89          'publishDestroy',
  90          'publishAdd',
  91          'publishRemove'
  92        );
  93        sails.models[identity] = AugmentedModel;
  94      }
  95    }
  96  };
  97
  98  function addLowLevelSocketMethods () {
  99
 100    sails.sockets = {};
 101    sails.sockets.DEFAULT_EVENT_NAME = 'message';
 102
 103    sails.sockets.subscribeToFirehose = require('./drink')(sails);
 104    sails.sockets.unsubscribeFromFirehose = require('./drink')(sails);
 105
 106    sails.sockets.publishToFirehose = require('./squirt')(sails);
 107
 108
 109    /**
 110     * Subscribe a socket to a generic room
 111     * @param  {object} socket   The socket to subscribe.
 112     * @param  {string} roomName The room to subscribe to
 113     */
 114    sails.sockets.join = function(sockets, roomName) {
 115
 116      if (!sails.util.isArray(sockets)) {
 117        sockets = [sockets];
 118      }
 119
 120      sails.util.each(sockets, function(socket) {
 121        // If a string was sent, try to look up a socket with that ID
 122        if (typeof socket == 'string') {socket = sails.io.sockets.socket(socket);}
 123
 124        // If it's not a valid socket object, bail
 125        if (! (socket && socket.manager) ) return;
 126
 127        // Join up!
 128        socket.join(roomName);
 129
 130      });
 131
 132      return true;
 133
 134    };
 135
 136    /**
 137     * Unsubscribe a socket from a generic room
 138     * @param  {object} socket   The socket to unsubscribe.
 139     * @param  {string} roomName The room to unsubscribe from
 140     */
 141    sails.sockets.leave = function(sockets, roomName) {
 142
 143      if (!sails.util.isArray(sockets)) {
 144        sockets = [sockets];
 145      }
 146
 147      sails.util.each(sockets, function(socket) {
 148        // If a string was sent, try to look up a socket with that ID
 149        if (typeof socket == 'string') {socket = sails.io.sockets.socket(socket);}
 150
 151        // If it's not a valid socket object, bail
 152        if (! (socket && socket.manager) ) return;
 153
 154        // See ya!
 155        socket.leave(roomName);
 156      });
 157
 158      return true;
 159    };
 160
 161    /**
 162     * Broadcast a message to a room
 163     *
 164     * If the event name is omitted, "message" will be used by default.
 165     * Thus, sails.sockets.broadcast(roomName, data) is also a valid usage.
 166     *
 167     * @param  {string} roomName The room to broadcast a message to
 168     * @param  {string} eventName    The event name to broadcast
 169     * @param  {object} data     The data to broadcast
 170     * @param  {object} socket   Optional socket to omit
 171     */
 172
 173    sails.sockets.broadcast = function(roomName, eventName, data, socketToOmit) {
 174
 175      // If the 'eventName' is an object, assume the argument was omitted and
 176      // parse it as data instead.
 177      if (typeof eventName === 'object') {
 178        data = eventName;
 179        eventName = null;
 180      }
 181
 182      // Default to the sails.sockets.DEFAULT_EVENT_NAME.
 183      if (!eventName) {
 184        eventName = sails.sockets.DEFAULT_EVENT_NAME;
 185      }
 186
 187      // If we were given a valid socket to omit, broadcast from there.
 188      if (socketToOmit && socketToOmit.manager) {
 189        socketToOmit.broadcast.to(roomName).emit(eventName, data);
 190      }
 191      // Otherwise broadcast to everyone
 192      else {
 193        sails.io.sockets.in(roomName).emit(eventName, data);
 194      }
 195    };
 196
 197
 198
 199    /**
 200     * Broadcast a message to all connected sockets
 201     *
 202     * If the event name is omitted, sails.sockets.DEFAULT_EVENT_NAME will be used by default.
 203     * Thus, sails.sockets.blast(data) is also a valid usage.
 204     *
 205     * @param  {string} event    The event name to broadcast
 206     * @param  {object} data     The data to broadcast
 207     * @param  {object} socket   Optional socket to omit
 208     */
 209
 210    sails.sockets.blast = function(eventName, data, socketToOmit) {
 211
 212      // If the 'eventName' is an object, assume the argument was omitted and
 213      // parse it as data instead.
 214      if (typeof eventName === 'object') {
 215        data = eventName;
 216        eventName = null;
 217      }
 218
 219      // Default to the sails.sockets.DEFAULT_EVENT_NAME eventName.
 220      if (!eventName) {
 221        eventName = sails.sockets.DEFAULT_EVENT_NAME;
 222      }
 223
 224      // If we were given a valid socket to omit, broadcast from there.
 225      if (socketToOmit && socketToOmit.manager) {
 226        socketToOmit.broadcast.emit(eventName, data);
 227      }
 228
 229      // Otherwise broadcast to everyone
 230      else {
 231        sails.io.sockets.emit(eventName, data);
 232      }
 233    };
 234
 235
 236
 237
 238    /**
 239     * Get the ID of a socket object
 240     * @param  {object} socket The socket object to get the ID of
 241     * @return {string}        The socket's ID
 242     */
 243    sails.sockets.id = function(socket) {
 244      // If a request was passed in, get its socket
 245      socket = socket.socket || socket;
 246      if (socket) {
 247        return socket.id;
 248      }
 249      else return undefined;
 250    };
 251
 252    /**
 253     * Emit a message to one or more sockets by ID
 254     *
 255     * If the event name is omitted, "message" will be used by default.
 256     * Thus, sails.sockets.emit(socketIDs, data) is also a valid usage.
 257     *
 258     * @param  {array|string} socketIDs The ID or IDs of sockets to send a message to
 259     * @param  {string} event     The name of the message to send
 260     * @param  {object} data      Optional data to send with the message
 261     */
 262    sails.sockets.emit = function(socketIDs, eventName, data) {
 263      if (!_.isArray(socketIDs)) {
 264        socketIDs = [socketIDs];
 265      }
 266
 267      if (typeof eventName === 'object') {
 268        data = eventName;
 269        eventName = null;
 270      }
 271
 272      if (!eventName) {
 273        eventName = sails.sockets.DEFAULT_EVENT_NAME;
 274      }
 275
 276      _.each(socketIDs, function(socketID) {
 277        sails.io.sockets.socket(socketID).emit(eventName, data);
 278      });
 279    };
 280
 281    /**
 282     * Get the list of IDs of sockets subscribed to a room
 283     * @param  {string} roomName The room to get subscribers of
 284     * @return {array} An array of socket instances
 285     */
 286    sails.sockets.subscribers = function(roomName) {
 287      return _.pluck(sails.io.sockets.clients(roomName), 'id');
 288    };
 289
 290    /**
 291     * Get the list of rooms a socket is subscribed to
 292     * @param  {object} socket The socket to get rooms for
 293     * @return {array} An array of room names
 294     */
 295    sails.sockets.socketRooms = function(socket) {
 296      return _.map(_.keys(sails.io.sockets.manager.roomClients[socket.id]), function(roomName) {return roomName.replace(/^\//,'');});
 297    };
 298
 299    /**
 300     * Get the list of all rooms
 301     * @return {array} An array of room names, minus the empty room
 302     */
 303    sails.sockets.rooms = function() {
 304      var rooms = sails.util.clone(sails.io.sockets.manager.rooms);
 305      delete rooms[""];
 306      return sails.util.map(sails.util.keys(rooms), function(room){return room.substr(1);});
 307    };
 308
 309  }
 310
 311  /**
 312   * These methods get appended to the Model class objects
 313   * Some take req.socket as an argument to get access
 314   * to user('s|s') socket object(s)
 315   */
 316
 317  function getPubsubMethods () {
 318
 319    return {
 320
 321      /**
 322       * Broadcast a message to a room
 323       *
 324       * Wrapper for sails.sockets.broadcast
 325       * Can be overridden at a model level, i.e. for encapsulating messages within a single event name.
 326       *
 327       * @param  {string} roomName The room to broadcast a message to
 328       * @param  {string} eventName    The event name to broadcast
 329       * @param  {object} data     The data to broadcast
 330       * @param  {object} socket   Optional socket to omit
 331       *
 332       * @api private
 333       */
 334
 335      broadcast: function(roomName, eventName, data, socketToOmit) {
 336        sails.sockets.broadcast(roomName, eventName, data, socketToOmit);
 337      },
 338
 339
 340      /**
 341       * TODO: document
 342       */
 343      getAllContexts: function() {
 344
 345        var contexts = ['update', 'destroy', 'message'];
 346        _.each(this.associations, function(association) {
 347          if (association.type == 'collection') {
 348            contexts.push('add:'+association.alias);
 349            contexts.push('remove:'+association.alias);
 350          }
 351        });
 352        return contexts;
 353
 354      },
 355
 356      /**
 357       * Broadcast a custom message to sockets connected to the specified models
 358       * @param {Object|String|Finite} record -- record or ID of record whose subscribers should receive the message
 359       * @param {Object|Array|String|Finite} message -- the message payload
 360       * @param {Request|Socket} req - if specified, broadcast using this
 361       * socket (effectively omitting it)
 362       *
 363       */
 364
 365      message: function(record, data, req) {
 366
 367        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 368        var socketToOmit = (req && req.socket ? req.socket : req);
 369
 370        // If no records provided, throw an error
 371        if (!record) {
 372          return sails.log.error(
 373            util.format(
 374            'Must specify a record or record ID when calling `Model.publish` '+
 375            '(you specified: `%s`)', record));
 376        }
 377
 378        // Otherwise publish to each instance room
 379        else {
 380
 381          // Get the record ID (if the record argument isn't already a scalar)
 382          var id = record[this.primaryKey] || record;
 383          // Get the socket room to publish to
 384          var room = this.room(id, "message");
 385          // Create the payload
 386          var payload = {
 387            verb: "messaged",
 388            id: id,
 389            data: data
 390          };
 391
 392          this.broadcast( room, this.identity, payload, socketToOmit );
 393          sails.log.silly("Published message to ", room, ": ", payload);
 394
 395        }
 396
 397      },
 398
 399      /**
 400       * Broadcast a message to sockets connected to the specified models
 401       * (or null to broadcast to the entire class room)
 402       *
 403       * @param {Object|Array|String|Finite} models -- models whose subscribers should receive the message
 404       * @param {String} eventName -- the event name to broadcast with
 405       * @param {String} context -- the context to broadcast to
 406       * @param {Object|Array|String|Finite} data -- the message payload
 407       * socket (effectively omitting it)
 408       *
 409       * @api private
 410       */
 411
 412      publish: function (models, eventName, context, data, req) {
 413        var self = this;
 414
 415        // If the event name is an object, assume we're seeing `publish(models, data, req)`
 416        if (typeof eventName === 'object') {
 417          req = context;
 418          context = null;
 419          data = eventName;
 420          eventName = null;
 421        }
 422
 423        // Default to the event name being the model identity
 424        if (!eventName) {
 425          eventName = this.identity;
 426        }
 427
 428        // If the context is an object, assume we're seeing `publish(models, eventName, data, req)`
 429        if (typeof context === 'object' && context !== null) {
 430          req = data;
 431          data = context;
 432          context = null;
 433        }
 434
 435        // Default to using the message context
 436        if (!context) {
 437          sails.log.warn('`Model.publish` should specify a context; defaulting to "message".  Try `Model.message` instead?');
 438          context = 'message';
 439        }
 440
 441        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 442        var socketToOmit = (req && req.socket ? req.socket : req);
 443
 444        // If no models provided, publish to the class room
 445        if (!models) {
 446          STRINGFILE.logDeprecationNotice(
 447            'Model.publish(null, ...)',
 448              STRINGFILE.get('links.docs.sockets.pubsub'),
 449              sails.log.debug) &&
 450          STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
 451
 452          sails.log.silly('Published ', eventName, ' to ', self.classRoom());
 453          self.broadcast( self.classRoom(), eventName, data, socketToOmit );
 454          return;
 455        }
 456
 457        // Otherwise publish to each instance room
 458        else {
 459          models = this.pluralize(models);
 460          var ids = _.pluck(models, this.primaryKey);
 461          if ( ids.length === 0 ) {
 462            sails.log.warn('Can\'t publish a message to an empty list of instances-- ignoring...');
 463          }
 464          _.each(ids,function eachInstance (id) {
 465            var room = self.room(id, context);
 466            sails.log.silly("Published ", eventName, " to ", room);
 467            self.broadcast( room, eventName, data, socketToOmit );
 468
 469
 470            // Also broadcasts a message to the legacy instance room (derived by
 471            // using the `legacy_v0.9` context).
 472            // Uses traditional eventName === "message".
 473            // Uses traditional message format.
 474            if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients']) {
 475              var legacyRoom = self.room(id, 'legacy_v0.9');
 476              var legacyMsg = _.cloneDeep(data);
 477              legacyMsg.model = self.identity;
 478              if (legacyMsg.verb === 'created') { legacyMsg.verb = 'create'; }
 479              if (legacyMsg.verb === 'updated') { legacyMsg.verb = 'update'; }
 480              if (legacyMsg.verb === 'destroyed') { legacyMsg.verb = 'destroy'; }
 481              self.broadcast( legacyRoom, 'message', legacyMsg, socketToOmit );
 482            }
 483          });
 484        }
 485
 486      },
 487
 488      /**
 489       * Check that models are a list, if not, make them a list
 490       * Also if they are ids, make them dummy objects with an `id` property
 491       *
 492       * @param {Object|Array|String|Finite} models
 493       * @returns {Array} array of things that have an `id` property
 494       *
 495       * @api private
 496       * @synchronous
 497       */
 498      pluralize: function (models) {
 499
 500        // If `models` is a non-array object,
 501        // turn it into a single-item array ("pluralize" it)
 502        // e.g. { id: 7 } -----> [ { id: 7 } ]
 503        if ( !_.isArray(models) ) {
 504          var model = models;
 505          models = [model];
 506        }
 507
 508        // If a list of ids things look ids (finite numbers or strings),
 509        // wrap them up as dummy objects; e.g. [1,2] ---> [ {id: 1}, {id: 2} ]
 510        var self = this;
 511        return _.map(models, function (model) {
 512          if ( _.isString(model) || _.isFinite(model) ) {
 513            var id = model;
 514            var data = {};
 515            data[self.primaryKey] = id;
 516            return data;
 517          }
 518          return model;
 519        });
 520      },
 521
 522      /**
 523       * @param  {String|} id
 524       * @return {String}    name of the instance room for an instance of this model w/ given id
 525       * @synchronous
 526       */
 527      room: function (id, context) {
 528        if (!id) return sails.log.error('Must specify an `id` when calling `Model.room(id)`');
 529        return 'sails_model_'+this.identity+'_'+id+':'+context;
 530      },
 531
 532      classRoom: function () {
 533        STRINGFILE.logDeprecationNotice(
 534          'Model.classRoom',
 535            STRINGFILE.get('links.docs.sockets.pubsub'),
 536            sails.log.debug) &&
 537        STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
 538
 539        return this._classRoom();
 540      },
 541
 542      /**
 543       * @return {String} name of this model's global class room
 544       * @synchronous
 545       * @api private
 546       */
 547      _classRoom: function() {
 548        return 'sails_model_create_'+this.identity;
 549      },
 550
 551      /**
 552       * Return the set of sockets subscribed to this instance
 553       * @param  {String|Integer} id
 554       * @return {Array[String]}
 555       * @synchronous
 556       * @api private
 557       */
 558      subscribers: function (id, context) {
 559        // For a single context, return just the socket subscribed to that context
 560        if (context) {
 561          return sails.sockets.subscribers(this.room(id, context));
 562        }
 563        // Otherwise return the unique set of sockets subscribed to ALL contexts
 564        //
 565        // TODO: handle custom contexts here, which aren't returned by getAllContexts
 566        // Not currently a big issue since `publish` is a private API, so subscribing
 567        // to a custom context doesn't let you do much.
 568        var contexts = this.getAllContexts();
 569        var subscribers = [];
 570        _.each(contexts, function(context) {
 571          subscribers = _.union(subscribers, this.subscribers(id, context));
 572        }, this);
 573        return _.uniq(subscribers);
 574      },
 575
 576      /**
 577       * Return the set of sockets subscribed to this class room
 578       * @return {Array[String]}
 579       * @synchronous
 580       * @api private
 581       */
 582      watchers: function() {
 583        return sails.sockets.subscribers(this._classRoom());
 584      },
 585
 586      /**
 587      * Subscribe a socket to a handful of records in this model
 588      *
 589      * Usage:
 590      * Model.subscribe(req,socket [, records] )
 591      *
 592      * @param {Request|Socket} req - request containing the socket to subscribe, or the socket itself
 593      * @param {Object|Array|String|Finite} records - id, array of ids, model, or array of records
 594      *
 595      * e.g.
 596      *   // Subscribe to User.create()
 597      *   User.subscribe(req.socket)
 598      *
 599      *   // Subscribe to User.update() and User.destroy()
 600      *   // for the specified instances (or user.save() / user.destroy())
 601      *   User.subscribe(req.socket, users)
 602      *
 603      * @api public
 604      */
 605
 606      subscribe: function (req, records, contexts) {
 607        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 608        var socket = req.socket ? req.socket : req;
 609
 610        // todo: consider using the following instead (to lessen potential confusion w/ http requests)
 611        // (however it would need to be applied to all occurances of `req` in methods in this hook)
 612        // if (!socket.manager || !(req.socket && req.socket.manager)) {
 613        //   console.log('-----',req,'\n\n---------\n\n');
 614        //   sails.log.verbose('`Model.subscribe()` called by a non-socket request. Only requests originating from a connected socket may be subscribed. Ignoring...');
 615        //   return;
 616        // }
 617
 618        var self = this;
 619
 620        // Subscribe to class room to hear about new records
 621        if (!records) {
 622          STRINGFILE.logDeprecationNotice(
 623            'Model.subscribe(socket, null, ...)',
 624              STRINGFILE.get('links.docs.sockets.pubsub'),
 625              sails.log.debug) &&
 626          STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
 627
 628          this.watch(req);
 629          return;
 630        }
 631
 632        contexts = contexts || this.autosubscribe;
 633
 634        if (!contexts) {
 635          sails.log.warn("`subscribe` called without context on a model with autosubscribe:false.  No action will be taken.");
 636          return;
 637        }
 638
 639        if (contexts === true || contexts == '*') {
 640          contexts = this.getAllContexts();
 641        } else if (sails.util.isString(contexts)) {
 642          contexts = [contexts];
 643        }
 644
 645
 646        // If the subscribing socket is using the legacy (v0.9.x) socket SDK (sails.io.js),
 647        // always subscribe the client to the `legacy_v0.9` context.
 648        if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
 649          var sdk = getSDKMetadata(socket.handshake);
 650          var isLegacySocketClient = sdk.version === '0.9.0';
 651          if (isLegacySocketClient) {
 652            contexts.push('legacy_v0.9');
 653          }
 654        }
 655
 656        // Subscribe to model instances
 657        records = self.pluralize(records);
 658        var ids = _.pluck(records, this.primaryKey);
 659
 660        _.each(ids,function (id) {
 661          _.each(contexts, function(context) {
 662            sails.log.silly(
 663              'Subscribed to the ' +
 664              self.globalId + ' with id=' + id + '\t(room :: ' + self.room(id, context) + ')'
 665            );
 666
 667            sails.sockets.join( socket, self.room(id, context) );
 668          });
 669        });
 670      },
 671
 672      /**
 673       * Unsubscribe a socket from some records
 674       *
 675       * @param {Request|Socket} req - request containing the socket to unsubscribe, or the socket itself
 676       * @param {Object|Array|String|Finite} models - id, array of ids, model, or array of models
 677       */
 678
 679      unsubscribe: function (req, records, contexts) {
 680
 681        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 682        var socket = req.socket ? req.socket : req;
 683
 684        var self = this;
 685
 686        // If no records provided, unsubscribe from the class room
 687        if (!records) {
 688          STRINGFILE.logDeprecationNotice(
 689            'Model.unsubscribe(socket, null, ...)',
 690              STRINGFILE.get('links.docs.sockets.pubsub'),
 691              sails.log.debug) &&
 692          STRINGFILE.logUpgradeNotice(STRINGFILE.get('upgrade.classrooms'), [], sails.log.debug);
 693
 694          this.unwatch();
 695
 696        }
 697
 698        contexts = contexts || this.getAllContexts();
 699
 700        if (contexts === true) {
 701          contexts = this.getAllContexts();
 702        }
 703
 704        if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
 705          var sdk = getSDKMetadata(socket.handshake);
 706          var isLegacySocketClient = sdk.version === '0.9.0';
 707          if (isLegacySocketClient) {
 708            contexts.push('legacy_v0.9');
 709          }
 710        }
 711
 712        records = self.pluralize(records);
 713        var ids = _.pluck(records, this.primaryKey);
 714        _.each(ids,function (id) {
 715          _.each(contexts, function(context) {
 716            sails.log.silly(
 717              'Unsubscribed from the ' +
 718              self.globalId + ' with id=' + id + '\t(room :: ' + self.room(id, context) + ')'
 719            );
 720            sails.sockets.leave( socket, self.room(id, context));
 721          });
 722        });
 723      },
 724
 725      /**
 726       * Publish an update on a particular model
 727       *
 728       * @param {String|Finite} id
 729       *    - primary key of the instance we're referring to
 730       *
 731       * @param {Object} changes
 732       *    - an object of changes to this instance that will be broadcasted
 733       *
 734       * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
 735       *
 736       * @api public
 737       */
 738
 739      publishUpdate: function (id, changes, req, options) {
 740
 741        // Make sure there's an options object
 742        options = options || {};
 743
 744        // Ensure that we're working with a clean, unencumbered object
 745        changes = _.clone(changes);
 746
 747        // Enforce valid usage
 748        var validId = _.isString(id) || _.isFinite(id);
 749        if ( !validId  ) {
 750          return sails.log.error(
 751            'Invalid usage of ' +
 752            '`' + this.identity + '.publishUpdate(id, changes, [socketToOmit])`'
 753          );
 754        }
 755
 756        if (sails.util.isFunction(this.beforePublishUpdate)) {
 757          this.beforePublishUpdate(id, changes, req, options);
 758        }
 759
 760        var data = {
 761          model: this.identity,
 762          verb: 'update',
 763          data: changes,
 764          id: id
 765        };
 766
 767        if (options.previous && !options.noReverse) {
 768
 769          var previous = options.previous;
 770
 771          // If any of the changes were to association attributes, publish add or remove messages.
 772          _.each(changes, function(val, key) {
 773
 774            // If value wasn't changed, do nothing
 775            if (val == previous[key]) return;
 776
 777            // Find an association matching this attribute
 778            var association = _.find(this.associations, {alias: key});
 779
 780            // If the attribute isn't an assoctiation, return
 781            if (!association) return;
 782
 783            // Get the associated model class
 784            var ReferencedModel = sails.models[association.type == 'model' ? association.model : association.collection];
 785
 786            // Bail if this attribute isn't in the model's schema
 787            if (association.type == 'model') {
 788
 789              var previousPK = _.isObject(previous[key]) ? previous[key][ReferencedModel.primaryKey] : previous[key];
 790              var newPK = _.isObject(val) ? val[this.primaryKey] : val;
 791              if (previousPK == newPK) return;
 792
 793              // Get the inverse association definition, if any
 794              reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key});
 795
 796                if (!reverseAssociation) {return;}
 797
 798                // If this is a to-many association, do publishAdd or publishRemove as necessary
 799                // on the other side
 800                if (reverseAssociation.type == 'collection') {
 801                  // If there was a previous value, alert the previously associated model
 802                  if (previous[key]) {
 803                    ReferencedModel.publishRemove(previousPK, reverseAssociation.alias, id, {noReverse:true});
 804                  }
 805                  // If there's a new value (i.e. it's not null), alert the newly associated model
 806                  if (val) {
 807                    ReferencedModel.publishAdd(newPK, reverseAssociation.alias, id, {noReverse:true});
 808                  }
 809                }
 810                // Otherwise do a publishUpdate
 811                else {
 812
 813                  var pubData = {};
 814
 815                  // If there was a previous association, notify it that it has been nullified
 816                  if (previous[key]) {
 817                    pubData[reverseAssociation.alias] = null;
 818                    ReferencedModel.publishUpdate(previousPK, pubData, req, {noReverse:true});
 819                  }
 820                  // If there's a new association, notify it that it has been linked
 821                  if (val) {
 822                    pubData[reverseAssociation.alias] = id;
 823                    ReferencedModel.publishUpdate(newPK, pubData, req, {noReverse:true});
 824                  }
 825
 826                }
 827
 828            }
 829
 830            else {
 831
 832              // Get the reverse association definition, if any
 833              reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via});
 834
 835              if (!reverseAssociation) {return;}
 836
 837                // If we can't get the previous PKs (b/c previous isn't populated), bail
 838                if (typeof(previous[key]) == 'undefined') return;
 839
 840                // Get the previous set of IDs
 841                var previousPKs = _.pluck(previous[key], ReferencedModel.primaryKey);
 842                // Get the current set of IDs
 843                var updatedPKs = _.map(val, function(_val) {
 844                  if (_.isObject(_val)) {
 845                    return _val[ReferencedModel.primaryKey];
 846                  } else {
 847                    return _val;
 848                  }
 849                });
 850                // Find any values that were added to the collection
 851                var addedPKs = _.difference(updatedPKs, previousPKs);
 852                // Find any values that were removed from the collection
 853                var removedPKs = _.difference(previousPKs, updatedPKs);
 854
 855                // If this is a to-many association, do publishAdd or publishRemove as necessary
 856                // on the other side
 857                if (reverseAssociation.type == 'collection') {
 858
 859                  // Alert any removed models
 860                  _.each(removedPKs, function(pk) {
 861                    ReferencedModel.publishRemove(pk, reverseAssociation.alias, id, {noReverse:true});
 862                  });
 863                  // Alert any added models
 864                  _.each(addedPKs, function(pk) {
 865                    ReferencedModel.publishAdd(pk, reverseAssociation.alias, id, {noReverse:true});
 866                  });
 867
 868                }
 869
 870                // Otherwise do a publishUpdate
 871                else {
 872
 873                  // Alert any removed models
 874                  _.each(removedPKs, function(pk) {
 875                    var pubData = {};
 876                    pubData[reverseAssociation.alias] = null;
 877                    ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
 878                  });
 879                  // Alert any added models
 880                  _.each(addedPKs, function(pk) {
 881                    var pubData = {};
 882                    pubData[reverseAssociation.alias] = id;
 883                    ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
 884                  });
 885
 886                }
 887
 888            }
 889          }, this);
 890        }
 891
 892        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 893        var socketToOmit = (req && req.socket ? req.socket : req);
 894
 895        // In development environment, blast out a message to everyone
 896        if (sails.config.environment == 'development') {
 897          sails.sockets.publishToFirehose(data);
 898        }
 899
 900        data.verb = 'updated';
 901        data.previous = options.previous;
 902        delete data.model;
 903
 904        // Broadcast to the model instance room
 905        this.publish(id, this.identity, 'update', data, socketToOmit);
 906
 907        if (sails.util.isFunction(this.afterPublishUpdate)) {
 908          this.afterPublishUpdate(id, changes, req, options);
 909        }
 910
 911
 912      },
 913
 914      /**
 915       * Publish the destruction of a particular model
 916       *
 917       * @param {String|Finite} id
 918       *    - primary key of the instance we're referring to
 919       *
 920       * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
 921       *
 922       */
 923
 924      publishDestroy: function (id, req, options) {
 925
 926        options = options || {};
 927
 928        // Enforce valid usage
 929        var invalidId = !id || _.isObject(id);
 930        if ( invalidId ) {
 931          return sails.log.error(
 932            'Invalid usage of ' + this.identity +
 933            '`publishDestroy(id, [socketToOmit])`'
 934          );
 935        }
 936
 937        if (sails.util.isFunction(this.beforePublishDestroy)) {
 938          this.beforePublishDestroy(id, req, options);
 939        }
 940
 941
 942        var data = {
 943          model: this.identity,
 944          verb: 'destroy',
 945          id: id,
 946          previous: options.previous
 947        };
 948
 949        // If a request object was sent, get its socket, otherwise assume a socket was sent.
 950        var socketToOmit = (req && req.socket ? req.socket : req);
 951
 952        // In development environment, blast out a message to everyone
 953        if (sails.config.environment == 'development') {
 954          sails.sockets.publishToFirehose(data);
 955        }
 956
 957        data.verb = 'destroyed';
 958        delete data.model;
 959
 960        // Broadcast to the model instance room
 961        this.publish(id, this.identity, 'destroy', data, socketToOmit);
 962
 963        // Unsubscribe everyone from the model instance
 964        this.retire(id);
 965
 966        if (options.previous) {
 967
 968          var previous = options.previous;
 969
 970          // Loop through associations and alert as necessary
 971          _.each(this.associations, function(association) {
 972
 973            var ReferencedModel;
 974
 975            // If it's a to-one association, and it wasn't falsy, alert
 976            // the reverse side
 977            if (association.type == 'model' && [association.alias] && previous[association.alias]) {
 978              ReferencedModel = sails.models[association.model];
 979              // Get the inverse association definition, if any
 980              reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity});
 981
 982              if (reverseAssociation) {
 983                // If it's a to-one, publish a simple update alert
 984                var referencedModelId = _.isObject(previous[association.alias]) ? previous[association.alias][ReferencedModel.primaryKey] : previous[association.alias];
 985                if (reverseAssociation.type == 'model') {
 986                  var pubData = {};
 987                  pubData[reverseAssociation.alias] = null;
 988                  ReferencedModel.publishUpdate(referencedModelId, pubData, {noReverse:true});
 989                }
 990                // If it's a to-many, publish a "removed" alert
 991                else {
 992                  ReferencedModel.publishRemove(referencedModelId, reverseAssociation.alias, id, req, {noReverse:true});
 993                }
 994              }
 995            }
 996
 997            else if (association.type == 'collection' && previous[association.alias].length) {
 998              ReferencedModel = sails.models[association.collection];
 999              // Get the inverse association definition, if any
1000              reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity}) || _.find(ReferencedModel.associations, {model: this.identity});
1001
1002              if (reverseAssociation) {
1003                _.each(previous[association.alias], function(associatedModel) {
1004                  // If it's a to-one, publish a simple update alert
1005                  if (reverseAssociation.type == 'model') {
1006                    var pubData = {};
1007                    pubData[reverseAssociation.alias] = null;
1008                    ReferencedModel.publishUpdate(associatedModel[ReferencedModel.primaryKey], pubData, req, {noReverse:true});
1009                  }
1010                  // If it's a to-many, publish a "removed" alert
1011                  else {
1012                    ReferencedModel.publishRemove(associatedModel[ReferencedModel.primaryKey], reverseAssociation.alias, id, req, {noReverse:true});
1013                  }
1014                });
1015              }
1016            }
1017
1018          }, this);
1019
1020        }
1021
1022        if (sails.util.isFunction(this.afterPublishDestroy)) {
1023          this.afterPublishDestroy(id, req, options);
1024        }
1025
1026      },
1027
1028
1029      /**
1030       * publishAdd
1031       *
1032       * @param  {[type]} id           [description]
1033       * @param  {[type]} alias        [description]
1034       * @param  {[type]} idAdded      [description]
1035       * @param  {[type]} socketToOmit [description]
1036       */
1037
1038      publishAdd: function(id, alias, idAdded, req, options) {
1039
1040        // Make sure there's an options object
1041        options = options || {};
1042
1043        // Enforce valid usage
1044        var invalidId = !id || _.isObject(id);
1045        var invalidAlias = !alias || !_.isString(alias);
1046        var invalidAddedId = !idAdded || _.isObject(idAdded);
1047        if ( invalidId || invalidAlias || invalidAddedId ) {
1048          return sails.log.error(
1049            'Invalid usage of ' + this.identity +
1050            '`publishAdd(id, alias, idAdded, [socketToOmit])`'
1051          );
1052        }
1053
1054        if (sails.util.isFunction(this.beforePublishAdd)) {
1055          this.beforePublishAdd(id, alias, idAdded, req);
1056        }
1057
1058        // If a request object was sent, get its socket, otherwise assume a socket was sent.
1059        var socketToOmit = (req && req.socket ? req.socket : req);
1060
1061
1062        // In development environment, blast out a message to everyone
1063        if (sails.config.environment == 'development') {
1064          sails.sockets.publishToFirehose({
1065            id: id,
1066            model: this.identity,
1067            verb: 'addedTo',
1068            attribute: alias,
1069            addedId: idAdded
1070          });
1071        }
1072
1073        this.publish(id, this.identity, 'add:'+alias, {
1074          id: id,
1075          verb: 'addedTo',
1076          attribute: alias,
1077          addedId: idAdded
1078        }, socketToOmit);
1079
1080        if (!options.noReverse) {
1081
1082          // Get the reverse association
1083          var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection];
1084
1085          var data;
1086
1087          // Subscribe to the model you're adding
1088          if (req) {
1089            data = {};
1090            data[reverseModel.primaryKey] = idAdded;
1091            reverseModel.subscribe(req, data);
1092          }
1093
1094          // Find the reverse association, if any
1095          var reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via}) ;
1096          if (reverseAssociation) {
1097            // If this is a many-to-many association, do a publishAdd for the
1098            // other side.
1099            if (reverseAssociation.type == 'collection') {
1100              reverseModel.publishAdd(idAdded, reverseAssociation.alias, id, req, {noReverse:true});
1101            }
1102
1103            // Otherwise, do a publishUpdate
1104            else {
1105              data = {};
1106              data[reverseAssociation.alias] = id;
1107              reverseModel.publishUpdate(idAdded, data, req, {noReverse:true});
1108            }
1109          }
1110
1111        }
1112
1113
1114        if (sails.util.isFunction(this.afterPublishAdd)) {
1115          this.afterPublishAdd(id, alias, idAdded, req);
1116        }
1117
1118      },
1119
1120
1121      /**
1122       * publishRemove
1123       *
1124       * @param  {[type]} id           [description]
1125       * @param  {[type]} alias        [description]
1126       * @param  {[type]} idRemoved    [description]
1127       * @param  {[type]} socketToOmit [description]
1128       */
1129
1130      publishRemove: function(id, alias, idRemoved, req, options) {
1131
1132        // Make sure there's an options object
1133        options = options || {};
1134
1135        // Enforce valid usage
1136        var invalidId = !id || _.isObject(id);
1137        var invalidAlias = !alias || !_.isString(alias);
1138        var invalidRemovedId = !idRemoved || _.isObject(idRemoved);
1139        if ( invalidId || invalidAlias || invalidRemovedId ) {
1140          return sails.log.error(
1141            'Invalid usage of ' + this.identity +
1142            '`publishRemove(id, alias, idRemoved, [socketToOmit])`'
1143          );
1144        }
1145        if (sails.util.isFunction(this.beforePublishRemove)) {
1146          this.beforePublishRemove(id, alias, idRemoved, req);
1147        }
1148
1149        // If a request object was sent, get its socket, otherwise assume a socket was sent.
1150        var socketToOmit = (req && req.socket ? req.socket : req);
1151
1152        // In development environment, blast out a message to everyone
1153        if (sails.config.environment == 'development') {
1154          sails.sockets.publishToFirehose({
1155            id: id,
1156            model: this.identity,
1157            verb: 'removedFrom',
1158            attribute: alias,
1159            removedId: idRemoved
1160          });
1161        }
1162
1163        this.publish(id, this.identity, 'remove:' + alias, {
1164          id: id,
1165          verb: 'removedFrom',
1166          attribute: alias,
1167          removedId: idRemoved
1168        }, socketToOmit);
1169
1170
1171        if (!options.noReverse) {
1172
1173          // Get the reverse association, if any
1174          var reverseModel = sails.models[_.find(this.associations, {alias: alias}).collection];
1175          var reverseAssociation = _.find(reverseModel.associations, {alias: _.find(this.associations, {alias: alias}).via});
1176
1177          if (reverseAssociation) {
1178            // If this is a many-to-many association, do a publishAdd for the
1179            // other side.
1180            if (reverseAssociation.type == 'collection') {
1181              reverseModel.publishRemove(idRemoved, reverseAssociation.alias, id, req, {noReverse:true});
1182            }
1183
1184            // Otherwise, do a publishUpdate
1185            else {
1186              var data = {};
1187              data[reverseAssociation.alias] = null;
1188              reverseModel.publishUpdate(idRemoved, data, req, {noReverse:true});
1189            }
1190          }
1191
1192        }
1193
1194        if (sails.util.isFunction(this.afterPublishRemove)) {
1195          this.afterPublishRemove(id, alias, idRemoved, req);
1196        }
1197
1198      },
1199
1200
1201      /**
1202       * Publish the creation of a model
1203       *
1204       * @param {Object} values
1205       *                - the data to publish
1206       *
1207       * @param {Request|Socket} req - if specified, broadcast using this socket (effectively omitting it)
1208       * @api private
1209       */
1210
1211      publishCreate: function(values, req, options) {
1212        var self = this;
1213
1214        options = options || {};
1215
1216        if (!values[this.primaryKey]) {
1217          return sails.log.error(
1218            'Invalid usage of publishCreate() :: ' +
1219            'Values must have an `'+this.primaryKey+'`, instead got ::\n' +
1220            util.inspect(values)
1221          );
1222        }
1223
1224        if (sails.util.isFunction(this.beforePublishCreate)) {
1225          this.beforePublishCreate(values, req);
1226        }
1227
1228        var id = values[this.primaryKey];
1229
1230        // If any of the added values were association attributes, publish add or remove messages.
1231        _.each(values, function(val, key) {
1232
1233          // If the user hasn't yet given this association a value, bail out
1234          if (val === null) {
1235            return;
1236          }
1237
1238          var association = _.find(this.associations, {alias: key});
1239
1240          // If the attribute isn't an assoctiation, return
1241          if (!association) return;
1242
1243          // Get the associated model class
1244          var ReferencedModel = sails.models[association.type == 'model' ? association.model : association.collection];
1245
1246          // Bail if the model doesn't exist
1247          if (!ReferencedModel) return;
1248
1249          // Bail if this attribute isn't in the model's schema
1250          if (association.type == 'model') {
1251
1252            // Get the inverse association definition, if any
1253            reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, via: key});
1254
1255              if (!reverseAssociation) {return;}
1256
1257              // If this is a to-many association, do publishAdd on the other side
1258              // TODO -- support nested creates.  For now, we can't tell if an object value here represents
1259              // a NEW object or an existing one, so we'll ignore it.
1260              if (reverseAssociation.type == 'collection' && !_.isObject(val)) {
1261                  ReferencedModel.publishAdd(val, reverseAssociation.alias, id, {noReverse:true});
1262              }
1263              // Otherwise do a publishUpdate
1264              // TODO -- support nested creates.  For now, we can't tell if an object value here represents
1265              // a NEW object or an existing one, so we'll ignore it.
1266              else {
1267
1268                var pubData = {};
1269
1270                if (!_.isObject(val)) {
1271                  pubData[reverseAssociation.alias] = id;
1272                  ReferencedModel.publishUpdate(val, pubData, req, {noReverse:true});
1273                }
1274
1275              }
1276
1277          }
1278
1279          else {
1280
1281            // Get the inverse association definition, if any
1282            reverseAssociation = _.find(ReferencedModel.associations, {collection: this.identity, via: key}) || _.find(ReferencedModel.associations, {model: this.identity, alias: association.via});
1283
1284              if (!reverseAssociation) {return;}
1285
1286              // If this is a to-many association, do publishAdds on the other side
1287              if (reverseAssociation.type == 'collection') {
1288
1289                // Alert any added models
1290                _.each(val, function(pk) {
1291                  // TODO -- support nested creates.  For now, we can't tell if an object value here represents
1292                  // a NEW object or an existing one, so we'll ignore it.
1293                  if (_.isObject(pk)) return;
1294                  ReferencedModel.publishAdd(pk, reverseAssociation.alias, id, {noReverse:true});
1295                });
1296
1297              }
1298
1299              // Otherwise do a publishUpdate
1300              else {
1301
1302                // Alert any added models
1303                _.each(val, function(pk) {
1304                  // TODO -- support nested creates.  For now, we can't tell if an object value here represents
1305                  // a NEW object or an existing one, so we'll ignore it.
1306                  if (_.isObject(pk)) return;
1307                  var pubData = {};
1308                  pubData[reverseAssociation.alias] = id;
1309                  ReferencedModel.publishUpdate(pk, pubData, req, {noReverse:true});
1310                });
1311
1312              }
1313
1314            }
1315
1316        }, this);
1317
1318        // Ensure that we're working with a plain object
1319        values = _.clone(values);
1320
1321        // If a request object was sent, get its socket, otherwise assume a socket was sent.
1322        var socketToOmit = (req && req.socket ? req.socket : req);
1323
1324        // Blast success message
1325        sails.sockets.publishToFirehose({
1326
1327          model: this.identity,
1328          verb: 'create',
1329          data: values,
1330          id: values[this.primaryKey]
1331
1332        });
1333
1334        // Publish to classroom
1335        var eventName = this.identity;
1336        this.broadcast(this._classRoom(), eventName, {
1337          verb: 'created',
1338          data: values,
1339          id: values[this.primaryKey]
1340        }, socketToOmit);
1341
1342        // Also broadcasts a message to the legacy class room (derived by
1343        // using the `:legacy_v0.9` trailer on the class room name).
1344        // Uses traditional eventName === "message".
1345        // Uses traditional message format.
1346        if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients']) {
1347          var legacyData = _.cloneDeep({
1348            verb: 'create',
1349            data: values,
1350            model: self.identity,
1351            id: values[this.primaryKey]
1352          });
1353          var legacyRoom = this._classRoom()+':legacy_v0.9';
1354          self.broadcast( legacyRoom, 'message', legacyData, socketToOmit );
1355        }
1356
1357        // Subscribe watchers to the new instance
1358        if (!options.noIntroduce) {
1359          this.introduce(values[this.primaryKey]);
1360        }
1361
1362        if (sails.util.isFunction(this.afterPublishCreate)) {
1363          this.afterPublishCreate(values, req);
1364        }
1365
1366      },
1367
1368
1369      /**
1370       *
1371       * @return {[type]} [description]
1372       */
1373      watch: function ( req ) {
1374
1375        var socket = req.socket ? req.socket : req;
1376
1377        // todo: consider using the following instead (to lessen potential confusion w/ http requests)
1378        // (however it would need to be applied to all occurances of `req` in methods in this hook)
1379        // if (!socket.handshake) {
1380        //   sails.log.verbose('`Model.watch()` called by a non-socket request. Only requests originating from a connected socket may be subscribed to a Model class. Ignoring...');
1381        //   return;
1382        // }
1383
1384        sails.sockets.join(socket, this._classRoom());
1385        sails.log.silly("Subscribed socket ", sails.sockets.id(socket), "to", this._classRoom());
1386
1387        if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
1388          var sdk = getSDKMetadata(socket.handshake);
1389          var isLegacySocketClient = sdk.version === '0.9.0';
1390          if (isLegacySocketClient) {
1391            sails.sockets.join(socket, this._classRoom()+':legacy_v0.9');
1392          }
1393        }
1394
1395      },
1396
1397      /**
1398       * [unwatch description]
1399       * @param  {[type]} socket [description]
1400       * @return {[type]}        [description]
1401       */
1402      unwatch: function ( req ) {
1403
1404        var socket = req.socket ? req.socket : req;
1405        sails.sockets.leave(socket, this._classRoom());
1406        sails.log.silly("Unubscribed socket ", sails.sockets.id(socket), "from", this._classRoom());
1407
1408        if (sails.config.sockets['backwardsCompatibilityFor0.9SocketClients'] && socket.handshake) {
1409          var sdk = getSDKMetadata(socket.handshake);
1410          var isLegacySocketClient = sdk.version === '0.9.0';
1411          if (isLegacySocketClient) {
1412            sails.sockets.leave(socket, this._classRoom()+':legacy_v0.9');
1413          }
1414        }
1415      },
1416
1417
1418      /**
1419       * Introduce a new instance
1420       *
1421       * Take all of the subscribers to the class room and 'introduce' them
1422       * to a new instance room
1423       *
1424       * @param {String|Finite} id
1425       *    - primary key of the instance we're referring to
1426       *
1427       * @api private
1428       */
1429
1430      introduce: function(model) {
1431
1432        var id = model[this.primaryKey] || model;
1433
1434        _.each(this.watchers(), function(socketId) {
1435          this.subscribe(socketId, id);
1436        }, this);
1437
1438      },
1439
1440      /**
1441       * Bid farewell to a destroyed instance
1442       * Take all of the socket subscribers in this instance room
1443       * and unsubscribe them from it
1444       */
1445      retire: function(model) {
1446
1447        var id = model[this.primaryKey] || model;
1448
1449        _.each(this.subscribers(id), function(socket) {
1450          this.unsubscribe(socket, id);
1451      

Large files files are truncated, but you can click here to view the full file