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