diffusion
Version:
Diffusion JavaScript client
275 lines (222 loc) • 8.35 kB
JavaScript
/*eslint valid-jsdoc: "off"*/
var encodeAsString = require('v4-stack/credential-tunnel').encodeAsString;
var Emitter = require('events/emitter');
var Queue = require('util/queue');
var ResponseCode = require('protocol/response-code');
var BufferInputStream = require('io/buffer-input-stream');
var BufferOutputStream = require('io/buffer-output-stream');
var log = require('util/logger').create('XHR transport');
/**
* Construct a valid URI from the given options.
*
* @param {Options} opts - The connection options.
* @returns {String} The connection URI
*/
function constructURI(req, opts) {
var scheme = opts.secure ? 'https' : 'http';
var uri = scheme + "://" + opts.host + ":" + opts.port + opts.path;
var headers = {
m : "0",
ty : "B",
ca : req.capabilities,
r : opts.reconnect.timeout,
v : req.version
};
if (req.token) {
headers.c = req.token;
headers.cs = req.availableClientSequence;
headers.ss =req.lastServerSequence;
}
if (opts.properties) {
headers.sp = encodeURIComponent(JSON.stringify(opts.properties));
}
if (opts.principal) {
headers.username = encodeURIComponent(opts.principal);
headers.password = encodeAsString(opts.credentials);
}
return { 'uri' : uri , 'headers' : headers };
}
function XHRTransport(opts, xhr) {
var emitter = Emitter.assign(this);
var token;
var pollSequence = 0;
var pingSequence = 0;
var URI;
var pollRequest = null;
var aborted = false;
var isSending = false;
var protocolVersion = null;
var self = this;
// Message message-queue
var queue = Queue.create();
var constructor = xhr;
/**
* Establish a polling connection with the server.
*/
this.connect = function connect(req, handshake) {
try {
var url = constructURI(req, opts);
protocolVersion = url.headers.v;
URI = url.uri;
var request = this.createRequest(url.headers, url.uri);
log.debug('Created XHR', url.uri);
request.onreadystatechange = function() {
if (request.readyState === 4 ) {
if (request.status === 200) {
var handShakeData = request.responseText;
// The initial connection is in plain-text no need for base64 encoding/decoding.
var serverResponse = handshake(new Buffer(handShakeData, 'binary'));
if (!serverResponse) {
// If the handshake could not be parsed or there was some sort of error return
// The transport will be closed by the error handler
return;
}
token = serverResponse.token;
var responseCode = serverResponse.response;
var isSuccesss = ResponseCode.isSuccess(responseCode);
if (isSuccesss) {
self.poll();
} else {
log.debug(responseCode.id + ' - ' + responseCode.message);
emitter.close();
}
} else {
emitter.close();
}
}
};
request.send(null);
} catch (error) {
throw new Error('Unable to create polling transport', error);
}
};
/**
* Aborting any opening poll request.
*/
this.close = function() {
log.trace("Closing XHR transport");
if (pollRequest) {
aborted = true;
pollRequest.abort();
}
// clear any pending messages
queue = Queue.create();
emitter.close();
};
/**
* This is the generic method to send data to the server
*
* @param headers the headers to send to the server
*/
this.createRequest = function createRequest(headers, uri) {
var request = new constructor();
request.open("POST", uri, true);
request.overrideMimeType('text\/plain; charset=x-user-defined');
for (var header in headers) {
try {
request.setRequestHeader(header,headers[header]);
} catch(e) {
log.warn("Can't set header " + header + ":" + headers.join(":"));
}
}
return request;
};
/**
* Send a poll request to the server.
*/
this.poll = function poll() {
var request = this.createRequest(
{ "m" : "1", "c" : token, "s" : pollSequence++, "v" : protocolVersion },
URI);
request.onreadystatechange = function() {
if (request.readyState === 4) {
// From protocol 9+, the client should send a reconnect request when instructed by the server
if (request.getResponseHeader('diffusion-connection') === 'reconnect') {
emitter.close();
}
else if (request.status === 200) {
var data = request.responseText;
var bis = new BufferInputStream(new Buffer(data, 'base64'));
while (bis.pos !== bis.count) {
var length = bis.readInt32();
if (length > opts.maxMessageSize) {
emitter.error(new Error(
"Received message of size: " +
length +
", exceeding max message size: " +
opts.maxMessageSize));
return;
}
var typeAndEncoding = bis.read();
var body = bis.readMany(length);
var payload = new Buffer(body.length + 1); // +1 for message type and encoding byte.
payload.writeUInt8(typeAndEncoding);
body.copy(payload, 1);
emitter.emit('data', payload);
}
self.poll();
}
else {
emitter.close();
}
}
};
pollRequest = request;
request.send(null);
};
/**
* Queue message to be sent to the message message-queue.
*
* Delegate the actual request sending to #flush().
*
* @param {Message} message to be queued
*/
this.dispatch = function dispatch(message) {
if (aborted) {
return false;
}
queue.add(message);
if (isSending) {
return true;
}
self.flush();
};
/**
* Flush all pending messages from the message-queue and send upstream.
*/
this.flush = function flush() {
if (queue.isEmpty()) {
return;
}
var buffer = drainToBuffer(queue.drain());
var request = this.createRequest(
{ "m" : "2", "c" : token, "s" : pingSequence++, "v" : protocolVersion },
URI);
request.onreadystatechange = function() {
if (request.readyState === 4) {
// From protocol 9+, the client should send a reconnect request when instructed by the server
if (request.getResponseHeader('diffusion-connection') === 'reconnect') {
emitter.close();
}
else if (request.status === 200) {
isSending = false;
self.flush();
} else {
emitter.close();
}
}
};
isSending = true;
request.send(buffer.toString('base64'));
};
}
function drainToBuffer (messages) {
var messagesBOS = new BufferOutputStream();
for (var i = 0; i < messages.length; i++) {
var msg = messages[i];
messagesBOS.writeInt32(msg.length - 1); // Skip the message type byte.
messagesBOS.writeMany(msg, 0, msg.length);
}
return messagesBOS.getBuffer();
}
module.exports = XHRTransport;