UNPKG

halley

Version:

A bayeux client for modern browsers and node. Forked from Faye

299 lines (242 loc) 8.82 kB
'use strict'; var Scheduler = require('./scheduler'); var Transport = require('../transport/transport'); var Channel = require('./channel'); var TransportPool = require('../transport/pool'); var uri = require('../util/uri'); var extend = require('../util/externals').extend; var Events = require('../util/externals').Events; var debug = require('debug')('halley:dispatcher'); var Promise = require('bluebird'); var Envelope = require('./envelope'); var TransportError = require('../util/errors').TransportError; var danglingFinally = require('../util/promise-util').danglingFinally; var HANDSHAKE = 'handshake'; var BAYEUX_VERSION = '1.0'; var STATE_UP = 1; var STATE_DOWN = 2; /** * The dispatcher sits between the client and the transport. * * It's responsible for tracking sending messages to the transport, * tracking in-flight messages */ function Dispatcher(endpoint, advice, options) { this._advice = advice; this._envelopes = {}; this._scheduler = options.scheduler || Scheduler; this._state = 0; this._pool = new TransportPool(this, uri.parse(endpoint), advice, options.disabled, Transport.getRegisteredTransports()); } Dispatcher.prototype = { destroy: function() { debug('destroy'); this.close(); }, close: function() { debug('_close'); this._cancelPending(); debug('Dispatcher close requested'); this._pool.close(); }, _cancelPending: function() { var envelopes = this._envelopes; this._envelopes = {}; var envelopeKeys = Object.keys(envelopes); debug('_cancelPending %s envelopes', envelopeKeys.length); envelopeKeys.forEach(function(id) { var envelope = envelopes[id]; envelope.reject(new Error('Dispatcher closed')); }, this); }, getConnectionTypes: function() { return Transport.getConnectionTypes(); }, selectTransport: function(allowedTransportTypes, cleanup) { return this._pool.setAllowed(allowedTransportTypes, cleanup); }, /** * Returns a promise of the response */ sendMessage: function(message, options) { var id = message.id; var envelopes = this._envelopes; var advice = this._advice; var envelope = envelopes[id] = new Envelope(message); var timeout; if (options && options.timeout) { timeout = options.timeout; } else { timeout = advice.timeout; } var scheduler = new this._scheduler(message, { timeout: timeout, interval: advice.retry, attempts: options && options.attempts }); var promise = this._attemptSend(envelope, message, scheduler); if (options && options.deadline) { promise = promise.timeout(options && options.deadline, 'Timeout on deadline'); } return danglingFinally(promise, function() { debug('sendMessage finally: message=%j', message); if (promise.isFulfilled()) { scheduler.succeed(); } else { scheduler.abort(); } delete envelopes[id]; }); }, _attemptSend: function(envelope, message, scheduler) { if (!scheduler.isDeliverable()) { return Promise.reject(new Error('No longer deliverable')); } scheduler.send(); var timeout = scheduler.getTimeout(); // 1. Obtain transport return this._pool.get() .bind(this) .then(function(transport) { debug('attemptSend: %j', message); envelope.startSend(transport); // 2. Send the message using the given transport var enrichedMessage = this._enrich(message, transport); return transport.sendMessage(enrichedMessage); }) .then(function() { this._triggerUp(); // 3. Wait for the response from the transport return envelope.awaitResponse(); }) .timeout(timeout, 'Timeout on message send') .finally(function() { envelope.stopSend(); }) .then(function(response) { // 4. Parse the response if (response.successful === false && response.advice && response.advice.reconnect === HANDSHAKE) { // This is not standard, and may need a bit of reconsideration // but if the client sends a message to the server and the server responds with // an error and tells the client it needs to rehandshake, // reschedule the send after the send after the handshake has occurred. throw new TransportError('Message send failed with advice reconnect:handshake, will reschedule send'); } return response; }) .catch(Promise.TimeoutError, TransportError, function(e) { debug('Error while attempting to send message: %j: %s', message, e); this._triggerDown(); scheduler.fail(); if (!scheduler.isDeliverable()) { throw e; } // Either the send timed out or no transport was // available. Either way, wait for the interval and try again return this._awaitRetry(envelope, message, scheduler); }); }, /** * Adds required fields into the message */ _enrich: function(message, transport) { if (message.channel === Channel.CONNECT) { message.connectionType = transport.connectionType; } if (message.channel === Channel.HANDSHAKE) { message.version = BAYEUX_VERSION; message.supportedConnectionTypes = this.getConnectionTypes(); } else { if (!this.clientId) { // Theres probably a nicer way of doing this. If the connection // is in the process of being re-established, throw an error // for non-handshake messages which will cause them to be rescheduled // in future, hopefully once the client is CONNECTED again throw new Error('client is not yet established'); } message.clientId = this.clientId; } return message; }, /** * Send has failed. Retry after interval */ _awaitRetry: function(envelope, message, scheduler) { // Either no transport is available or a timeout occurred waiting for // the transport. Wait a bit, the try again return Promise.delay(scheduler.getInterval()) .bind(this) .then(function() { return this._attemptSend(envelope, message, scheduler); }); }, handleResponse: function(reply) { if (reply.advice) this._advice.update(reply.advice); var id = reply.id; var envelope = id && this._envelopes[id]; if (reply.successful !== undefined && envelope) { // This is a response to a message we fired. envelope.resolve(reply); } else { // Distribe this message through channels // Don't trigger a message if this is a reply // to a request, otherwise it'll pass // through the extensions twice this.trigger('message', reply); } }, _triggerDown: function() { if (this._state === STATE_DOWN) return; debug('Dispatcher is DOWN'); this._state = STATE_DOWN; this.trigger('transport:down'); }, _triggerUp: function() { if (this._state === STATE_UP) return; debug('Dispatcher is UP'); this._state = STATE_UP; this.trigger('transport:up'); // If we've disable websockets due to a network // outage, try re-enable them now this._pool.reevaluate(); }, /** * Called by transports on connection error */ handleError: function(transport) { // This method may be called from outside the eventloop // so queue the method to ensure any finally methods // on pending promises are called prior to executing Promise.resolve() .bind(this) .then(function() { var envelopes = this._envelopes; // If the transport goes down, reject any outstanding // connect messages. We don't reject non-connect messages // as we assume that they've been sent to the server // already, and we don't need to resend them. If they had failed // to send, the send would have been rejected already. // As a failback, the message timeout will eventually // result in a rejection anyway. Object.keys(envelopes).forEach(function(id) { var envelope = envelopes[id]; var message = envelope.message; if (envelope.transport === transport && message && message.channel === Channel.CONNECT) { envelope.reject(new Error('Transport failed')); } }); // If this transport is the current, // report the connection as down if (transport === this._pool.current()) { this._triggerDown(); } this._pool.down(transport); }); }, isTransportUp: function() { return this._state === STATE_UP; } }; /* Mixins */ extend(Dispatcher.prototype, Events); module.exports = Dispatcher;