vector-im-hydrogen-web/src/matrix/SessionContainer.js

238 lines
8.0 KiB
JavaScript
Raw Normal View History

import {createEnum} from "../utils/enum.js";
import {ObservableValue} from "../observable/ObservableValue.js";
import {HomeServerApi} from "./net/HomeServerApi.js";
import {Reconnector, ConnectionStatus} from "./net/Reconnector.js";
import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay.js";
import {HomeServerError, ConnectionError, AbortError} from "./error.js";
import {Sync, SyncStatus} from "./Sync.js";
import {Session} from "./Session.js";
2020-04-09 23:19:49 +02:00
2020-04-18 19:16:16 +02:00
export const LoadStatus = createEnum(
"NotLoading",
"Login",
"LoginFailed",
2020-04-09 23:19:49 +02:00
"Loading",
"Migrating", //not used atm, but would fit here
"FirstSync",
2020-04-09 23:19:49 +02:00
"Error",
"Ready",
);
2020-04-18 19:16:16 +02:00
export const LoginFailure = createEnum(
"Connection",
2020-04-18 19:16:16 +02:00
"Credentials",
"Unknown",
);
export class SessionContainer {
constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage}) {
2020-04-18 19:16:16 +02:00
this._random = random;
this._clock = clock;
this._onlineStatus = onlineStatus;
this._request = request;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
2020-04-18 19:16:16 +02:00
this._status = new ObservableValue(LoadStatus.NotLoading);
this._error = null;
this._loginFailure = null;
this._reconnector = null;
this._session = null;
this._sync = null;
this._sessionId = null;
this._storage = null;
2020-04-09 23:19:49 +02:00
}
createNewSessionId() {
2020-04-18 19:16:16 +02:00
return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString();
2020-04-09 23:19:49 +02:00
}
2020-04-18 19:16:16 +02:00
async startWithExistingSession(sessionId) {
if (this._status.get() !== LoadStatus.NotLoading) {
return;
}
this._status.set(LoadStatus.Loading);
try {
const sessionInfo = await this._sessionInfoStorage.get(sessionId);
if (!sessionInfo) {
throw new Error("Invalid session id: " + sessionId);
}
2020-04-18 19:16:16 +02:00
await this._loadSessionInfo(sessionInfo);
} catch (err) {
this._error = err;
this._status.set(LoadStatus.Error);
}
2020-04-09 23:19:49 +02:00
}
2020-04-18 19:16:16 +02:00
async startWithLogin(homeServer, username, password) {
if (this._status.get() !== LoadStatus.NotLoading) {
return;
}
this._status.set(LoadStatus.Login);
let sessionInfo;
try {
const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout});
2020-04-18 19:16:16 +02:00
const loginData = await hsApi.passwordLogin(username, password).response();
const sessionId = this.createNewSessionId();
2020-04-18 19:16:16 +02:00
sessionInfo = {
id: sessionId,
deviceId: loginData.device_id,
userId: loginData.user_id,
homeServer: homeServer,
accessToken: loginData.access_token,
lastUsed: this._clock.now()
};
await this._sessionInfoStorage.add(sessionInfo);
2020-04-18 19:16:16 +02:00
} catch (err) {
this._error = err;
if (err instanceof HomeServerError) {
2020-04-22 20:49:21 +02:00
if (err.errcode === "M_FORBIDDEN") {
2020-04-18 19:16:16 +02:00
this._loginFailure = LoginFailure.Credentials;
} else {
this._loginFailure = LoginFailure.Unknown;
}
this._status.set(LoadStatus.LoginFailed);
2020-04-19 19:05:12 +02:00
} else if (err instanceof ConnectionError) {
this._loginFailure = LoginFailure.Connection;
2020-04-18 19:16:16 +02:00
this._status.set(LoadStatus.LoginFailure);
} else {
this._status.set(LoadStatus.Error);
}
return;
}
// loading the session can only lead to
// LoadStatus.Error in case of an error,
// so separate try/catch
try {
await this._loadSessionInfo(sessionInfo);
} catch (err) {
this._error = err;
this._status.set(LoadStatus.Error);
2020-04-09 23:19:49 +02:00
}
}
2020-04-18 19:16:16 +02:00
async _loadSessionInfo(sessionInfo) {
this._status.set(LoadStatus.Loading);
this._reconnector = new Reconnector({
onlineStatus: this._onlineStatus,
retryDelay: new ExponentialRetryDelay(this._clock.createTimeout),
2020-04-18 19:16:16 +02:00
createMeasure: this._clock.createMeasure
});
const hsApi = new HomeServerApi({
homeServer: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken,
request: this._request,
reconnector: this._reconnector,
createTimeout: this._clock.createTimeout
2020-04-18 19:16:16 +02:00
});
this._sessionId = sessionInfo.id;
this._storage = await this._storageFactory.create(sessionInfo.id);
2020-04-18 19:16:16 +02:00
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
userId: sessionInfo.userId,
homeServer: sessionInfo.homeServer,
};
this._session = new Session({storage: this._storage, sessionInfo: filteredSessionInfo, hsApi});
2020-04-18 19:16:16 +02:00
await this._session.load();
this._sync = new Sync({hsApi, storage: this._storage, session: this._session});
2020-04-18 19:16:16 +02:00
// notify sync and session when back online
this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => {
if (state === ConnectionStatus.Online) {
this._sync.start();
this._session.start(this._reconnector.lastVersionsResponse);
}
});
2020-04-19 19:02:10 +02:00
await this._waitForFirstSync();
2020-04-19 19:02:10 +02:00
this._status.set(LoadStatus.Ready);
// if the sync failed, and then the reconnector
// restored the connection, it would have already
// started to session, so check first
// to prevent an extra /versions request
if (!this._session.isStarted) {
const lastVersionsResponse = await hsApi.versions({timeout: 10000}).response();
this._session.start(lastVersionsResponse);
}
2020-04-19 19:02:10 +02:00
}
async _waitForFirstSync() {
2020-04-18 19:16:16 +02:00
try {
this._sync.start();
2020-05-04 19:38:23 +02:00
this._status.set(LoadStatus.FirstSync);
2020-04-18 19:16:16 +02:00
} catch (err) {
2020-04-19 19:05:12 +02:00
// swallow ConnectionError here and continue,
2020-04-18 19:16:16 +02:00
// as the reconnector above will call
// sync.start again to retry in this case
2020-04-19 19:05:12 +02:00
if (!(err instanceof ConnectionError)) {
2020-04-18 19:16:16 +02:00
throw err;
}
}
// only transition into Ready once the first sync has succeeded
2020-04-19 19:02:10 +02:00
this._waitForFirstSyncHandle = this._sync.status.waitFor(s => s === SyncStatus.Syncing);
try {
await this._waitForFirstSyncHandle.promise;
} catch (err) {
// if dispose is called from stop, bail out
if (err instanceof AbortError) {
return;
}
throw err;
} finally {
this._waitForFirstSyncHandle = null;
}
2020-04-18 19:16:16 +02:00
}
get loadStatus() {
return this._status;
}
get loadError() {
return this._error;
}
/** only set at loadStatus InitialSync, CatchupSync or Ready */
2020-04-09 23:19:49 +02:00
get sync() {
return this._sync;
}
2020-04-18 19:16:16 +02:00
/** only set at loadStatus InitialSync, CatchupSync or Ready */
2020-04-09 23:19:49 +02:00
get session() {
return this._session;
}
get reconnector() {
return this._reconnector;
}
2020-04-18 19:16:16 +02:00
stop() {
this._reconnectSubscription();
this._reconnectSubscription = null;
this._sync.stop();
this._session.stop();
2020-04-19 19:02:10 +02:00
if (this._waitForFirstSyncHandle) {
this._waitForFirstSyncHandle.dispose();
this._waitForFirstSyncHandle = null;
}
if (this._storage) {
this._storage.close();
this._storage = null;
}
}
async deleteSession() {
if (this._sessionId) {
// if one fails, don't block the other from trying
// also, run in parallel
await Promise.all([
this._storageFactory.delete(this._sessionId),
this._sessionInfoStorage.delete(this._sessionId),
]);
this._sessionId = null;
}
2020-04-09 23:19:49 +02:00
}
2020-04-18 19:16:16 +02:00
}