-
-
Notifications
You must be signed in to change notification settings - Fork 29
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
Changes from 8 commits
ae00003
1fbdfa3
7ecb07a
8f33234
12c72f9
cff83d0
a023104
12e2844
050a47e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) { | ||
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); | ||
} | ||
}); | ||
}; |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need for a default case. It's not hurting anything either. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
}; |
There was a problem hiding this comment.
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?