mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
218 lines
6.2 KiB
JavaScript
218 lines
6.2 KiB
JavaScript
import {
|
|
toSockjsUrl,
|
|
toWebsocketUrl,
|
|
} from "./urls.js";
|
|
|
|
import { StreamClientCommon } from "./common.js";
|
|
|
|
// Statically importing SockJS here will prevent native WebSocket usage
|
|
// below (in favor of SockJS), but will ensure maximum compatibility for
|
|
// clients stuck in unusual networking environments.
|
|
import SockJS from "./sockjs-1.6.1-min-.js";
|
|
|
|
export class ClientStream extends StreamClientCommon {
|
|
// @param url {String} URL to Meteor app
|
|
// "http://subdomain.meteor.com/" or "/" or
|
|
// "ddp+sockjs://foo-**.meteor.com/sockjs"
|
|
constructor(url, options) {
|
|
super(options);
|
|
|
|
this._initCommon(this.options);
|
|
|
|
//// Constants
|
|
|
|
// how long between hearing heartbeat from the server until we declare
|
|
// the connection dead. heartbeats come every 45s (stream_server.js)
|
|
//
|
|
// NOTE: this is a older timeout mechanism. We now send heartbeats at
|
|
// the DDP level (https://github.com/meteor/meteor/pull/1865), and
|
|
// expect those timeouts to kill a non-responsive connection before
|
|
// this timeout fires. This is kept around for compatibility (when
|
|
// talking to a server that doesn't support DDP heartbeats) and can be
|
|
// removed later.
|
|
this.HEARTBEAT_TIMEOUT = 100 * 1000;
|
|
|
|
this.rawUrl = url;
|
|
this.socket = null;
|
|
this.lastError = null;
|
|
|
|
this.heartbeatTimer = null;
|
|
|
|
// Listen to global 'online' event if we are running in a browser.
|
|
window.addEventListener(
|
|
'online',
|
|
this._online.bind(this),
|
|
false /* useCapture */
|
|
);
|
|
|
|
//// Kickoff!
|
|
this._launchConnection();
|
|
}
|
|
|
|
// data is a utf8 string. Data sent while not connected is dropped on
|
|
// the floor, and it is up the user of this API to retransmit lost
|
|
// messages on 'reset'
|
|
send(data) {
|
|
if (this.currentStatus.connected) {
|
|
this.socket.send(data);
|
|
}
|
|
}
|
|
|
|
// Changes where this connection points
|
|
_changeUrl(url) {
|
|
this.rawUrl = url;
|
|
}
|
|
|
|
_connected() {
|
|
if (this.connectionTimer) {
|
|
clearTimeout(this.connectionTimer);
|
|
this.connectionTimer = null;
|
|
}
|
|
|
|
if (this.currentStatus.connected) {
|
|
// already connected. do nothing. this probably shouldn't happen.
|
|
return;
|
|
}
|
|
|
|
// update status
|
|
this.currentStatus.status = 'connected';
|
|
this.currentStatus.connected = true;
|
|
this.currentStatus.retryCount = 0;
|
|
this.statusChanged();
|
|
|
|
// fire resets. This must come after status change so that clients
|
|
// can call send from within a reset callback.
|
|
this.forEachCallback('reset', callback => {
|
|
callback();
|
|
});
|
|
}
|
|
|
|
_cleanup(maybeError) {
|
|
this._clearConnectionAndHeartbeatTimers();
|
|
if (this.socket) {
|
|
this.socket.onmessage = this.socket.onclose = this.socket.onerror = this.socket.onheartbeat = () => {};
|
|
this.socket.close();
|
|
this.socket = null;
|
|
}
|
|
|
|
this.forEachCallback('disconnect', callback => {
|
|
callback(maybeError);
|
|
});
|
|
}
|
|
|
|
_clearConnectionAndHeartbeatTimers() {
|
|
if (this.connectionTimer) {
|
|
clearTimeout(this.connectionTimer);
|
|
this.connectionTimer = null;
|
|
}
|
|
if (this.heartbeatTimer) {
|
|
clearTimeout(this.heartbeatTimer);
|
|
this.heartbeatTimer = null;
|
|
}
|
|
}
|
|
|
|
_heartbeat_timeout() {
|
|
console.log('Connection timeout. No sockjs heartbeat received.');
|
|
this._lostConnection(new this.ConnectionError("Heartbeat timed out"));
|
|
}
|
|
|
|
_heartbeat_received() {
|
|
// If we've already permanently shut down this stream, the timeout is
|
|
// already cleared, and we don't need to set it again.
|
|
if (this._forcedToDisconnect) return;
|
|
if (this.heartbeatTimer) clearTimeout(this.heartbeatTimer);
|
|
this.heartbeatTimer = setTimeout(
|
|
this._heartbeat_timeout.bind(this),
|
|
this.HEARTBEAT_TIMEOUT
|
|
);
|
|
}
|
|
|
|
_sockjsProtocolsWhitelist() {
|
|
// only allow polling protocols. no streaming. streaming
|
|
// makes safari spin.
|
|
var protocolsWhitelist = [
|
|
'xdr-polling',
|
|
'xhr-polling',
|
|
'iframe-xhr-polling',
|
|
'jsonp-polling'
|
|
];
|
|
|
|
// iOS 4 and 5 and below crash when using websockets over certain
|
|
// proxies. this seems to be resolved with iOS 6. eg
|
|
// https://github.com/LearnBoost/socket.io/issues/193#issuecomment-7308865.
|
|
//
|
|
// iOS <4 doesn't support websockets at all so sockjs will just
|
|
// immediately fall back to http
|
|
var noWebsockets =
|
|
navigator &&
|
|
/iPhone|iPad|iPod/.test(navigator.userAgent) &&
|
|
/OS 4_|OS 5_/.test(navigator.userAgent);
|
|
|
|
if (!noWebsockets)
|
|
protocolsWhitelist = ['websocket'].concat(protocolsWhitelist);
|
|
|
|
return protocolsWhitelist;
|
|
}
|
|
|
|
_launchConnection() {
|
|
this._cleanup(); // cleanup the old socket, if there was one.
|
|
|
|
var options = {
|
|
transports: this._sockjsProtocolsWhitelist(),
|
|
...this.options._sockjsOptions
|
|
};
|
|
|
|
const hasSockJS = typeof SockJS === "function";
|
|
const disableSockJS = __meteor_runtime_config__.DISABLE_SOCKJS;
|
|
|
|
this.socket = hasSockJS && !disableSockJS
|
|
// Convert raw URL to SockJS URL each time we open a connection, so
|
|
// that we can connect to random hostnames and get around browser
|
|
// per-host connection limits.
|
|
? new SockJS(toSockjsUrl(this.rawUrl), undefined, options)
|
|
: new WebSocket(toWebsocketUrl(this.rawUrl));
|
|
|
|
this.socket.onopen = data => {
|
|
this.lastError = null;
|
|
this._connected();
|
|
};
|
|
|
|
this.socket.onmessage = data => {
|
|
this.lastError = null;
|
|
this._heartbeat_received();
|
|
if (this.currentStatus.connected) {
|
|
this.forEachCallback('message', callback => {
|
|
callback(data.data);
|
|
});
|
|
}
|
|
};
|
|
|
|
this.socket.onclose = () => {
|
|
this._lostConnection();
|
|
};
|
|
|
|
this.socket.onerror = error => {
|
|
const { lastError } = this;
|
|
this.lastError = error;
|
|
if (lastError) return;
|
|
console.error(
|
|
'stream error',
|
|
error,
|
|
new Date().toDateString()
|
|
);
|
|
};
|
|
|
|
this.socket.onheartbeat = () => {
|
|
this.lastError = null;
|
|
this._heartbeat_received();
|
|
};
|
|
|
|
if (this.connectionTimer) clearTimeout(this.connectionTimer);
|
|
this.connectionTimer = setTimeout(() => {
|
|
this._lostConnection(
|
|
new this.ConnectionError("DDP connection timed out")
|
|
);
|
|
}, this.CONNECT_TIMEOUT);
|
|
}
|
|
}
|