import Util from '../util';
import Runtime from 'runtime';
import Strategy from './strategy';
import SequentialStrategy from './sequential_strategy';
import StrategyOptions from './strategy_options';
import TransportStrategy from './transport_strategy';
import Timeline from '../timeline/timeline';
import * as Collections from '../utils/collections';
export interface TransportStrategyDictionary {
[key: string]: TransportStrategy;
}
/** Caches the last successful transport and, after the first few attempts,
* uses the cached transport for subsequent attempts.
*
* @param {Strategy} strategy
* @param {Object} transports
* @param {Object} options
*/
export default class WebSocketPrioritizedCachedStrategy implements Strategy {
strategy: Strategy;
transports: TransportStrategyDictionary;
ttl: number;
usingTLS: boolean;
timeline: Timeline;
constructor(
strategy: Strategy,
transports: TransportStrategyDictionary,
options: StrategyOptions
) {
this.strategy = strategy;
this.transports = transports;
this.ttl = options.ttl || 1800 * 1000;
this.usingTLS = options.useTLS;
this.timeline = options.timeline;
}
isSupported(): boolean {
return this.strategy.isSupported();
}
connect(minPriority: number, callback: Function) {
var usingTLS = this.usingTLS;
var info = fetchTransportCache(usingTLS);
var cacheSkipCount = info && info.cacheSkipCount ? info.cacheSkipCount : 0;
var strategies = [this.strategy];
if (info && info.timestamp + this.ttl >= Util.now()) {
var transport = this.transports[info.transport];
if (transport) {
if (['ws', 'wss'].includes(info.transport) || cacheSkipCount > 3) {
this.timeline.info({
cached: true,
transport: info.transport,
latency: info.latency
});
strategies.push(
new SequentialStrategy([transport], {
timeout: info.latency * 2 + 1000,
failFast: true
})
);
} else {
cacheSkipCount++;
}
}
}
var startTimestamp = Util.now();
var runner = strategies
.pop()
.connect(minPriority, function cb(error, handshake) {
if (error) {
flushTransportCache(usingTLS);
if (strategies.length > 0) {
startTimestamp = Util.now();
runner = strategies.pop().connect(minPriority, cb);
} else {
callback(error);
}
} else {
storeTransportCache(
usingTLS,
handshake.transport.name,
Util.now() - startTimestamp,
cacheSkipCount
);
callback(null, handshake);
}
});
return {
abort: function() {
runner.abort();
},
forceMinPriority: function(p) {
minPriority = p;
if (runner) {
runner.forceMinPriority(p);
}
}
};
}
}
function getTransportCacheKey(usingTLS: boolean): string {
return 'pusherTransport' + (usingTLS ? 'TLS' : 'NonTLS');
}
function fetchTransportCache(usingTLS: boolean): any {
var storage = Runtime.getLocalStorage();
if (storage) {
try {
var serializedCache = storage[getTransportCacheKey(usingTLS)];
if (serializedCache) {
return JSON.parse(serializedCache);
}
} catch (e) {
flushTransportCache(usingTLS);
}
}
return null;
}
function storeTransportCache(
usingTLS: boolean,
transport: TransportStrategy,
latency: number,
cacheSkipCount: number
) {
var storage = Runtime.getLocalStorage();
if (storage) {
try {
storage[getTransportCacheKey(usingTLS)] = Collections.safeJSONStringify({
timestamp: Util.now(),
transport: transport,
latency: latency,
cacheSkipCount: cacheSkipCount
});
} catch (e) {
// catch over quota exceptions raised by localStorage
}
}
}
function flushTransportCache(usingTLS: boolean) {
var storage = Runtime.getLocalStorage();
if (storage) {
try {
delete storage[getTransportCacheKey(usingTLS)];
} catch (e) {
// catch exceptions raised by localStorage
}
}
}
|