WIP
This commit is contained in:
parent
65cca83f7f
commit
b6a5a02a33
2 changed files with 174 additions and 5 deletions
164
src/matrix/Reconnecter.js
Normal file
164
src/matrix/Reconnecter.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue