faye
Version:
Simple pub/sub messaging for the web
386 lines (313 loc) • 14.2 kB
JavaScript
'use strict';
var asap = require('asap'),
Class = require('../util/class'),
Promise = require('../util/promise'),
array = require('../util/array'),
browser = require('../util/browser'),
constants = require('../util/constants'),
assign = require('../util/assign'),
validateOptions = require('../util/validate_options'),
Deferrable = require('../mixins/deferrable'),
Logging = require('../mixins/logging'),
Publisher = require('../mixins/publisher'),
Channel = require('./channel'),
Dispatcher = require('./dispatcher'),
Error = require('./error'),
Extensible = require('./extensible'),
Publication = require('./publication'),
Subscription = require('./subscription');
var Client = Class({ className: 'Client',
UNCONNECTED: 1,
CONNECTING: 2,
CONNECTED: 3,
DISCONNECTED: 4,
HANDSHAKE: 'handshake',
RETRY: 'retry',
NONE: 'none',
CONNECTION_TIMEOUT: 60,
DEFAULT_ENDPOINT: '/bayeux',
INTERVAL: 0,
initialize: function(endpoint, options) {
this.info('New client created for ?', endpoint);
options = options || {};
validateOptions(options, ['interval', 'timeout', 'endpoints', 'proxy', 'retry', 'scheduler', 'websocketExtensions', 'tls', 'ca']);
this._channels = new Channel.Set();
this._dispatcher = Dispatcher.create(this, endpoint || this.DEFAULT_ENDPOINT, options);
this._messageId = 0;
this._state = this.UNCONNECTED;
this._responseCallbacks = {};
this._advice = {
reconnect: this.RETRY,
interval: 1000 * (options.interval || this.INTERVAL),
timeout: 1000 * (options.timeout || this.CONNECTION_TIMEOUT)
};
this._dispatcher.timeout = this._advice.timeout / 1000;
this._dispatcher.bind('message', this._receiveMessage, this);
if (browser.Event && global.onbeforeunload !== undefined)
browser.Event.on(global, 'beforeunload', function() {
if (array.indexOf(this._dispatcher._disabled, 'autodisconnect') < 0)
this.disconnect();
}, this);
},
addWebsocketExtension: function(extension) {
return this._dispatcher.addWebsocketExtension(extension);
},
disable: function(feature) {
return this._dispatcher.disable(feature);
},
setHeader: function(name, value) {
return this._dispatcher.setHeader(name, value);
},
// Request
// MUST include: * channel
// * version
// * supportedConnectionTypes
// MAY include: * minimumVersion
// * ext
// * id
//
// Success Response Failed Response
// MUST include: * channel MUST include: * channel
// * version * successful
// * supportedConnectionTypes * error
// * clientId MAY include: * supportedConnectionTypes
// * successful * advice
// MAY include: * minimumVersion * version
// * advice * minimumVersion
// * ext * ext
// * id * id
// * authSuccessful
handshake: function(callback, context) {
if (this._advice.reconnect === this.NONE) return;
if (this._state !== this.UNCONNECTED) return;
this._state = this.CONNECTING;
var self = this;
this.info('Initiating handshake with ?', this._dispatcher.endpoint.href);
this._dispatcher.selectTransport(constants.MANDATORY_CONNECTION_TYPES);
this._sendMessage({
channel: Channel.HANDSHAKE,
version: constants.BAYEUX_VERSION,
supportedConnectionTypes: this._dispatcher.getConnectionTypes()
}, {}, function(response) {
if (response.successful) {
this._state = this.CONNECTED;
this._dispatcher.clientId = response.clientId;
this._dispatcher.selectTransport(response.supportedConnectionTypes);
this.info('Handshake successful: ?', this._dispatcher.clientId);
this.subscribe(this._channels.getKeys(), true);
if (callback) asap(function() { callback.call(context) });
} else {
this.info('Handshake unsuccessful');
global.setTimeout(function() { self.handshake(callback, context) }, this._dispatcher.retry * 1000);
this._state = this.UNCONNECTED;
}
}, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * connectionType * clientId
// MAY include: * ext MAY include: * error
// * id * advice
// * ext
// * id
// * timestamp
connect: function(callback, context) {
if (this._advice.reconnect === this.NONE) return;
if (this._state === this.DISCONNECTED) return;
if (this._state === this.UNCONNECTED)
return this.handshake(function() { this.connect(callback, context) }, this);
this.callback(callback, context);
if (this._state !== this.CONNECTED) return;
this.info('Calling deferred actions for ?', this._dispatcher.clientId);
this.setDeferredStatus('succeeded');
this.setDeferredStatus('unknown');
if (this._connectRequest) return;
this._connectRequest = true;
this.info('Initiating connection for ?', this._dispatcher.clientId);
this._sendMessage({
channel: Channel.CONNECT,
clientId: this._dispatcher.clientId,
connectionType: this._dispatcher.connectionType
}, {}, this._cycleConnection, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// MAY include: * ext * clientId
// * id MAY include: * error
// * ext
// * id
disconnect: function() {
if (this._state !== this.CONNECTED) return;
this._state = this.DISCONNECTED;
this.info('Disconnecting ?', this._dispatcher.clientId);
var promise = new Publication();
this._sendMessage({
channel: Channel.DISCONNECT,
clientId: this._dispatcher.clientId
}, {}, function(response) {
if (response.successful) {
this._dispatcher.close();
promise.setDeferredStatus('succeeded');
} else {
promise.setDeferredStatus('failed', Error.parse(response.error));
}
}, this);
this.info('Clearing channel listeners for ?', this._dispatcher.clientId);
this._channels = new Channel.Set();
return promise;
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * subscription * clientId
// MAY include: * ext * subscription
// * id MAY include: * error
// * advice
// * ext
// * id
// * timestamp
subscribe: function(channel, callback, context) {
if (channel instanceof Array)
return array.map(channel, function(c) {
return this.subscribe(c, callback, context);
}, this);
var subscription = new Subscription(this, channel, callback, context),
force = (callback === true),
hasSubscribe = this._channels.hasSubscription(channel);
if (hasSubscribe && !force) {
this._channels.subscribe([channel], subscription);
subscription.setDeferredStatus('succeeded');
return subscription;
}
this.connect(function() {
this.info('Client ? attempting to subscribe to ?', this._dispatcher.clientId, channel);
if (!force) this._channels.subscribe([channel], subscription);
this._sendMessage({
channel: Channel.SUBSCRIBE,
clientId: this._dispatcher.clientId,
subscription: channel
}, {}, function(response) {
if (!response.successful) {
subscription.setDeferredStatus('failed', Error.parse(response.error));
return this._channels.unsubscribe(channel, subscription);
}
var channels = [].concat(response.subscription);
this.info('Subscription acknowledged for ? to ?', this._dispatcher.clientId, channels);
subscription.setDeferredStatus('succeeded');
}, this);
}, this);
return subscription;
},
// Request Response
// MUST include: * channel MUST include: * channel
// * clientId * successful
// * subscription * clientId
// MAY include: * ext * subscription
// * id MAY include: * error
// * advice
// * ext
// * id
// * timestamp
unsubscribe: function(channel, subscription) {
if (channel instanceof Array)
return array.map(channel, function(c) {
return this.unsubscribe(c, subscription);
}, this);
var dead = this._channels.unsubscribe(channel, subscription);
if (!dead) return;
this.connect(function() {
this.info('Client ? attempting to unsubscribe from ?', this._dispatcher.clientId, channel);
this._sendMessage({
channel: Channel.UNSUBSCRIBE,
clientId: this._dispatcher.clientId,
subscription: channel
}, {}, function(response) {
if (!response.successful) return;
var channels = [].concat(response.subscription);
this.info('Unsubscription acknowledged for ? from ?', this._dispatcher.clientId, channels);
}, this);
}, this);
},
// Request Response
// MUST include: * channel MUST include: * channel
// * data * successful
// MAY include: * clientId MAY include: * id
// * id * error
// * ext * ext
publish: function(channel, data, options) {
validateOptions(options || {}, ['attempts', 'deadline']);
var publication = new Publication();
this.connect(function() {
this.info('Client ? queueing published message to ?: ?', this._dispatcher.clientId, channel, data);
this._sendMessage({
channel: channel,
data: data,
clientId: this._dispatcher.clientId
}, options, function(response) {
if (response.successful)
publication.setDeferredStatus('succeeded');
else
publication.setDeferredStatus('failed', Error.parse(response.error));
}, this);
}, this);
return publication;
},
_sendMessage: function(message, options, callback, context) {
message.id = this._generateMessageId();
var timeout = this._advice.timeout
? 1.2 * this._advice.timeout / 1000
: 1.2 * this._dispatcher.retry;
this.pipeThroughExtensions('outgoing', message, null, function(message) {
if (!message) return;
if (callback) this._responseCallbacks[message.id] = [callback, context];
this._dispatcher.sendMessage(message, timeout, options || {});
}, this);
},
_generateMessageId: function() {
this._messageId += 1;
if (this._messageId >= Math.pow(2,32)) this._messageId = 0;
return this._messageId.toString(36);
},
_receiveMessage: function(message) {
var id = message.id, callback;
if (message.successful !== undefined) {
callback = this._responseCallbacks[id];
delete this._responseCallbacks[id];
}
this.pipeThroughExtensions('incoming', message, null, function(message) {
if (!message) return;
if (message.advice) this._handleAdvice(message.advice);
this._deliverMessage(message);
if (callback) callback[0].call(callback[1], message);
}, this);
},
_handleAdvice: function(advice) {
assign(this._advice, advice);
this._dispatcher.timeout = this._advice.timeout / 1000;
if (this._advice.reconnect === this.HANDSHAKE && this._state !== this.DISCONNECTED) {
this._state = this.UNCONNECTED;
this._dispatcher.clientId = null;
this._cycleConnection();
}
},
_deliverMessage: function(message) {
if (!message.channel || message.data === undefined) return;
this.info('Client ? calling listeners for ? with ?', this._dispatcher.clientId, message.channel, message.data);
this._channels.distributeMessage(message);
},
_cycleConnection: function() {
if (this._connectRequest) {
this._connectRequest = null;
this.info('Closed connection for ?', this._dispatcher.clientId);
}
var self = this;
global.setTimeout(function() { self.connect() }, this._advice.interval);
}
});
assign(Client.prototype, Deferrable);
assign(Client.prototype, Publisher);
assign(Client.prototype, Logging);
assign(Client.prototype, Extensible);
module.exports = Client;