This repository has been archived on 2022-08-19. You can view files and clone it, but cannot push or open issues or pull requests.
hydrogen-web/src/matrix/Reconnector.js
Bruno Windels 1f15ca6498 more WIP
2020-04-18 19:16:16 +02:00

170 lines
4.7 KiB
JavaScript

export class ExponentialRetryDelay {
constructor(start = 2000, createTimeout) {
this._start = start;
this._current = start;
this._createTimeout = createTimeout;
this._max = 60 * 5 * 1000; //5 min
this._timeout = null;
}
async waitForRetry() {
this._timeout = this._createTimeout(this._current);
try {
await this._timeout.elapsed();
// only increase delay if we didn't get interrupted
const seconds = this._current / 1000;
const powerOfTwo = (seconds * seconds) * 1000;
this._current = Math.max(this._max, powerOfTwo);
} catch(err) {
// swallow AbortError, means abort was called
if (!(err instanceof AbortError)) {
throw err;
}
} finally {
this._timeout = null;
}
}
abort() {
if (this._timeout) {
this._timeout.abort();
}
}
reset() {
this._current = this._start;
this.abort();
}
get nextValue() {
return this._current;
}
}
// we need a clock interface that gives us both timestamps and a timer that we can interrupt?
// state
// - offline
// - waiting to reconnect
// - reconnecting
// - online
//
//
function createEnum(...values) {
const obj = {};
for (const value of values) {
obj[value] = value;
}
return Object.freeze(obj);
}
export const ConnectionState = createEnum(
"Offline",
"Waiting",
"Reconnecting",
"Online"
);
export class Reconnector {
constructor({retryDelay, createTimeMeasure, isOnline}) {
this._isOnline = isOnline;
this._retryDelay = retryDelay;
this._createTimeMeasure = createTimeMeasure;
// assume online, and do our thing when something fails
this._state = new ObservableValue(ConnectionState.Online);
this._isReconnecting = false;
this._versionsResponse = null;
}
get lastVersionsResponse() {
return this._versionsResponse;
}
get connectionState() {
return this._state;
}
get retryIn() {
if (this._state.get() === ConnectionState.Waiting) {
return this._retryDelay.nextValue - this._stateSince.measure();
}
return 0;
}
async onRequestFailed(hsApi) {
if (!this._isReconnecting) {
this._setState(ConnectionState.Offline);
const isOnlineSubscription = this._isOnline && this._isOnline.subscribe(online => {
if (online) {
this.tryNow();
}
});
try {
await this._reconnectLoop(hsApi);
} finally {
if (isOnlineSubscription) {
// unsubscribe from this._isOnline
isOnlineSubscription();
}
}
}
}
tryNow() {
if (this._retryDelay) {
// this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop
this._retryDelay.abort();
}
}
_setState(state) {
if (state !== this._state.get()) {
if (state === ConnectionState.Waiting) {
this._stateSince = this._createTimeMeasure();
} else {
this._stateSince = null;
}
this._state.set(state);
}
}
async _reconnectLoop(hsApi) {
this._isReconnecting = true;
this._versionsResponse = null;
this._retryDelay.reset();
try {
while (!this._versionsResponse) {
try {
this._setState(ConnectionState.Reconnecting);
// use 10s timeout, because we don't want to be waiting for
// a stale connection when we just came online again
const versionsRequest = hsApi.versions({timeout: 10000});
this._versionsResponse = await versionsRequest.response();
this._setState(ConnectionState.Online);
} catch (err) {
if (err instanceof NetworkError) {
this._setState(ConnectionState.Waiting);
try {
await this._retryDelay.waitForRetry();
} catch (err) {
if (!(err instanceof AbortError)) {
throw err;
}
}
} else {
throw err;
}
}
}
} catch (err) {
// nothing is catching the error above us,
// so just log here
console.err(err);
}
}
}