diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 27dad7cc..fcedb371 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -16,7 +16,7 @@ limitations under the License. import {ViewModel} from "../ViewModel.js"; import {createEnum} from "../../utils/enum"; -import {ConnectionStatus} from "../../matrix/net/Reconnector.js"; +import {ConnectionStatus} from "../../matrix/net/Reconnector"; import {SyncStatus} from "../../matrix/Sync.js"; const SessionStatus = createEnum( diff --git a/src/logging/types.ts b/src/logging/types.ts index ae001889..f4a20ee0 100644 --- a/src/logging/types.ts +++ b/src/logging/types.ts @@ -50,6 +50,8 @@ export interface ILogItem { ensureRefId(): void; catch(err: Error): Error; serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined; + finish(): void; + child(labelOrValues: LabelOrValues, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem; } export interface ILogger { diff --git a/src/matrix/SessionContainer.js b/src/matrix/SessionContainer.js index 44d7b3e4..677cde34 100644 --- a/src/matrix/SessionContainer.js +++ b/src/matrix/SessionContainer.js @@ -19,11 +19,11 @@ import {createEnum} from "../utils/enum"; import {lookupHomeserver} from "./well-known.js"; import {AbortableOperation} from "../utils/AbortableOperation"; import {ObservableValue} from "../observable/ObservableValue"; -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 {HomeServerApi} from "./net/HomeServerApi"; +import {Reconnector, ConnectionStatus} from "./net/Reconnector"; +import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay"; +import {MediaRepository} from "./net/MediaRepository"; +import {RequestScheduler} from "./net/RequestScheduler"; import {Sync, SyncStatus} from "./Sync.js"; import {Session} from "./Session.js"; import {PasswordLoginMethod} from "./login/PasswordLoginMethod"; diff --git a/src/matrix/net/ExponentialRetryDelay.js b/src/matrix/net/ExponentialRetryDelay.ts similarity index 86% rename from src/matrix/net/ExponentialRetryDelay.js rename to src/matrix/net/ExponentialRetryDelay.ts index 853f8758..a6a874f3 100644 --- a/src/matrix/net/ExponentialRetryDelay.js +++ b/src/matrix/net/ExponentialRetryDelay.ts @@ -15,18 +15,28 @@ limitations under the License. */ import {AbortError} from "../../utils/error"; +import type {Timeout} from "../../platform/web/dom/Clock.js"; + +type TimeoutCreator = (ms: number) => Timeout; + +const enum Default { start = 2000 } export class ExponentialRetryDelay { - constructor(createTimeout) { + private readonly _start: number = Default.start; + private _current: number = Default.start; + private readonly _createTimeout: TimeoutCreator; + private readonly _max: number; + private _timeout?: Timeout; + + constructor(createTimeout: TimeoutCreator) { const start = 2000; this._start = start; this._current = start; this._createTimeout = createTimeout; this._max = 60 * 5 * 1000; //5 min - this._timeout = null; } - async waitForRetry() { + async waitForRetry(): Promise { this._timeout = this._createTimeout(this._current); try { await this._timeout.elapsed(); @@ -39,22 +49,22 @@ export class ExponentialRetryDelay { throw err; } } finally { - this._timeout = null; + this._timeout = undefined; } } - abort() { + abort(): void { if (this._timeout) { this._timeout.abort(); } } - reset() { + reset(): void { this._current = this._start; this.abort(); } - get nextValue() { + get nextValue(): number { return this._current; } } diff --git a/src/matrix/net/HomeServerApi.js b/src/matrix/net/HomeServerApi.ts similarity index 50% rename from src/matrix/net/HomeServerApi.js rename to src/matrix/net/HomeServerApi.ts index 308e71d9..bacf26b0 100644 --- a/src/matrix/net/HomeServerApi.js +++ b/src/matrix/net/HomeServerApi.ts @@ -15,14 +15,33 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {encodeQueryParams, encodeBody} from "./common.js"; -import {HomeServerRequest} from "./HomeServerRequest.js"; +import {encodeQueryParams, encodeBody} from "./common"; +import {HomeServerRequest} from "./HomeServerRequest"; +import type {IHomeServerRequest} from "./HomeServerRequest"; +import type {Reconnector} from "./Reconnector"; +import type {EncodedBody} from "./common"; +import type {IRequestOptions, RequestFunction} from "../../platform/types/types"; +import type {ILogItem} from "../../logging/types"; + +type RequestMethod = "POST" | "GET" | "PUT"; const CS_R0_PREFIX = "/_matrix/client/r0"; const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; +type Options = { + homeserver: string; + accessToken: string; + request: RequestFunction; + reconnector: Reconnector; +}; + export class HomeServerApi { - constructor({homeserver, accessToken, request, reconnector}) { + private readonly _homeserver: string; + private readonly _accessToken: string; + private readonly _requestFn: RequestFunction; + private readonly _reconnector: Reconnector; + + constructor({homeserver, accessToken, request, reconnector}: Options) { // store these both in a closure somehow so it's harder to get at in case of XSS? // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write this._homeserver = homeserver; @@ -31,14 +50,14 @@ export class HomeServerApi { this._reconnector = reconnector; } - _url(csPath, prefix = CS_R0_PREFIX) { + private _url(csPath: string, prefix: string = CS_R0_PREFIX): string { return this._homeserver + prefix + csPath; } - _baseRequest(method, url, queryParams, body, options, accessToken) { + private _baseRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions, accessToken?: string): IHomeServerRequest { const queryString = encodeQueryParams(queryParams); url = `${url}?${queryString}`; - let log; + let log: ILogItem | undefined; if (options?.log) { const parent = options?.log; log = parent.child({ @@ -47,8 +66,8 @@ export class HomeServerApi { method, }, parent.level.Info); } - let encodedBody; - const headers = new Map(); + let encodedBody: EncodedBody["body"]; + const headers: Map = new Map(); if (accessToken) { headers.set("Authorization", `Bearer ${accessToken}`); } @@ -56,7 +75,6 @@ export class HomeServerApi { if (body) { const encoded = encodeBody(body); headers.set("Content-Type", encoded.mimeType); - headers.set("Content-Length", encoded.length); encodedBody = encoded.body; } @@ -86,63 +104,63 @@ export class HomeServerApi { return hsRequest; } - _unauthedRequest(method, url, queryParams, body, options) { - return this._baseRequest(method, url, queryParams, body, options, null); + private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { + return this._baseRequest(method, url, queryParams, body, options); } - _authedRequest(method, url, queryParams, body, options) { + private _authedRequest(method: RequestMethod, url: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { return this._baseRequest(method, url, queryParams, body, options, this._accessToken); } - _post(csPath, queryParams, body, options) { + private _post(csPath: string, queryParams: Record, body: Record, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - _put(csPath, queryParams, body, options) { + private _put(csPath: string, queryParams: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - _get(csPath, queryParams, body, options) { + private _get(csPath: string, queryParams?: Record, body?: Record, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); } - sync(since, filter, timeout, options = null) { - return this._get("/sync", {since, timeout, filter}, null, options); + sync(since: string, filter: string, timeout: number, options?: IRequestOptions): IHomeServerRequest { + return this._get("/sync", {since, timeout, filter}, undefined, options); } // params is from, dir and optionally to, limit, filter. - messages(roomId, params, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); + messages(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options); } // params is at, membership and not_membership - members(roomId, params, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); + members(roomId: string, params: Record, options?: IRequestOptions): IHomeServerRequest { + return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, undefined, options); } - send(roomId, eventType, txnId, content, options = null) { + send(roomId: string, eventType: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); } - redact(roomId, eventId, txnId, content, options = null) { + redact(roomId: string, eventId: string, txnId: string, content: Record, options?: IRequestOptions): IHomeServerRequest { return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); } - receipt(roomId, receiptType, eventId, options = null) { + receipt(roomId: string, receiptType: string, eventId: string, options?: IRequestOptions): IHomeServerRequest { return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`, {}, {}, options); } - state(roomId, eventType, stateKey, options = null) { - return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); + state(roomId: string, eventType: string, stateKey: string, options?: IRequestOptions): IHomeServerRequest { + return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, undefined, options); } - getLoginFlows() { - return this._unauthedRequest("GET", this._url("/login"), null, null, null); + getLoginFlows(): IHomeServerRequest { + return this._unauthedRequest("GET", this._url("/login")); } - passwordLogin(username, password, initialDeviceDisplayName, options = null) { - return this._unauthedRequest("POST", this._url("/login"), null, { + passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.password", "identifier": { "type": "m.id.user", @@ -153,8 +171,8 @@ export class HomeServerApi { }, options); } - tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { - return this._unauthedRequest("POST", this._url("/login"), null, { + tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest { + return this._unauthedRequest("POST", this._url("/login"), undefined, { "type": "m.login.token", "identifier": { "type": "m.id.user", @@ -165,91 +183,91 @@ export class HomeServerApi { }, options); } - createFilter(userId, filter, options = null) { - return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); + createFilter(userId: string, filter: Record, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/user/${encodeURIComponent(userId)}/filter`, {}, filter, options); } - versions(options = null) { - return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); + versions(options?: IRequestOptions): IHomeServerRequest { + return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, undefined, undefined, options); } - uploadKeys(dehydratedDeviceId, payload, options = null) { + uploadKeys(dehydratedDeviceId: string, payload: Record, options?: IRequestOptions): IHomeServerRequest { let path = "/keys/upload"; if (dehydratedDeviceId) { path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; } - return this._post(path, null, payload, options); + return this._post(path, {}, payload, options); } - queryKeys(queryRequest, options = null) { - return this._post("/keys/query", null, queryRequest, options); + queryKeys(queryRequest: Record, options?: IRequestOptions): IHomeServerRequest { + return this._post("/keys/query", {}, queryRequest, options); } - claimKeys(payload, options = null) { - return this._post("/keys/claim", null, payload, options); + claimKeys(payload: Record, options?: IRequestOptions): IHomeServerRequest { + return this._post("/keys/claim", {}, payload, options); } - sendToDevice(type, payload, txnId, options = null) { - return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); + sendToDevice(type: string, payload: Record, txnId: string, options?: IRequestOptions): IHomeServerRequest { + return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options); } - roomKeysVersion(version = null, options = null) { + roomKeysVersion(version?: string, options?: IRequestOptions): IHomeServerRequest { let versionPart = ""; if (version) { versionPart = `/${encodeURIComponent(version)}`; } - return this._get(`/room_keys/version${versionPart}`, null, null, options); + return this._get(`/room_keys/version${versionPart}`, undefined, undefined, options); } - roomKeyForRoomAndSession(version, roomId, sessionId, options = null) { - return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options); + roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: IRequestOptions): IHomeServerRequest { + return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, undefined, options); } - uploadAttachment(blob, filename, options = null) { + uploadAttachment(blob: Blob, filename: string, options?: IRequestOptions): IHomeServerRequest { return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options); } - setPusher(pusher, options = null) { - return this._post("/pushers/set", null, pusher, options); + setPusher(pusher: Record, options?: IRequestOptions): IHomeServerRequest { + return this._post("/pushers/set", {}, pusher, options); } - getPushers(options = null) { - return this._get("/pushers", null, null, options); + getPushers(options?: IRequestOptions): IHomeServerRequest { + return this._get("/pushers", undefined, undefined, options); } - join(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options); + join(roomId: string, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options); } - joinIdOrAlias(roomIdOrAlias, options = null) { - return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options); + joinIdOrAlias(roomIdOrAlias: string, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, {}, {}, options); } - leave(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options); + leave(roomId: string, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, {}, {}, options); } - forget(roomId, options = null) { - return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options); + forget(roomId: string, options?: IRequestOptions): IHomeServerRequest { + return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, {}, {}, options); } - logout(options = null) { - return this._post(`/logout`, null, null, options); + logout(options?: IRequestOptions): IHomeServerRequest { + return this._post(`/logout`, {}, {}, options); } - getDehydratedDevice(options = {}) { + getDehydratedDevice(options: IRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; - return this._get(`/dehydrated_device`, null, null, options); + return this._get(`/dehydrated_device`, undefined, undefined, options); } - createDehydratedDevice(payload, options = {}) { + createDehydratedDevice(payload: Record, options: IRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; - return this._put(`/dehydrated_device`, null, payload, options); + return this._put(`/dehydrated_device`, {}, payload, options); } - claimDehydratedDevice(deviceId, options = {}) { + claimDehydratedDevice(deviceId: string, options: IRequestOptions): IHomeServerRequest { options.prefix = DEHYDRATION_PREFIX; - return this._post(`/dehydrated_device/claim`, null, {device_id: deviceId}, options); + return this._post(`/dehydrated_device/claim`, {}, {device_id: deviceId}, options); } } @@ -258,11 +276,13 @@ import {Request as MockRequest} from "../../mocks/Request.js"; export function tests() { return { "superficial happy path for GET": async assert => { + // @ts-ignore const hsApi = new HomeServerApi({ request: () => new MockRequest().respond(200, 42), - homeserver: "https://hs.tld" + homeserver: "https://hs.tld", }); - const result = await hsApi._get("foo", null, null, null).response(); + // @ts-ignore + const result = await hsApi._get("foo").response(); assert.strictEqual(result, 42); } } diff --git a/src/matrix/net/HomeServerRequest.js b/src/matrix/net/HomeServerRequest.ts similarity index 90% rename from src/matrix/net/HomeServerRequest.js rename to src/matrix/net/HomeServerRequest.ts index 97728c28..ea5d2e40 100644 --- a/src/matrix/net/HomeServerRequest.js +++ b/src/matrix/net/HomeServerRequest.ts @@ -16,9 +16,21 @@ limitations under the License. */ import {HomeServerError, ConnectionError} from "../error.js"; +import type {RequestResult} from "../../platform/web/dom/request/fetch.js"; +import type {ILogItem} from "../../logging/types"; -export class HomeServerRequest { - constructor(method, url, sourceRequest, log) { +export interface IHomeServerRequest { + abort(): void; + response(): Promise; +} + +export class HomeServerRequest implements IHomeServerRequest { + private readonly _log?: ILogItem; + private _sourceRequest?: RequestResult; + // as we add types for expected responses from hs, this could be a generic class instead + private readonly _promise: Promise; + + constructor(method: string, url: string, sourceRequest: RequestResult, log?: ILogItem) { this._log = log; this._sourceRequest = sourceRequest; this._promise = sourceRequest.response().then(response => { @@ -80,16 +92,16 @@ export class HomeServerRequest { }); } - abort() { + abort(): void { if (this._sourceRequest) { this._log?.set("aborted", true); this._sourceRequest.abort(); // to mark that it was on purpose in above rejection handler - this._sourceRequest = null; + this._sourceRequest = undefined; } } - response() { + response(): Promise { return this._promise; } } diff --git a/src/matrix/net/MediaRepository.js b/src/matrix/net/MediaRepository.ts similarity index 70% rename from src/matrix/net/MediaRepository.js rename to src/matrix/net/MediaRepository.ts index 3f718c85..357b17c6 100644 --- a/src/matrix/net/MediaRepository.js +++ b/src/matrix/net/MediaRepository.ts @@ -14,16 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {encodeQueryParams} from "./common.js"; +import {encodeQueryParams} from "./common"; import {decryptAttachment} from "../e2ee/attachment.js"; +import {Platform} from "../../platform/web/Platform.js"; +import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; +import type {Attachment, EncryptedFile} from "./types/response"; export class MediaRepository { - constructor({homeserver, platform}) { + private readonly _homeserver: string; + private readonly _platform: Platform; + + constructor({homeserver, platform}: {homeserver:string, platform: Platform}) { this._homeserver = homeserver; this._platform = platform; } - mxcUrlThumbnail(url, width, height, method) { + mxcUrlThumbnail(url: string, width: number, height: number, method: "crop" | "scale"): string | null { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; @@ -33,7 +39,7 @@ export class MediaRepository { return null; } - mxcUrl(url) { + mxcUrl(url: string): string | null { const parts = this._parseMxcUrl(url); if (parts) { const [serverName, mediaId] = parts; @@ -43,7 +49,7 @@ export class MediaRepository { } } - _parseMxcUrl(url) { + private _parseMxcUrl(url: string): string[] | null { const prefix = "mxc://"; if (url.startsWith(prefix)) { return url.substr(prefix.length).split("/", 2); @@ -52,24 +58,24 @@ export class MediaRepository { } } - async downloadEncryptedFile(fileEntry, cache = false) { + async downloadEncryptedFile(fileEntry: EncryptedFile, cache: boolean = false): Promise { const url = this.mxcUrl(fileEntry.url); const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); const decryptedBuffer = await decryptAttachment(this._platform, encryptedBuffer, fileEntry); return this._platform.createBlob(decryptedBuffer, fileEntry.mimetype); } - async downloadPlaintextFile(mxcUrl, mimetype, cache = false) { + async downloadPlaintextFile(mxcUrl: string, mimetype: string, cache: boolean = false): Promise { const url = this.mxcUrl(mxcUrl); const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); return this._platform.createBlob(buffer, mimetype); } - async downloadAttachment(content, cache = false) { + async downloadAttachment(content: Attachment, cache: boolean = false): Promise { if (content.file) { return this.downloadEncryptedFile(content.file, cache); } else { - return this.downloadPlaintextFile(content.url, content.info?.mimetype, cache); + return this.downloadPlaintextFile(content.url!, content.info?.mimetype, cache); } } } diff --git a/src/matrix/net/Reconnector.js b/src/matrix/net/Reconnector.ts similarity index 78% rename from src/matrix/net/Reconnector.js rename to src/matrix/net/Reconnector.ts index 6eaa78d7..bc54ab73 100644 --- a/src/matrix/net/Reconnector.js +++ b/src/matrix/net/Reconnector.ts @@ -14,42 +14,59 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {createEnum} from "../../utils/enum"; import {ObservableValue} from "../../observable/ObservableValue"; +import type {ExponentialRetryDelay} from "./ExponentialRetryDelay"; +import type {TimeMeasure} from "../../platform/web/dom/Clock.js"; +import type {OnlineStatus} from "../../platform/web/dom/OnlineStatus.js"; +import type {VersionResponse} from "./types/response"; +import type {HomeServerApi} from "./HomeServerApi"; -export const ConnectionStatus = createEnum( +export enum ConnectionStatus { "Waiting", "Reconnecting", "Online" -); +}; + +type Ctor = { + retryDelay: ExponentialRetryDelay; + createMeasure: () => TimeMeasure; + onlineStatus: OnlineStatus +}; export class Reconnector { - constructor({retryDelay, createMeasure, onlineStatus}) { + private readonly _retryDelay: ExponentialRetryDelay; + private readonly _createTimeMeasure: () => TimeMeasure; + private readonly _onlineStatus: OnlineStatus; + private readonly _state: ObservableValue; + private _isReconnecting: boolean; + private _versionsResponse?: VersionResponse; + private _stateSince: TimeMeasure; + + constructor({retryDelay, createMeasure, onlineStatus}: Ctor) { this._onlineStatus = onlineStatus; this._retryDelay = retryDelay; this._createTimeMeasure = createMeasure; // assume online, and do our thing when something fails this._state = new ObservableValue(ConnectionStatus.Online); this._isReconnecting = false; - this._versionsResponse = null; } - get lastVersionsResponse() { + get lastVersionsResponse(): VersionResponse | undefined { return this._versionsResponse; } - get connectionStatus() { + get connectionStatus(): ObservableValue { return this._state; } - get retryIn() { + get retryIn(): number { if (this._state.get() === ConnectionStatus.Waiting) { return this._retryDelay.nextValue - this._stateSince.measure(); } return 0; } - async onRequestFailed(hsApi) { + async onRequestFailed(hsApi: HomeServerApi): Promise { if (!this._isReconnecting) { this._isReconnecting = true; @@ -75,14 +92,14 @@ export class Reconnector { } } - tryNow() { + tryNow(): void { if (this._retryDelay) { // this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop this._retryDelay.abort(); } } - _setState(state) { + private _setState(state: ConnectionStatus): void { if (state !== this._state.get()) { if (state === ConnectionStatus.Waiting) { this._stateSince = this._createTimeMeasure(); @@ -93,8 +110,8 @@ export class Reconnector { } } - async _reconnectLoop(hsApi) { - this._versionsResponse = null; + private async _reconnectLoop(hsApi: HomeServerApi): Promise { + this._versionsResponse = undefined; this._retryDelay.reset(); while (!this._versionsResponse) { @@ -120,7 +137,7 @@ export class Reconnector { import {Clock as MockClock} from "../../mocks/Clock.js"; -import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js"; +import {ExponentialRetryDelay as _ExponentialRetryDelay} from "./ExponentialRetryDelay"; import {ConnectionError} from "../error.js" export function tests() { @@ -146,13 +163,14 @@ export function tests() { const clock = new MockClock(); const {createMeasure} = clock; const onlineStatus = new ObservableValue(false); - const retryDelay = new ExponentialRetryDelay(clock.createTimeout); + const retryDelay = new _ExponentialRetryDelay(clock.createTimeout); const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); const {connectionStatus} = reconnector; - const statuses = []; + const statuses: ConnectionStatus[] = []; const subscription = reconnector.connectionStatus.subscribe(s => { statuses.push(s); }); + // @ts-ignore reconnector.onRequestFailed(createHsApiMock(1)); await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; clock.elapse(2000); @@ -170,9 +188,10 @@ export function tests() { const clock = new MockClock(); const {createMeasure} = clock; const onlineStatus = new ObservableValue(false); - const retryDelay = new ExponentialRetryDelay(clock.createTimeout); + const retryDelay = new _ExponentialRetryDelay(clock.createTimeout); const reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure}); const {connectionStatus} = reconnector; + // @ts-ignore reconnector.onRequestFailed(createHsApiMock(1)); await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; onlineStatus.set(true); //skip waiting diff --git a/src/matrix/net/RequestScheduler.js b/src/matrix/net/RequestScheduler.ts similarity index 52% rename from src/matrix/net/RequestScheduler.js rename to src/matrix/net/RequestScheduler.ts index f9adec97..45405da3 100644 --- a/src/matrix/net/RequestScheduler.js +++ b/src/matrix/net/RequestScheduler.ts @@ -17,35 +17,45 @@ limitations under the License. import {AbortError} from "../../utils/error"; import {HomeServerError} from "../error.js"; -import {HomeServerApi} from "./HomeServerApi.js"; -import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js"; +import {HomeServerApi} from "./HomeServerApi"; +import {ExponentialRetryDelay} from "./ExponentialRetryDelay"; +import {Clock} from "../../platform/web/dom/Clock.js"; +import type {IHomeServerRequest} from "./HomeServerRequest.js"; -class Request { - constructor(methodName, args) { - this._methodName = methodName; - this._args = args; +class Request implements IHomeServerRequest { + public readonly methodName: string; + public readonly args: any[]; + public resolve: (result: any) => void; + public reject: (error: Error) => void; + public requestResult?: IHomeServerRequest; + private readonly _responsePromise: Promise; + + constructor(methodName: string, args: any[]) { + this.methodName = methodName; + this.args = args; this._responsePromise = new Promise((resolve, reject) => { - this._resolve = resolve; - this._reject = reject; + this.resolve = resolve; + this.reject = reject; }); - this._requestResult = null; } - abort() { - if (this._requestResult) { - this._requestResult.abort(); + abort(): void { + if (this.requestResult) { + this.requestResult.abort(); } else { - this._reject(new AbortError()); + this.reject(new AbortError()); } } - response() { + response(): Promise { return this._responsePromise; } } class HomeServerApiWrapper { - constructor(scheduler) { + private readonly _scheduler: RequestScheduler; + + constructor(scheduler: RequestScheduler) { this._scheduler = scheduler; } } @@ -60,21 +70,22 @@ for (const methodName of Object.getOwnPropertyNames(HomeServerApi.prototype)) { } export class RequestScheduler { - constructor({hsApi, clock}) { + private readonly _hsApi: HomeServerApi; + private readonly _clock: Clock; + private readonly _requests: Set = new Set(); + private _stopped = false; + private _wrapper = new HomeServerApiWrapper(this); + + constructor({ hsApi, clock }: { hsApi: HomeServerApi; clock: Clock }) { this._hsApi = hsApi; this._clock = clock; - this._requests = new Set(); - this._isRateLimited = false; - this._isDrainingRateLimit = false; - this._stopped = true; - this._wrapper = new HomeServerApiWrapper(this); } - get hsApi() { - return this._wrapper; + get hsApi(): HomeServerApi { + return this._wrapper as unknown as HomeServerApi; } - stop() { + stop(): void { this._stopped = true; for (const request of this._requests) { request.abort(); @@ -82,40 +93,49 @@ export class RequestScheduler { this._requests.clear(); } - start() { + start(): void { this._stopped = false; } - _hsApiRequest(name, args) { + private _hsApiRequest(name: string, args: any[]): Request { const request = new Request(name, args); this._doSend(request); return request; } - async _doSend(request) { + private async _doSend(request: Request): Promise { this._requests.add(request); try { - let retryDelay; + let retryDelay: ExponentialRetryDelay | undefined; while (!this._stopped) { try { - const requestResult = this._hsApi[request._methodName].apply(this._hsApi, request._args); + const requestResult = this._hsApi[ + request.methodName + ].apply(this._hsApi, request.args); // so the request can be aborted - request._requestResult = requestResult; + request.requestResult = requestResult; const response = await requestResult.response(); - request._resolve(response); + request.resolve(response); return; } catch (err) { - if (err instanceof HomeServerError && err.errcode === "M_LIMIT_EXCEEDED") { + if ( + err instanceof HomeServerError && + err.errcode === "M_LIMIT_EXCEEDED" + ) { if (Number.isSafeInteger(err.retry_after_ms)) { - await this._clock.createTimeout(err.retry_after_ms).elapsed(); + await this._clock + .createTimeout(err.retry_after_ms) + .elapsed(); } else { if (!retryDelay) { - retryDelay = new ExponentialRetryDelay(this._clock.createTimeout); + retryDelay = new ExponentialRetryDelay( + this._clock.createTimeout + ); } await retryDelay.waitForRetry(); } } else { - request._reject(err); + request.reject(err); return; } } diff --git a/src/matrix/net/common.js b/src/matrix/net/common.ts similarity index 73% rename from src/matrix/net/common.js rename to src/matrix/net/common.ts index 889042ae..4ba42395 100644 --- a/src/matrix/net/common.js +++ b/src/matrix/net/common.ts @@ -15,7 +15,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function encodeQueryParams(queryParams) { +import {BlobHandle} from "../../platform/web/dom/BlobHandle.js"; + +export type EncodedBody = { + mimeType: string; + body: BlobHandle | string; +} + +export function encodeQueryParams(queryParams?: object): string { return Object.entries(queryParams || {}) .filter(([, value]) => value !== undefined) .map(([name, value]) => { @@ -27,21 +34,19 @@ export function encodeQueryParams(queryParams) { .join("&"); } -export function encodeBody(body) { - if (body.nativeBlob && body.mimeType) { - const blob = body; +export function encodeBody(body: BlobHandle | object): EncodedBody { + if (body instanceof BlobHandle) { + const blob = body as BlobHandle; return { mimeType: blob.mimeType, - body: blob, // will be unwrapped in request fn - length: blob.size + body: blob // will be unwrapped in request fn }; } else if (typeof body === "object") { const json = JSON.stringify(body); return { mimeType: "application/json", - body: json, - length: body.length - }; + body: json + } } else { throw new Error("Unknown body type: " + body); } diff --git a/src/matrix/net/request/replay.js b/src/matrix/net/request/replay.ts similarity index 64% rename from src/matrix/net/request/replay.js rename to src/matrix/net/request/replay.ts index c85a32fd..e03b91a8 100644 --- a/src/matrix/net/request/replay.js +++ b/src/matrix/net/request/replay.ts @@ -14,29 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { - AbortError, - ConnectionError -} from "../../error.js"; +import {AbortError, ConnectionError} from "../../error.js"; +import type {IRequestOptions, RequestFunction} from "../../../platform/types/types"; +import type {RequestResult} from "../../../platform/web/dom/request/fetch.js"; + +type Options = IRequestOptions & { + method?: any; + delay?: boolean; +} class RequestLogItem { - constructor(url, options) { + public readonly url: string; + public readonly options: Options; + public error: {aborted: boolean, network: boolean, message: string}; + public status: number; + public body: Response["body"]; + public start: number = performance.now(); + public end: number = 0; + + constructor(url: string, options: Options) { this.url = url; this.options = options; - this.error = null; - this.body = null; - this.status = status; - this.start = performance.now(); - this.end = 0; } - async handleResponse(response) { + async handleResponse(response: Response) { this.end = performance.now(); this.status = response.status; this.body = response.body; } - handleError(err) { + handleError(err: Error): void { this.end = performance.now(); this.error = { aborted: err instanceof AbortError, @@ -47,13 +54,15 @@ class RequestLogItem { } export class RecordRequester { - constructor(request) { + private readonly _origRequest: RequestFunction; + private readonly _requestLog: RequestLogItem[] = []; + + constructor(request: RequestFunction) { this._origRequest = request; - this._requestLog = []; this.request = this.request.bind(this); } - request(url, options) { + request(url: string, options: Options): RequestResult { const requestItem = new RequestLogItem(url, options); this._requestLog.push(requestItem); try { @@ -68,24 +77,27 @@ export class RecordRequester { } } - log() { + log(): RequestLogItem[] { return this._requestLog; } } export class ReplayRequester { - constructor(log, options) { + private readonly _log: RequestLogItem[]; + private readonly _options: Options; + + constructor(log: RequestLogItem[], options: Options) { this._log = log.slice(); this._options = options; this.request = this.request.bind(this); } - request(url, options) { - const idx = this._log.findIndex(item => { + request(url: string, options: Options): ReplayRequestResult { + const idx = this._log.findIndex((item) => { return item.url === url && options.method === item.options.method; }); if (idx === -1) { - return new ReplayRequestResult({status: 404}, options); + return new ReplayRequestResult({ status: 404 } as RequestLogItem, options); } else { const [item] = this._log.splice(idx, 1); return new ReplayRequestResult(item, options); @@ -94,17 +106,21 @@ export class ReplayRequester { } class ReplayRequestResult { - constructor(item, options) { + private readonly _item: RequestLogItem; + private readonly _options: Options; + private _aborted: boolean; + + constructor(item: RequestLogItem, options: Options) { this._item = item; this._options = options; this._aborted = false; } - abort() { + abort(): void { this._aborted = true; } - async response() { + async response(): Promise { if (this._options.delay) { const delay = this._item.end - this._item.start; await new Promise(resolve => setTimeout(resolve, delay)); diff --git a/src/matrix/net/types/response.ts b/src/matrix/net/types/response.ts new file mode 100644 index 00000000..f1102a1b --- /dev/null +++ b/src/matrix/net/types/response.ts @@ -0,0 +1,58 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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. +*/ + +export type Attachment = { + body: string; + info: AttachmentInfo; + msgtype: string; + url?: string; + file?: EncryptedFile; + filename?: string; +} + +export type EncryptedFile = { + key: JsonWebKey; + iv: string; + hashes: { + sha256: string; + }; + url: string; + v: string; + mimetype?: string; +} + +type AttachmentInfo = { + h?: number; + w?: number; + mimetype: string; + size: number; + duration?: number; + thumbnail_url?: string; + thumbnail_file?: EncryptedFile; + thumbnail_info?: ThumbnailInfo; +} + +type ThumbnailInfo = { + h: number; + w: number; + mimetype: string; + size: number; +} + +export type VersionResponse = { + versions: string[]; + unstable_features?: Record; +} diff --git a/src/platform/types/types.ts b/src/platform/types/types.ts new file mode 100644 index 00000000..58d2216f --- /dev/null +++ b/src/platform/types/types.ts @@ -0,0 +1,33 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +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 type {RequestResult} from "../web/dom/request/fetch.js"; +import type {EncodedBody} from "../../matrix/net/common"; +import type {ILogItem} from "../../logging/types"; + +export interface IRequestOptions { + uploadProgress?: (loadedBytes: number) => void; + timeout?: number; + body?: EncodedBody; + headers?: Map; + cache?: boolean; + log?: ILogItem; + prefix?: string; + method?: string; + format?: string; +} + +export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult; diff --git a/src/platform/web/dom/Clock.js b/src/platform/web/dom/Clock.js index 855e925c..6691bc73 100644 --- a/src/platform/web/dom/Clock.js +++ b/src/platform/web/dom/Clock.js @@ -60,7 +60,6 @@ class Interval { } } - class TimeMeasure { constructor() { this._start = window.performance.now(); diff --git a/src/platform/web/main.js b/src/platform/web/main.js index 34522ea8..21cc3986 100644 --- a/src/platform/web/main.js +++ b/src/platform/web/main.js @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js"; +// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay"; import {SessionContainer} from "../../matrix/SessionContainer.js"; import {RootViewModel} from "../../domain/RootViewModel.js"; import {createNavigation, createRouter} from "../../domain/navigation/index.js";