faye
Version:
Simple pub/sub messaging for the web
162 lines (127 loc) • 4.58 kB
JavaScript
;
var Class = require('../util/class'),
Promise = require('../util/promise'),
Set = require('../util/set'),
URI = require('../util/uri'),
browser = require('../util/browser'),
copyObject = require('../util/copy_object'),
assign = require('../util/assign'),
toJSON = require('../util/to_json'),
ws = require('../util/websocket'),
Deferrable = require('../mixins/deferrable'),
Transport = require('./transport');
var WebSocket = assign(Class(Transport, {
UNCONNECTED: 1,
CONNECTING: 2,
CONNECTED: 3,
batching: false,
isUsable: function(callback, context) {
this.callback(function() { callback.call(context, true) });
this.errback(function() { callback.call(context, false) });
this.connect();
},
request: function(messages) {
this._pending = this._pending || new Set();
for (var i = 0, n = messages.length; i < n; i++) this._pending.add(messages[i]);
var self = this;
var promise = new Promise(function(resolve, reject) {
self.callback(function(socket) {
if (!socket || socket.readyState !== 1) return;
socket.send(toJSON(messages));
resolve(socket);
});
self.connect();
});
return {
abort: function() { promise.then(function(ws) { ws.close() }) }
};
},
connect: function() {
if (WebSocket._unloaded) return;
this._state = this._state || this.UNCONNECTED;
if (this._state !== this.UNCONNECTED) return;
this._state = this.CONNECTING;
var socket = this._createSocket();
if (!socket) return this.setDeferredStatus('failed');
var self = this;
socket.onopen = function() {
if (socket.headers) self._storeCookies(socket.headers['set-cookie']);
self._socket = socket;
self._state = self.CONNECTED;
self._everConnected = true;
self.setDeferredStatus('succeeded', socket);
};
var closed = false;
socket.onclose = socket.onerror = function() {
if (closed) return;
closed = true;
var wasConnected = (self._state === self.CONNECTED);
socket.onopen = socket.onclose = socket.onerror = socket.onmessage = null;
delete self._socket;
self._state = self.UNCONNECTED;
var pending = self._pending ? self._pending.toArray() : [];
delete self._pending;
if (wasConnected || self._everConnected) {
self.setDeferredStatus('unknown');
self._handleError(pending, wasConnected);
} else {
self.setDeferredStatus('failed');
}
};
socket.onmessage = function(event) {
var replies;
try { replies = JSON.parse(event.data) } catch (error) {}
if (!replies) return;
replies = [].concat(replies);
for (var i = 0, n = replies.length; i < n; i++) {
if (replies[i].successful === undefined) continue;
self._pending.remove(replies[i]);
}
self._receive(replies);
};
},
close: function() {
if (!this._socket) return;
this._socket.close();
},
_createSocket: function() {
var url = WebSocket.getSocketUrl(this.endpoint),
headers = this._dispatcher.headers,
extensions = this._dispatcher.wsExtensions,
cookie = this._getCookies(),
tls = this._dispatcher.tls,
options = { extensions: extensions, headers: headers, proxy: this._proxy, tls: tls };
if (cookie !== '') options.headers['Cookie'] = cookie;
try {
return ws.create(url, [], options);
} catch (e) {
// catch CSP error to allow transport to fallback to next connType
}
}
}), {
PROTOCOLS: {
'http:': 'ws:',
'https:': 'wss:'
},
create: function(dispatcher, endpoint) {
var sockets = dispatcher.transports.websocket = dispatcher.transports.websocket || {};
sockets[endpoint.href] = sockets[endpoint.href] || new this(dispatcher, endpoint);
return sockets[endpoint.href];
},
getSocketUrl: function(endpoint) {
endpoint = copyObject(endpoint);
endpoint.protocol = this.PROTOCOLS[endpoint.protocol];
return URI.stringify(endpoint);
},
isUsable: function(dispatcher, endpoint, callback, context) {
this.create(dispatcher, endpoint).isUsable(callback, context);
}
});
assign(WebSocket.prototype, Deferrable);
if (browser.Event && global.onbeforeunload !== undefined) {
browser.Event.on(global, 'beforeunload', function() {
if (WebSocket._unloaded === undefined)
WebSocket._unloaded = true;
});
}
module.exports = WebSocket;