1 /**
  2  * @fileOverview Session Resource class definition
  3  */
  4 
  5 var Resource = require('./resource')
  6   , Account = require('./account')
  7   , Channel = require('./channel')
  8   , Subscription = require('./subscription')
  9   , Application = require('./application')
 10   , Member = require('./member')
 11   , _ = require('underscore')
 12   , async = require('async')
 13   ;
 14 
 15 /**
 16  * Represents a session in the spire api.
 17  *
 18  * <p>Sessions contain other resources, like channels and subscriptions.  These
 19  * can be accessed in the <code>session.resources</code> object.</p>
 20  *
 21  * <p>One important resource inside <code>session</code> is the account resource
 22  * <code>session.account</code>, which is only given to sessions authenticated
 23  * with an email and password.  The account resource is documented in its own
 24  * class.</p>
 25  *
 26  * <p>Session objects maintain lists of channels and subscriptions.  If you call
 27  * the <code>session.channels</code> or <code>session.subscriptions</code>
 28  * methods, you will get back cached data if it exists.  Use the <code>$</code>
 29  * cache-bypass methods: <code>session.channels$</code> and
 30  * <code>session.subscriptions$</code> to get fresh data from the api.</p>
 31  *
 32  * @class Session Resource
 33  *
 34  * @constructor
 35  * @extends Resource
 36  * @param {object} spire Spire object
 37  * @param {object} data Session data from the spire api
 38  */
 39 function Session(spire, data) {
 40   /**
 41    * Reference to spire object.
 42    */
 43   this.spire = spire;
 44 
 45   /**
 46    * Actual data from the spire.io api.
 47    */
 48   this.data = data;
 49 
 50   this.resourceName = 'session';
 51 
 52   this._channels = {};
 53   this._subscriptions = {};
 54   this._applications = {};
 55 	this._storeResources();
 56 
 57 }
 58 
 59 Session.prototype = new Resource();
 60 
 61 module.exports = Session;
 62 
 63 /**
 64  * <p>Gets the Session resource.
 65  *
 66  * @param {function (err, session)} cb Callback
 67  */
 68 Session.prototype.get = function (cb) {
 69   var session = this;
 70   this.request('get', function (err, data) {
 71     if (err) return cb(err);
 72     session.data = data;
 73     session._storeResources();
 74     cb(null, session);
 75   });
 76 };
 77 
 78 /**
 79  * Gets the account resource.  Only available to sessions that are authenticated
 80  * with an email and password.
 81  *
 82  * Returns a value from the cache, if one if available.
 83  *
 84  * @example
 85  * session.account(function (err, account) {
 86  *   if (!err) {
 87  *     // `account` is account resource.
 88  *   }
 89  * });
 90  *
 91  * @param {function (err, account)} cb Callback
 92  */
 93 Session.prototype.account = function (cb) {
 94   if (this._account) return cb(null, this._account);
 95   this.account$(cb);
 96 };
 97 
 98 /**
 99  * Gets the account resource.  Only available to sessions that are authenticated
100  * with an email and password.
101  *
102  * Always gets a fresh value from the api.
103  *
104  * @example
105  * session.account$(function (err, account) {
106  *   if (!err) {
107  *     // `account` is account resource.
108  *   }
109  * });
110  * @param {function (err, account)} cb Callback
111  */
112 Session.prototype.account$ = function (cb) {
113   var session = this;
114   this.request('account', function (err, account) {
115     if (err) return cb(err);
116     session._account = new Account(session.spire, account);
117     cb(null, session._account);
118   });
119 };
120 
121 /**
122  * Resets the account resource.  Only available to sessions that are authenticated
123  * with an email and password.
124  * *
125  * @example
126  * session.resetAccount(function (err, session) {
127  *   if (!err) {
128  *     // `session` is session with new account resource.
129  *   }
130  * });
131  * @param {function (err, session)} cb Callback
132  */
133 Session.prototype.resetAccount = function (cb) {
134   var session = this;
135   this._account.reset(function (err, sessionData) {
136     if (err) return cb(err);
137 		session.data = sessionData;
138 		session._storeResources();
139     cb(null, session);
140   });
141 };
142 
143 /**
144  * Gets the applications collection.  Returns a hash of Application resources.
145  *
146  * Returns a value from the cache, if one if available.
147  *
148  * @example
149  * session.applications(function (err, applications) {
150  *   if (!err) {
151  *     // `applications` is a hash of all the account's applications
152  *   }
153  * });
154  *
155  * @param {function (err, applications)} cb Callback
156  */
157 Session.prototype.applications = function (cb) {
158   if (!_.isEmpty(this._applications)) return cb(null, this._applications);
159   this.applications$(cb);
160 };
161 
162 /**
163  * Gets the applications collection.  Returns a hash of Application resources.
164  *
165  * Always gets a fresh value from the api.
166  *
167  * @example
168  * session.applications$(function (err, applications) {
169  *   if (!err) {
170  *     // `applications` is a hash of all the account's applications
171  *   }
172  * });
173  *
174  * @param {function (err, applications)} cb Callback
175  */
176 Session.prototype.applications$ = function (cb) {
177   var session = this;
178   this.request('applications', function (err, applicationsData) {
179     if (err) return cb(err);
180     _.each(applicationsData, function (application, name) {
181       session._memoizeApplication(new Application(session.spire, application));
182     });
183     cb(null, session._applications);
184   });
185 };
186 
187 /**
188  * Gets an application by name.  Returns an Application resource
189  *
190  * Always gets a fresh value from the api.
191  *
192  * @example
193  * session.applicationByName('name_of_application', function (err, application) {
194  *   if (!err) {
195  *     // `application` now contains an application object
196  *   }
197  * });
198  *
199  * @param {String} applicationName Name of application
200  * @param {function (err, application)} cb Callback
201  */
202 Session.prototype.applicationByName = function (applicationName, cb) {
203   var session = this;
204   this.request('application_by_name', applicationName, function (err, applicationData) {
205     if (err) return cb(err);
206     application = new Application(session.spire, applicationData[applicationName]);
207     session._memoizeApplication(application);
208     cb(null, application);
209   });
210 };
211 
212 /**
213  * Gets the channels collection.  Returns a hash of Channel resources.
214  *
215  * Returns a value from the cache, if one if available.
216  *
217  * @example
218  * session.channels(function (err, channels) {
219  *   if (!err) {
220  *     // `channels` is a hash of all the account's channels
221  *   }
222  * });
223  *
224  * @param {function (err, channels)} cb Callback
225  */
226 Session.prototype.channels = function (cb) {
227   if (!_.isEmpty(this._channels)) return cb(null, this._channels);
228   this.channels$(cb);
229 };
230 
231 /**
232  * Gets the channels collection.  Returns a hash of Channel resources.
233  *
234  * Always gets a fresh value from the api.
235  *
236  * @example
237  * session.channels$(function (err, channels) {
238  *   if (!err) {
239  *     // `channels` is a hash of all the account's channels
240  *   }
241  * });
242  *
243  * @param {function (err, channels)} cb Callback
244  */
245 Session.prototype.channels$ = function (cb) {
246   var session = this;
247   this.request('channels', function (err, channelsData) {
248     if (err) return cb(err);
249     _.each(channelsData, function (channel, name) {
250       session._memoizeChannel(new Channel(session.spire, channel));
251     });
252     cb(null, session._channels);
253   });
254 };
255 
256 /**
257  * Gets a channel by name.  Returns a Channel resource
258  *
259  * Always gets a fresh value from the api.
260  *
261  * @example
262  * session.channelByName('name_of_channel', function (err, channel) {
263  *   if (!err) {
264  *     // `channel` now contains a channel object
265  *   }
266  * });
267  *
268  * @param {String} channelName Name of channel
269  * @param {function (err, channel)} cb Callback
270  */
271 Session.prototype.channelByName = function (channelName, cb) {
272   var session = this;
273   this.request('channel_by_name', channelName, function (err, channelData) {
274     if (err) return cb(err);
275     channel = new Channel(session.spire, channelData[channelName]);
276     session._memoizeChannel(channel);
277     cb(null, channel);
278   });
279 };
280 
281 /**
282  * Gets the subscriptions collection.  Returns a hash of Subscription resources.
283  *
284  * Returns a value from the cache, if one if available.
285  *
286  * @example
287  * session.subscriptions(function (err, subscriptions) {
288  *   if (!err) {
289  *     // `subscriptions` is a hash of all the account's subscriptions
290  *   }
291  * });
292  *
293  * @param {function (err, channels)} cb Callback
294  */
295 Session.prototype.subscriptions = function (cb) {
296   if (!_.isEmpty(this._subscriptions)) return cb(null, this._subscriptions);
297   this.subscriptions$(cb);
298 };
299 
300 /**
301  * Gets the subscriptions collection.  Returns a hash of Subscription resources.
302  *
303  * Always gets a fresh value from the api.
304  *
305  * @example
306  * session.subscriptions$(function (err, subscriptions) {
307  *   if (!err) {
308  *     // `subscriptions` is a hash of all the account's subscriptions
309  *   }
310  * });
311  *
312  * @param {function (err, subscriptions)} cb Callback
313  */
314 Session.prototype.subscriptions$ = function (cb) {
315   var session = this;
316   this.request('subscriptions', function (err, subscriptions) {
317     if (err) return cb(err);
318     session._subscriptions = {};
319     _.each(subscriptions, function (subscription, name) {
320       session._memoizeSubscription(new Subscription(session.spire, subscription));
321     });
322     cb(null, session._subscriptions);
323   });
324 };
325 
326 /**
327  * Gets a subscription by name.  Returns a Subscription resource
328  *
329  * Always gets a fresh value from the api.
330  *
331  * @example
332  * session.subscriptionByName('name_of_subscription', function (err, subscription) {
333  *   if (!err) {
334  *     // `subscription` now contains a subscription object
335  *   }
336  * });
337  *
338  * @param {String} subscriptionName Name of subscription
339  * @param {function (err, subscription)} cb Callback
340  */
341 Session.prototype.subscriptionByName = function (subscriptionName, cb) {
342   var session = this;
343   this.request('subscription_by_name', subscriptionName, function (err, subscriptionData) {
344     if (err) return cb(err);
345     subscription = new Subscription(session.spire, subscriptionData[subscriptionName]);
346     session._memoizeSubscription(subscription);
347     cb(null, subscription);
348   });
349 };
350 
351 /**
352  * Creates an application.  Returns an application resource.  Errors if an application with the
353  * specified name exists.
354  *
355  * @example
356  * session.createApplication('name_of_application', function (err, application) {
357  *   if (!err) {
358  *     // `application` now contains a application object
359  *   }
360  * });
361  * @param {string} name Application name
362  * @param {function (err, application)} cb Callback
363  */
364 Session.prototype.createApplication = function (name, cb) {
365   var session = this;
366   this.request('create_application', name, function (err, data) {
367     if (err) return cb(err);
368     var application = new Application(session.spire, data);
369     session._memoizeApplication(application);
370     cb(null, application);
371   });
372 };
373 
374 /**
375  * Creates a channel.  Returns a Channel resource.  Errors if a channel with the
376  * specified name exists.
377  *
378  * @example
379  * session.createChannel('foo', function (err, channel) {
380  *   if (!err) {
381  *     // `channel` is the channel named "foo".
382  *   }
383  * });
384  * @param {string} name Channel name
385  * @param {number} [limit] Number of messages to keep in channel
386  * @param {function (err, channel)} cb Callback
387  */
388 Session.prototype.createChannel = function (name, limit, cb) {
389   var session = this;
390   if (!cb) {
391     cb = limit;
392     limit = null;
393   }
394 
395   var opts = {
396     name: name
397   }
398 
399   if (limit !== null) {
400     opts.limit = limit
401   }
402 
403   this.request('create_channel', opts, function (err, data) {
404     if (err) return cb(err);
405     var channel = new Channel(session.spire, data);
406     session._memoizeChannel(channel);
407     cb(null, channel);
408   });
409 };
410 
411 /**
412  * Find or creates a channel
413  *
414  * @example
415  * session.findOrCreateChannel('foo', function (err, channel) {
416  *   if (!err) {
417  *     // `channel` is the channel named "foo".
418  *   }
419  * });
420  *
421  * @param {string} name Channel name to get or create
422  * @param {number} [limit] Number of messages to keep in channel
423  * @param {function(err, channel)} cb Callback
424  */
425 Session.prototype.findOrCreateChannel = function (name, limit, cb) {
426   var session = this;
427   var spire = this.spire;
428 
429   if (!cb) {
430     cb = limit;
431     limit = null;
432   }
433 
434   if (session._channels[name]) {
435     return cb(null, session._channels[name]);
436   }
437 
438   var creationCount = 0;
439 
440   function createChannel() {
441     creationCount++;
442     session.createChannel(name, limit, function (err, channel) {
443       if (!err) return cb(null, channel);
444       if (err.status !== 409) return cb(err);
445       if (creationCount >= spire.CREATION_RETRY_LIMIT) {
446         return cb(new Error("Could not create channel: " + name));
447       }
448       getChannel();
449     });
450   }
451 
452   function getChannel() {
453     spire.session.channelByName(name, function (err, channel) {
454       if (err && err.status !== 404) return cb(err);
455       if (channel) return cb(null, channel);
456       createChannel();
457     });
458   }
459 
460   getChannel();
461 };
462 
463 /**
464  * Creates a subscription to any number of channels.  Returns a Subscription
465  * resource.  Errors if a subscription with the specified name exists.
466  *
467  * @example
468  * session.findOrCreateSubscription({
469  *   name: 'my-sub',
470  *   channelNames: ['foo', 'bar']
471  * },
472  * function (err, subscription) {
473  *   if (!err) {
474  *     // `subscription` is the new subscription
475  *   }
476  * });
477  *
478  * @param {object} options Options
479  * @param {string} options.name Subscription name
480  * @param {array} options.channelNames Channel names to subscribe to
481  * @param {array} options.channelUrls Channel urls to subscribe to
482  * @param {number} options.expiration Subscription expiration (ms)
483  * @param {function (err, subscription)} cb Callback
484  */
485 Session.prototype.createSubscription = function (options, cb) {
486   var session = this;
487 
488   var name = options.name;
489   var channelNames = options.channelNames || [];
490   var channelUrls = options.channelUrls || [];
491   var expiration = options.expiration;
492 
493   function createSubscription() {
494     session.channels(function (channels) {
495       channelUrls.push.apply(channelUrls, _.map(channelNames, function (name) {
496         return session._channels[name].url();
497       }));
498       session.request('create_subscription', name, channelUrls, expiration, function (err, sub) {
499         if (err) return cb(err);
500         var subscription = new Subscription(session.spire, sub);
501         session._memoizeSubscription(subscription);
502         cb(null, subscription);
503       });
504     });
505   }
506 
507   if (channelNames.length) {
508     async.forEach(
509       channelNames,
510       function (channelName, innerCB) {
511         session.findOrCreateChannel(channelName, innerCB);
512       },
513       function (err) {
514         if (err) return cb(err);
515         createSubscription();
516       }
517     );
518   } else {
519     createSubscription();
520   }
521 };
522 
523 /**
524  * Gets a subscription to the given channels.  Creates the channels and the
525  * subscription if necessary.
526  *
527  * @example
528  * session.findOrCreateSubscription('mySubscription', ['foo', 'bar'], function (err, subscription) {
529  *   if (!err) {
530  *     // `subscription` is a subscription named 'mySubscription', listening on channels named 'foo' and 'bar'.
531  *   }
532  * });
533  *
534  * @param {string} Subscription name
535  * @param {array or string} channelOrChannels Either a single channel name, or an array of
536  *   channel names to subscribe to
537  * @param {function (err, subscription)} cb Callback
538  */
539 Session.prototype.findOrCreateSubscription = function (options, cb) {
540   var name = options.name;
541 
542   var session = this;
543   var spire = this.spire;
544 
545   var creationCount = 0;
546 
547   function createSubscription() {
548     creationCount++;
549     session.createSubscription(options, function (err, sub) {
550       if (!err) return cb(null, sub);
551       if (err.status !== 409) return cb(err);
552       if (creationCount >= spire.CREATION_RETRY_LIMIT) {
553         return cb(new Error("Could not create subscription: " + name));
554       }
555       getSubscription();
556     });
557   }
558 
559   function getSubscription() {
560     session.subscriptionByName(options.name, function (err, subscription) {
561       if (err && err.status !== 404) return cb(err);
562       if (subscription) return cb(null, subscription);
563       createSubscription();
564     });
565   }
566 
567   createSubscription();
568 };
569 
570 /**
571  * Creates an new subscription to a channel or channels, and adds a listener.
572  *
573  * @example
574  * session.subscribe('myChannel', options, function (messages) {
575  *   // `messages` is array of messages sent to the channel
576  * }, function (err) {
577  *   // `err` will be non-null if there was a problem creating the subscription.
578  * });
579  *
580  * By default this will get all events from the beginning of time.
581  * If you only want messages created from this point forward, pass { last: 'now' } in the options:
582  *
583  * @example
584  * session.subscribe('myChannel', options, function (messages) {
585  *   // `messages` is array of messages sent to the channel
586  * }, function (err) {
587  *   // `err` will be non-null if there was a problem creating the subscription.
588  * });
589  *
590  * @param {array or string} channelOrChannels Either a single channel name, or an array of
591  *   channel names to subscribe to
592  * @param {object} [options] Options to pass to the listener
593  * @param {function (messages)} listener Listener that will get called with each batch of messages
594  * @param {function (err, subscription)} [cb] Callback
595  */
596 Session.prototype.subscribe = function (channelOrChannels, options, listener, cb) {
597   if (typeof options === 'function') {
598     cb = listener;
599     listener = options;
600     options = {}
601   }
602 
603   if (typeof channelOrChannels === "string") {
604     channels = [channelOrChannels];
605   } else {
606     channels = channelOrChannels;
607   }
608 
609   cb = cb || function () {};
610 
611   this.findOrCreateSubscription({
612     channelNames: channels
613   }, function (err, subscription) {
614     if (err) return cb(err);
615     subscription.addListener('messages', listener);
616     subscription.startListening(options);
617     process.nextTick(function () {
618       cb(null, subscription);
619     });
620   });
621 };
622 
623 /**
624  * Publish to a channel.
625  *
626  * Creates the channel if necessary.
627  *
628  * @example
629  * session.publish('my_channel', 'my message', function (err, message) {
630  *   if (!err) {
631  *     //  Message sent successfully
632  *   }
633  * });
634  *
635  * @param {string} channelName Channel name
636  * @param {object, string} message Message
637  * @param {function (err, message)} cb Callback
638  */
639 Session.prototype.publish = function (channelName, message, cb) {
640   this.findOrCreateChannel(channelName, function (err, channel) {
641     if (err) { return cb(err); }
642     channel.publish(message, cb);
643   });
644 };
645 
646  /**
647  * Stores the application resource in a hash by its name.
648  *
649  * @param channel {object} Application to store
650  */
651 Session.prototype._memoizeApplication = function (application) {
652   this._applications[application.name()] = application;
653 };
654 
655  /**
656  * Stores the channel resource in a hash by its name.
657  *
658  * @param channel {object} Channel to store
659  */
660 Session.prototype._memoizeChannel = function (channel) {
661   this._channels[channel.name()] = channel;
662 };
663 
664 /**
665  * Stores the subscription resource in a hash by its name.
666  *
667  * @param subscription {object} Subscription to store
668  */
669 Session.prototype._memoizeSubscription = function (subscription) {
670   this._subscriptions[subscription.name()] = subscription;
671 };
672 
673 /**
674  * Stores the resources.
675  */
676 Session.prototype._storeResources = function () {
677   var session = this;
678 	var resources = {};
679   _.each(this.data.resources, function (resource, name) {
680     // Turn the account object into an instance of Resource.
681     if (name === 'account') {
682       resource = new Account(session.spire, resource);
683       session._account = resource;
684     }
685     resources[name] = resource;
686   });
687 
688   this.resources = resources;
689 };
690 
691 /**
692  * Requests
693  * These define API calls and have no side effects.  They can be run by calling
694  *     this.request(<request name>);
695  */
696 
697 /**
698  * Gets the account resource.
699  * @name account
700  * @ignore
701  */
702 Resource.defineRequest(Session.prototype, 'account', function () {
703   var resource = this.data.resources.account;
704   return {
705     method: 'get',
706     url: resource.url,
707     headers: {
708       'Authorization': this.authorization('get', resource),
709       'Accept': this.mediaType('account')
710     }
711   };
712 });
713 
714 /**
715  * Gets the channels collection.
716  * @name channels
717  * @ignore
718  */
719 Resource.defineRequest(Session.prototype, 'channels', function () {
720   var collection = this.data.resources.channels;
721   return {
722     method: 'get',
723     url: collection.url,
724     headers: {
725       'Authorization': this.authorization('all', collection),
726       'Accept': this.mediaType('channels')
727     }
728   };
729 });
730 
731 /**
732  * Gets a channel by name.  Returns a collection with a single value: { name: channel }.
733  * @name channel_by_name
734  * @ignore
735  */
736 Resource.defineRequest(Session.prototype, 'channel_by_name', function (name) {
737   var collection = this.data.resources.channels;
738   return {
739     method: 'get',
740     url: collection.url,
741     query: { name: name },
742     headers: {
743       'Authorization': this.authorization('get_by_name', collection),
744       'Accept': this.mediaType('channels')
745     }
746   };
747 });
748 
749 /**
750  * Creates a channel.  Returns a channel object.
751  * @name create_channel
752  * @ignore
753  */
754 Resource.defineRequest(Session.prototype, 'create_channel', function (opts) {
755   var collection = this.data.resources.channels;
756   return {
757     method: 'post',
758     url: collection.url,
759     content: opts,
760     headers: {
761       'Authorization': this.authorization('create', collection),
762       'Accept': this.mediaType('channel'),
763       'Content-Type': this.mediaType('channel')
764     }
765   };
766 });
767 
768 /**
769  * Gets the subscriptions collection.
770  * @name subscriptions
771  * @ignore
772  */
773 Resource.defineRequest(Session.prototype, 'subscriptions', function () {
774   var collection = this.data.resources.subscriptions;
775   return {
776     method: 'get',
777     url: collection.url,
778     headers: {
779       'Authorization': this.authorization('all', collection),
780       'Accept': this.mediaType('subscriptions')
781     }
782   };
783 });
784 
785 /**
786  * Gets a subscription by name.  Returns a collection with a single value: { name: subscription }.
787  * @name subscription_by_name
788  * @ignore
789  */
790 Resource.defineRequest(Session.prototype, 'subscription_by_name', function (name) {
791   var collection = this.data.resources.subscriptions;
792   return {
793     method: 'get',
794     url: collection.url,
795     query: { name: name },
796     headers: {
797       'Authorization': this.authorization('get_by_name', collection),
798       'Accept': this.mediaType('subscriptions')
799     }
800   };
801 });
802 
803 /**
804  * Creates a subscrtiption.  Returns a subscription object.
805  * @name create_subscription
806  * @ignore
807  */
808 Resource.defineRequest(Session.prototype, 'create_subscription', function (name, channelUrls, expiration) {
809   var collection = this.data.resources.subscriptions;
810   return {
811     method: 'post',
812     url: collection.url,
813     content: {
814       name: name,
815       channels: channelUrls,
816       expiration: expiration
817     },
818     headers: {
819       'Authorization': this.authorization('create', collection),
820       'Accept': this.mediaType('subscription'),
821       'Content-Type': this.mediaType('subscription')
822     }
823   };
824 });
825 
826 /**
827  * Gets the applications collection.
828  * @name applications
829  * @ignore
830  */
831 Resource.defineRequest(Session.prototype, 'applications', function () {
832   var collection = this.data.resources.applications;
833   return {
834     method: 'get',
835     url: collection.url,
836     headers: {
837       'Authorization': this.authorization('all', collection),
838       'Accept': this.mediaType('applications')
839     }
840   };
841 });
842 
843 /**
844  * Creates an application.  Returns an application object.
845  * @name create_application
846  * @ignore
847  */
848 Resource.defineRequest(Session.prototype, 'create_application', function (name) {
849   var collection = this.data.resources.applications;
850   return {
851     method: 'post',
852     url: collection.url,
853     content: {
854       name: name
855     },
856     headers: {
857       'Authorization': this.authorization('create', collection),
858       'Accept': this.mediaType('application'),
859       'Content-Type': this.mediaType('application')
860     }
861   };
862 });
863 
864 /**
865  * Gets an application by name
866  * @name application_by_name
867  * @ignore
868  */
869 Resource.defineRequest(Session.prototype, 'application_by_name', function (name) {
870   var collection = this.data.resources.applications;
871   return {
872     method: 'get',
873     url: collection.url,
874     query: {
875       name: name
876     },
877     headers: {
878       'Authorization': this.authorization('get_by_name', collection),
879       'Accept': this.mediaType('applications')
880     }
881   };
882 });
883