hydrogen-web/src/matrix/SessionContainer.js

315 lines
11 KiB
JavaScript
Raw Normal View History

2020-08-05 22:08:55 +05:30
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
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 {MediaRepository} from "./net/MediaRepository.js";
import {RequestScheduler} from "./net/RequestScheduler.js";
import {HomeServerError, ConnectionError, AbortError} from "./error.js";
import {Sync, SyncStatus} from "./Sync.js";
import {Session} from "./Session.js";
2020-04-10 02:49:49 +05:30
2020-04-18 22:46:16 +05:30
export const LoadStatus = createEnum(
"NotLoading",
"Login",
"LoginFailed",
2020-04-10 02:49:49 +05:30
"Loading",
"SessionSetup", // upload e2ee keys, ...
2020-04-10 02:49:49 +05:30
"Migrating", //not used atm, but would fit here
"FirstSync",
2020-04-10 02:49:49 +05:30
"Error",
"Ready",
);
2020-04-18 22:46:16 +05:30
export const LoginFailure = createEnum(
"Connection",
2020-04-18 22:46:16 +05:30
"Credentials",
"Unknown",
);
export class SessionContainer {
constructor({platform, olmPromise, workerPromise}) {
this._platform = platform;
this._sessionStartedByReconnector = false;
2020-04-18 22:46:16 +05:30
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;
this._requestScheduler = null;
2020-08-27 16:54:55 +05:30
this._olmPromise = olmPromise;
this._workerPromise = workerPromise;
2020-04-10 02:49:49 +05:30
}
createNewSessionId() {
return (Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)).toString();
2020-04-10 02:49:49 +05:30
}
get sessionId() {
return this._sessionId;
}
2020-04-18 22:46:16 +05:30
async startWithExistingSession(sessionId) {
if (this._status.get() !== LoadStatus.NotLoading) {
return;
}
this._status.set(LoadStatus.Loading);
await this._platform.logger.run("load session", async log => {
2021-02-23 23:52:25 +05:30
log.set("id", sessionId);
try {
const sessionInfo = await this._platform.sessionInfoStorage.get(sessionId);
if (!sessionInfo) {
throw new Error("Invalid session id: " + sessionId);
}
await this._loadSessionInfo(sessionInfo, false, log);
log.set("status", this._status.get());
} catch (err) {
log.catch(err);
this._error = err;
this._status.set(LoadStatus.Error);
}
2021-02-23 23:52:25 +05:30
});
2020-04-10 02:49:49 +05:30
}
2020-04-18 22:46:16 +05:30
async startWithLogin(homeServer, username, password) {
if (this._status.get() !== LoadStatus.NotLoading) {
return;
}
await this._platform.logger.run("login", async log => {
2021-02-23 23:52:25 +05:30
this._status.set(LoadStatus.Login);
const clock = this._platform.clock;
let sessionInfo;
try {
const request = this._platform.request;
const hsApi = new HomeServerApi({homeServer, request, createTimeout: clock.createTimeout});
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen", {log}).response();
const sessionId = this.createNewSessionId();
sessionInfo = {
id: sessionId,
deviceId: loginData.device_id,
userId: loginData.user_id,
homeServer: homeServer,
accessToken: loginData.access_token,
lastUsed: clock.now()
};
log.set("id", sessionId);
await this._platform.sessionInfoStorage.add(sessionInfo);
} catch (err) {
this._error = err;
if (err instanceof HomeServerError) {
if (err.errcode === "M_FORBIDDEN") {
this._loginFailure = LoginFailure.Credentials;
} else {
this._loginFailure = LoginFailure.Unknown;
}
log.set("loginFailure", this._loginFailure);
this._status.set(LoadStatus.LoginFailed);
} else if (err instanceof ConnectionError) {
this._loginFailure = LoginFailure.Connection;
this._status.set(LoadStatus.LoginFailed);
2020-04-18 22:46:16 +05:30
} else {
2021-02-23 23:52:25 +05:30
this._status.set(LoadStatus.Error);
2020-04-18 22:46:16 +05:30
}
2021-02-23 23:52:25 +05:30
return;
}
// loading the session can only lead to
// LoadStatus.Error in case of an error,
// so separate try/catch
try {
await this._loadSessionInfo(sessionInfo, true, log);
log.set("status", this._status.get());
} catch (err) {
log.catch(err);
this._error = err;
2020-04-18 22:46:16 +05:30
this._status.set(LoadStatus.Error);
}
2021-02-23 23:52:25 +05:30
});
2020-04-10 02:49:49 +05:30
}
2021-02-23 23:52:25 +05:30
async _loadSessionInfo(sessionInfo, isNewLogin, log) {
const clock = this._platform.clock;
this._sessionStartedByReconnector = false;
2020-04-18 22:46:16 +05:30
this._status.set(LoadStatus.Loading);
this._reconnector = new Reconnector({
onlineStatus: this._platform.onlineStatus,
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
createMeasure: clock.createMeasure
2020-04-18 22:46:16 +05:30
});
const hsApi = new HomeServerApi({
homeServer: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken,
request: this._platform.request,
2020-04-18 22:46:16 +05:30
reconnector: this._reconnector,
createTimeout: clock.createTimeout
2020-04-18 22:46:16 +05:30
});
this._sessionId = sessionInfo.id;
this._storage = await this._platform.storageFactory.create(sessionInfo.id);
2020-04-18 22:46:16 +05:30
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
userId: sessionInfo.userId,
homeServer: sessionInfo.homeServer,
};
2020-08-27 16:54:55 +05:30
const olm = await this._olmPromise;
2020-09-11 14:13:17 +05:30
let olmWorker = null;
if (this._workerPromise) {
2020-09-11 14:13:17 +05:30
olmWorker = await this._workerPromise;
}
this._requestScheduler = new RequestScheduler({hsApi, clock});
this._requestScheduler.start();
const mediaRepository = new MediaRepository({
homeServer: sessionInfo.homeServer,
platform: this._platform,
});
this._session = new Session({
storage: this._storage,
sessionInfo: filteredSessionInfo,
hsApi: this._requestScheduler.hsApi,
olm,
olmWorker,
mediaRepository,
platform: this._platform,
});
2021-02-23 23:52:25 +05:30
await this._session.load(log);
if (isNewLogin) {
this._status.set(LoadStatus.SessionSetup);
2021-02-23 23:52:25 +05:30
await log.wrap("createIdentity", log => this._session.createIdentity(log));
}
2020-04-18 22:46:16 +05:30
2021-02-12 23:26:26 +05:30
this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session, logger: this._platform.logger});
2020-04-18 22:46:16 +05:30
// notify sync and session when back online
this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => {
if (state === ConnectionStatus.Online) {
2021-02-23 23:52:25 +05:30
this._platform.logger.runDetached("reconnect", async log => {
// needs to happen before sync and session or it would abort all requests
this._requestScheduler.start();
this._sync.start();
this._sessionStartedByReconnector = true;
await log.wrap("session start", log => this._session.start(this._reconnector.lastVersionsResponse, log));
});
2020-04-18 22:46:16 +05:30
}
});
2021-02-24 14:44:26 +05:30
await log.wrap("wait first sync", () => this._waitForFirstSync());
2020-04-19 22:32:10 +05:30
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._sessionStartedByReconnector) {
2021-02-23 23:52:25 +05:30
const lastVersionsResponse = await hsApi.versions({timeout: 10000, log}).response();
// log as ref as we don't want to await it
await log.wrap("session start", log => this._session.start(lastVersionsResponse, log));
}
2020-04-19 22:32:10 +05:30
}
2021-02-24 14:44:26 +05:30
async _waitForFirstSync() {
2020-04-18 22:46:16 +05:30
try {
this._sync.start();
2020-05-04 23:08:23 +05:30
this._status.set(LoadStatus.FirstSync);
2020-04-18 22:46:16 +05:30
} catch (err) {
2020-04-19 22:35:12 +05:30
// swallow ConnectionError here and continue,
2020-04-18 22:46:16 +05:30
// as the reconnector above will call
// sync.start again to retry in this case
2020-04-19 22:35:12 +05:30
if (!(err instanceof ConnectionError)) {
2020-04-18 22:46:16 +05:30
throw err;
}
}
// only transition into Ready once the first sync has succeeded
2020-06-27 02:56:24 +05:30
this._waitForFirstSyncHandle = this._sync.status.waitFor(s => s === SyncStatus.Syncing || s === SyncStatus.Stopped);
2020-04-19 22:32:10 +05:30
try {
await this._waitForFirstSyncHandle.promise;
2020-06-27 02:56:24 +05:30
if (this._sync.status.get() === SyncStatus.Stopped) {
throw this._sync.error;
2020-06-27 02:56:24 +05:30
}
2020-04-19 22:32:10 +05:30
} catch (err) {
// if dispose is called from stop, bail out
if (err instanceof AbortError) {
return;
}
throw err;
} finally {
this._waitForFirstSyncHandle = null;
}
2020-04-18 22:46:16 +05:30
}
get loadStatus() {
return this._status;
}
get loadError() {
return this._error;
}
/** only set at loadStatus InitialSync, CatchupSync or Ready */
2020-04-10 02:49:49 +05:30
get sync() {
return this._sync;
}
2020-04-18 22:46:16 +05:30
/** only set at loadStatus InitialSync, CatchupSync or Ready */
2020-04-10 02:49:49 +05:30
get session() {
return this._session;
}
get reconnector() {
return this._reconnector;
}
2020-09-18 16:41:10 +05:30
dispose() {
if (this._reconnectSubscription) {
this._reconnectSubscription();
this._reconnectSubscription = null;
}
if (this._requestScheduler) {
this._requestScheduler.stop();
}
if (this._sync) {
this._sync.stop();
}
if (this._session) {
2020-09-18 16:41:10 +05:30
this._session.dispose();
}
2020-04-19 22:32:10 +05:30
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._platform.storageFactory.delete(this._sessionId),
this._platform.sessionInfoStorage.delete(this._sessionId),
]);
this._sessionId = null;
}
2020-04-10 02:49:49 +05:30
}
2020-04-18 22:46:16 +05:30
}