Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timob 12259 Messaging library #22

Merged
merged 9 commits into from
Jan 16, 2013
3 changes: 2 additions & 1 deletion lib/appc.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

/**
* thank's to John Resig for this concise function
* thank's to John Resig for this concise function
*/
Array.prototype.remove = function (from, to) {
var rest = this.slice((to || from) + 1 || this.length);
Expand Down Expand Up @@ -33,6 +33,7 @@ if (!global.dump) {
'image',
'i18n',
'ios',
'messaging',
'net',
'pkginfo',
'plist',
Expand Down
180 changes: 180 additions & 0 deletions lib/messaging.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/**
* Appcelerator Common Library for Node.js
* Copyright (c) 2012 by Appcelerator, Inc. All Rights Reserved.
* Please see the LICENSE file for information about licensing.
*
* <p>
* The messaging module provides communication capabilities with other processes via a RESTful client-server
* architecture. The transport mechanism is pluggable so that different transports can be swapped out without changing
* client code. Messages are passed inside of one or more protocols. The top-level protocol is defined below, and any
* other parent/lower-level protocols are defined on a transport by transport basis.
* <pre>
* Request:
* {
* messageType: [value],
* data: [value],
* }
*
* messageType: always a string, and is the value of messageType supplied to {@link module:messaging#MessagingInterface.listen} or {@link module:messaging#MessagingInterface.send}
* data: any valid JSON value
*
* Response:
*
* {
* error: [value]
* }
*
* or
*
* {
* data: [value]
* }
*
* error: error is a string if an error occured, null otherwise
* data: any valid JSON value
* Note: a response must always sent, even if there is no data to send, because the message serves as a request ACK.
* </pre>
* </p>
*
* @module messaging
*/

/**
* Creates a messaging interface over the specified transport
*
* @method
* @param {String} transportType The transport to use. Must be 'stdio' currently
*/
exports.create = function(transportType) {
if (['stdio'].indexOf(transportType) === -1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be nicer if this could do a little detection to see what messaging interfaces exist?

throw new Error('Invalid messaging transport type "' + transportType + '"');
}
return new MessagingInterface(transportType);
};

/**
* @classdesc A messaging interface for communicating with an external service
*
* @constructor
* @name module:messaging#MessagingInterface
*/
function MessagingInterface(transportType) {
this._transportType = transportType;
this._listeners = {};
}

/**
* Opens the messaging interface
*
* @method
* @name module:messaging#MessagingInterface.open
*/
MessagingInterface.prototype.open = function () {
if (this._transport) {
throw new Error('Cannot open a messaging interface that is already open');
}
this._transport = new (require('./messaging/' + this._transportType + 'Transport'))(function (requestData, response) {
try {
requestData = JSON.parse(requestData);
if (!requestData.messageType) {
throw 'missing message type';
}
} catch(e) {
response(JSON.stringify({
error: 'Malformed message: ' + e
}));
return;
}
if (this._listeners[requestData.messageType]) {
this._listeners[requestData.messageType](requestData, function (error, data) {
response(JSON.stringify({
error: error,
data: data
}));
});
} else {
response({
error: 'No listener available to handle request'
});
}
}.bind(this));
};

/**
* Closes the messaging interface
*
* @method
* @name module:messaging#MessagingInterface.close
*/
MessagingInterface.prototype.close = function () {
this._transport.close();
this._transport = null;
};

/**
* @method
* @name module:messaging#MessagingInterface~listenCallback
* @param {Object} request The request object
* @param {Any} request.data The data received, after having been parsed via JSON.parse
* @param {module:messaging~listenCallbackResponse} response The response object
*/
/**
* @method
* @name module:messaging#MessagingInterface~listenCallbackResponse
* @param {String|undefined} error The error, if one occured. Anything falsey is understood to mean no error occured, and
* the value is converted to undefined
* @param {Any|undefined} data The data, if any. The value is ignored if an error is supplied
*/
/**
* Listens for a message from Studio. Note: only one listener per message type is allowed because multiple listeners
* would send multiple responses to the sender
*
* @method
* @name module:messaging#MessagingInterface.listen
* @param {String} messageType The name of the message to listen for
* @param {module:messaging#MessagingInterface~listenCallback} callback The callback to fire when a message arrives. The callback is passed
* two parameters: request and response
*/
MessagingInterface.prototype.listen = function (messageType, callback) {
this._listeners[messageType] = callback;
};

/**
* @method
* @name module:messaging#MessagingInterface~sendCallback
* @param {String|undefined} error The error message, if one occured, else undefined
* @param {Any|undefined} data The data, if an error did not occur, else undefined
*/
/**
* Sends a message to Studio
*
* @method
* @name module:messaging#MessagingInterface.send
* @param {String|undefined} messageType The name of the message to send
* @param {Any|undefined} data The data to send. Must be JSON.stringify-able (i.e. no cyclic structures). Can be primitive or
* undefined, although undefined is converted to null
* @param {module:messaging#MessagingInterface~sendCallback} callback The callback to fire once the transmission is complete or
* has errored. The error parameter is null if no error occured, or a string indicating the error. The data
* parameter is null if an error occured, or any type of data (including null) if no error occured.
* @throws {Error} An exception is thrown if send is called while the interface is closed
*/
MessagingInterface.prototype.send = function (messageType, data, callback) {
if (!this._transport) {
throw new Error('Attempted to send data over a closed messaging interface');
}
this._transport.send(JSON.stringify({
messageType: messageType,
data: typeof data === 'undefined' ? null : data
}), function (response) {
try {
response = JSON.parse(response);
} catch(e) {
response = {
error: 'Malformed message: ' + e
};
}
if (callback) {
callback(response.error, response.data);
}
});
};
167 changes: 167 additions & 0 deletions lib/messaging/stdioTransport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Appcelerator Common Library for Node.js
* Copyright (c) 2012 by Appcelerator, Inc. All Rights Reserved.
* Please see the LICENSE file for information about licensing.
*
* <p>A transport that uses stdin and stdout as the communications channel. A low-level packet format, defined below, is
* used to ensure proper message delivery.
* <pre>
* [Message Type],[Sequence ID],[Message Length],[data]
* MessageType: A three character sequence that is either 'REQ' (request) or 'RES' (response)
* Sequence ID: A 32-bit, base 16 number that identifies the message. This value is always 8 characters long, and
* includes 0 padding if necessary. Hex letters must be lower case. Note: Response messages have the same
* Sequence ID as the request that generated the response
* Message Length: A 32-bit, base 16 number that identifies the length of the message. This value is always 8
* characters long, and includes 0 padding if necessary. Hex letters must be lower case.
* Example: REQ,000079AC,0000000C,{foo: 'bar'}
* Example: RES,000079AC,0000000C,{foo: 'baz'}
* <pre>
* </p>
*
* @module messaging/stdio
*/

/**
* @private
*/
var channels = {},
numChannels = 0,
STATE_MESSAGE_TYPE = 1,
STATE_SEQUENCE_ID = 2,
STATE_MESSAGE_LENGTH = 3,
STATE_DATA = 4;

function zeroPad(value, length) {
var string = value.toString();
while(string.length < length) {
string = '0' + string;
}
return string;
}

/**
* @private
*/
module.exports = function (requestCallback) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of times I see node modules where they'll do "function stdioTransport() {" and stdioTransport.prototype.whatever, then at the bottom do module.exports = stdioTransport; It makes the code a tiny bit less confusing, but it's your preference.

var i,
channel,
buffer = '',
state = STATE_MESSAGE_TYPE,
messageType,
sequenceID,
messageLength,
data;
for(i = 1; i < 256; i++) {
if (!channels[i]) {
channel = i;
break;
}
}
if (!channel) {
throw new Error('All stdio messaging channels are in use (max limit is 255).');
}

channels[channel] = this;
this._sequenceIDPrefix = channel << 24;
this._requestCallback = requestCallback;
this._sequenceIDCount = 1;
this._responseCallbacks = {};

if (!numChannels) {
process.stdin.setEncoding('utf8');
process.stdin.resume();
process.stdin.setRawMode(true);
}
numChannels++;
process.stdin.on('data', function processChunk(chunk) {
buffer += chunk;
switch(state) {
case STATE_MESSAGE_TYPE:
if (buffer.length > 3) {
messageType = buffer.substring(0,3);
buffer = buffer.substring(4);
state = STATE_SEQUENCE_ID;
} else {
break;
}
case STATE_SEQUENCE_ID:
if (buffer.length > 8) {
sequenceID = parseInt(buffer.substring(0,8), 16);
buffer = buffer.substring(9);
state = STATE_MESSAGE_LENGTH;
} else {
break;
}
case STATE_MESSAGE_LENGTH:
if (buffer.length > 8) {
messageLength = parseInt(buffer.substring(0,8), 16);
buffer = buffer.substring(9);
state = STATE_DATA;
} else {
break;
}
case STATE_DATA:
if (buffer.length >= messageLength) {
data = buffer.substring(0, messageLength);

switch(messageType) {
case 'REQ':
requestCallback(data, function (responseData) {
var msg = 'RES,' + zeroPad(sequenceID.toString(16), 8) + ',' +
zeroPad(responseData.length.toString(16), 8) + ',' + responseData;
process.stdout.write(msg);
});
break;
case 'RES':
if (this._responseCallbacks[sequenceID]) {
this._responseCallbacks[sequenceID](data);
}
break;
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for a default case. It's not hurting anything either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol, I originally meant to do something with this default cause but couldn't think of anything and forgot to remove it :)

break;
}
state = STATE_MESSAGE_TYPE;

buffer = buffer.substring(messageLength);
if (buffer.length) {
processChunk('');
}
}
break;
}
}.bind(this));
return;
};

/**
* @private
*/
module.exports.prototype.close = function () {
var i;
numChannels--;
if (!numChannels) {
process.stdin.pause();
process.stdin.setRawMode(false);
}
for(i = 0; i < 256; i++) {
if (channels[i] === this) {
delete channels[i];
}
}
};

/**
* @private
*/
module.exports.prototype.send = function (data, callback) {
var seqId = this._sequenceIDPrefix + (this._sequenceIDCount++),
msg = 'REQ,' + zeroPad(seqId.toString(16), 8) + ',' + zeroPad(data.length.toString(16), 8) + ',' + data;
if (this._sequenceIDCount > 0xFFFFFF) {
this._sequenceIDCount = 0;
}
process.stdout.write(msg);
this._responseCallbacks[seqId] = function(data) {
delete this._responseCallbacks[seqId];
callback && callback(data);
}.bind(this);
};