UNPKG

faye

Version:

Simple pub/sub messaging for the web

386 lines (313 loc) 14.2 kB
'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;