UNPKG

nitrogen

Version:

Nitrogen is a platform for building connected devices. Nitrogen provides the authentication, authorization, and real time message passing framework so that you can focus on your device and application. All with a consistent development platform that lev

299 lines (249 loc) 9.89 kB
/** * The Message object is the core of the Nitrogen framework. Applications, devices, and * services use Messages to communicate with and issue commands to each other. All messages * that don't begin with an unscore are checked against a schema chosen by the messages 'type' * and 'schema_version' fields such that a message of a given type is known to conform to a * particular structure. This enables sharing between differing devices and applications. For * custom message types, an application may use an unscore prefix (eg. '_myMessage') with any * schema that they'd like. This supports communication between principals of the same * organization over a private schema. That said, it is strongly encouraged to use standard * schemas wherever possible. * * Messages have a sender principal (referred to as 'from') and a receiver principal (referred * to as 'to'). These fields are used to route messages to their receiver. * * Message types are divided into two main classes: data and commands. Data messages carry * information, typically the output of a device's operation. For example, a message typed * 'image' contains an image url in its body in its 'url' property. * * The second class of messages are commands. Command messages are sent from one principal to * another to request an operation on the receiving principal. For example, a message of the * type 'cameraCommand' contains a command that directs the operation of a camera principal. * * @class Message * @namespace nitrogen */ function Message(json) { this.ts = new Date(); this.body = {}; for(var key in json) { if(json.hasOwnProperty(key)) { if (key === 'ts' || key === 'expires' || key === 'index_until') this[key] = new Date(Date.parse(json[key])); else this[key] = json[key]; } } } /** * Find messages filtered by the passed query and limited to and sorted by the passed options. * * @method find * @async * @param {Object} session The session with a Nitrogen service to make this request under. * @param {Object} query A query filter for the messages you want to find defined using MongoDB query format. * @param {Object} options Options for the query: 'limit': maximum number of results to be returned. 'sort': The field that the results should be sorted on, 'dir': The direction that the results should be sorted. 'skip': The number of results that should be skipped before pulling results. * @param {Function} callback Callback function of the form f(err, messages). **/ Message.find = function(session, query, options, callback) { if (!session) return callback('no session passed to Message.find'); if (!callback || typeof(callback) !== 'function') return callback('no callback passed to Message.find.'); if (!query) query = {}; if (!options) options = {}; var messageUrl = session.service.config.endpoints.messages; session.get({ url: messageUrl, query: query, queryOptions: options, json: true }, function(err, resp, body) { if (err) return callback(err); var messages = body.messages.map(function(message) { return new Message(message); }); callback(null, messages); }); }; /** * Returns true if the message is of the passed type. * * @method is * @param {String} type Message type to compare against. * @returns {Boolean} Returns true if the message is of the passed type. **/ Message.prototype.is = function(type) { return this.type === type; }; /** * Returns true if the message is from the passed principal. * * @method isFrom * @param {String} principalId Principal id to compare against. * @returns {Boolean} Returns true if the message is from the passed principal id. **/ Message.prototype.isFrom = function(principal) { return this.from === principal.id; }; /** * Returns true if the message is in response to the passed message. * * @method isResponseTo * @param {String} type Message to compare against. * @returns {Boolean} Returns true if the message is in response to the passed message. **/ Message.prototype.isResponseTo = function(otherMessage) { return otherMessage.id && this.response_to && this.response_to.indexOf(otherMessage.id) !== -1; }; /** * Returns true if the message is of the passed type. * * @method isTo * @param {String} principalId Principal id to compare against. * @returns {Boolean} Returns true if the message is of the passed type. **/ Message.prototype.isTo = function(principal) { return this.to === principal.id; }; /** * Removes a set of messages specified by passed filter. Used by the internal service principal to * to cleanup expired messages etc. * * @method remove * @async * @static * @private * @param {Object} session An open session with a Nitrogen service. * @param {Object} query A query filter for the messages you want to remove. * @param {Function} callback Callback function of the form f(err, removedCount). */ Message.remove = function(session, query, callback) { session.remove({ url: session.service.config.endpoints.messages, query: query, json: true }, function(err, resp, body) { if (err) return callback(err); if (resp.statusCode != 200) return callback(resp.statusCode); callback(null, body.removed); }); }; /** * Remove this message. Used by the internal service principal for cleanup. * * @method remove * @async * @private * @param {Object} session An open session with a Nitrogen service. * @param {Function} callback Callback function of the form f(err, removedCount). **/ Message.prototype.remove = function(session, callback) { Message.remove(session, { "_id": this.id }, callback || function() {}); }; /** * Send this message. * * @method send * @async * @param {Object} session An open session with a Nitrogen service. * @param {Function} callback Callback function of the form f(err, sentMessages). **/ Message.prototype.send = function(session, callback) { Message.sendMany(session, [this], callback || function() {}); }; /** * Send multiple messages. * * @method sendMany * @async * @param {Object} session An open session with a Nitrogen service. * @param {Array} messages An array of messages to send. * @param {Function} callback Callback function of the form f(err, sentMessages). **/ Message.sendMany = function(session, messages, callback) { if (!session) return callback('session required for Message.sendMany'); var self = this; Message.queuedMessages.push({ session: session, messages: messages, callback: callback }); Message.setupSendTimeout(); }; // TODO: there is an efficiency opportunity here to combine messages sent over the same session into one request. // // Challenges: // - Would need to do accounting around callbacks to make sure they are all called with the right messages. // - How do you handle the case where there is a 400 in one of the blocks but not in another. Message.backoffMillis = 1; Message.sendQueue = function() { Message.sendTimeout = null; var context = Message.queuedMessages[0]; Message.queuedMessages = Message.queuedMessages.slice(1); context.session.post({ url: context.session.service.config.endpoints.messages, json: context.messages }, function(err, resp, body) { if (resp && resp.statusCode !== 200) { err = err || body.message || resp.statusCode; // bad request responses are fatal, otherwise requeue and try again. if (resp.statusCode !== 400) Message.queuedMessages.unshift(context); } if (err) { Message.backoffMillis *= 4; Message.backoffMillis = Math.min((64 + Math.random()) * 1000, Message.backoffMillis) } else { Message.backoffMillis = 1; } // if we still have messages in the queue after all of this, set up the next process. if (Message.queuedMessages.length) { Message.setupSendTimeout(); } if (err) { context.session.log.error('sending message failed with error: ' + JSON.stringify(err)); if (context.callback) context.callback(err); return; } var sentMessages = []; body.messages.forEach(function(messageJson) { sentMessages.push(new Message(messageJson)); }); if (context.callback) context.callback(null, sentMessages); }); }; Message.setupSendTimeout = function() { var self = this; if (!Message.sendTimeout) { Message.sendTimeout = setTimeout(function() { self.sendQueue(); }, Message.backoffMillis); } }; Message.queuedMessages = []; Message.sendTimeout = null; /** * Returns true if the message expired. * * @method expired * @returns {Boolean} Returns true if the message is expired. **/ Message.prototype.expired = function() { return this.millisToExpiration() < 0; }; /** * Returns the number of milliseconds before this message expires. * * @method millisToExpiration * @returns {Number} Number of milliseconds before this message expires. **/ Message.prototype.millisToExpiration = function() { return this.expires - new Date().getTime(); }; /** * Returns the number of milliseconds before the timestamp for this message. Used to calculate * time to execution for command messages. * * @method millisToTimestamp * @returns {Number} Number of milliseconds before the timestamp for this message. **/ Message.prototype.millisToTimestamp = function() { return this.ts - new Date().getTime(); }; Message.NEVER_EXPIRE = new Date(Date.UTC(2500, 0, 1)); Message.INDEX_FOREVER = new Date(Date.UTC(2500, 0, 1)); module.exports = Message;