This commit is contained in:
Bruno Windels 2020-03-30 23:56:03 +02:00
parent 65cca83f7f
commit b6a5a02a33
2 changed files with 174 additions and 5 deletions

164
src/matrix/Reconnecter.js Normal file
View file

@ -0,0 +1,164 @@
class Clock {
// use cases
// StopWatch: not sure I like that name ... but measure time difference from start to current time
// Timeout: wait for a given number of ms, and be able to interrupt the wait
// Clock.timeout() -> creates a new timeout?
// Now: get current timestamp
// Clock.now(), or pass Clock.now so others can do now()
//
// should use subinterfaces so we can only pass part needed to other constructors
//
}
// need to prevent memory leaks here!
export class DomOnlineDetected {
constructor(reconnecter) {
// window.addEventListener('offline', () => appendOnlineStatus(false));
// window.addEventListener('online', () => appendOnlineStatus(true));
// appendOnlineStatus(navigator.onLine);
// on online, reconnecter.tryNow()
}
}
export class ExponentialRetryDelay {
constructor(start = 2000, delay) {
this._start = start;
this._current = start;
this._delay = delay;
this._max = 60 * 5 * 1000; //5 min
this._timer = null;
}
async waitForRetry() {
this._timer = this._delay(this._current);
try {
await this._timer.timeout();
// 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);
} finally {
this._timer = null;
}
}
reset() {
this._current = this._start;
if (this._timer) {
this._timer.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 Reconnecter {
constructor({hsApi, retryDelay, clock}) {
this._online
this._retryDelay = retryDelay;
this._currentDelay = null;
this._hsApi = hsApi;
this._clock = clock;
// assume online, and do our thing when something fails
this._state = ConnectionState.Online;
this._isReconnecting = false;
this._versionsResponse = null;
}
get lastVersionsResponse() {
return this._versionsResponse;
}
get state() {
return this._state;
}
get retryIn() {
return this._stateSince.measure();
}
onRequestFailed() {
if (!this._isReconnecting) {
this._setState(ConnectionState.Offline);
// do something with versions response of loop here?
// we might want to pass it to session to know what server supports?
// so emit it ...
this._reconnectLoop();
// start loop
}
}
// don't throw from here
tryNow() {
// skip waiting
if (this._currentDelay) {
this._currentDelay.abort();
}
}
_setState(state) {
if (state !== this._state) {
this._state = state;
if (this._state === ConnectionState.Waiting) {
this._stateSince = this._clock.stopwatch();
} else {
this._stateSince = null;
}
this.emit("change", state);
}
}
async _reconnectLoop() {
this._isReconnecting = true;
this._retryDelay.reset();
this._versionsResponse = null;
while (!this._versionsResponse) {
try {
this._setState(ConnectionState.Reconnecting);
const versionsRequest = this._hsApi.versions(10000);
this._versionsResponse = await versionsRequest.response();
this._setState(ConnectionState.Online);
} catch (err) {
this._setState(ConnectionState.Waiting);
this._currentDelay = this._retryDelay.next();
try {
await this._currentDelay
} catch (err) {
// waiting interrupted, we should retry immediately,
// swallow error
} finally {
this._currentDelay = null;
}
}
}
}
}

View file

@ -40,7 +40,7 @@ export default class HomeServerApi {
return `${this._homeserver}/_matrix/client/r0${csPath}`; return `${this._homeserver}/_matrix/client/r0${csPath}`;
} }
_request(method, csPath, queryParams = {}, body) { _request(method, url, queryParams = {}, body) {
const queryString = Object.entries(queryParams) const queryString = Object.entries(queryParams)
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
.map(([name, value]) => { .map(([name, value]) => {
@ -50,7 +50,7 @@ export default class HomeServerApi {
return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; return `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
}) })
.join("&"); .join("&");
const url = this._url(`${csPath}?${queryString}`); url = `${url}?${queryString}`;
let bodyString; let bodyString;
const headers = new Headers(); const headers = new Headers();
if (this._accessToken) { if (this._accessToken) {
@ -70,15 +70,15 @@ export default class HomeServerApi {
} }
_post(csPath, queryParams, body) { _post(csPath, queryParams, body) {
return this._request("POST", csPath, queryParams, body); return this._request("POST", this._url(csPath), queryParams, body);
} }
_put(csPath, queryParams, body) { _put(csPath, queryParams, body) {
return this._request("PUT", csPath, queryParams, body); return this._request("PUT", this._url(csPath), queryParams, body);
} }
_get(csPath, queryParams, body) { _get(csPath, queryParams, body) {
return this._request("GET", csPath, queryParams, body); return this._request("GET", this._url(csPath), queryParams, body);
} }
sync(since, filter, timeout) { sync(since, filter, timeout) {
@ -108,4 +108,9 @@ export default class HomeServerApi {
createFilter(userId, filter) { createFilter(userId, filter) {
return this._post(`/user/${encodeURIComponent(userId)}/filter`, undefined, filter); return this._post(`/user/${encodeURIComponent(userId)}/filter`, undefined, filter);
} }
versions(timeout) {
// TODO: implement timeout
return this._request("GET", `${this._homeserver}/_matrix/client/versions`);
}
} }