Merge pull request #588 from vector-im/ts-conversion-matrix-net

Convert /matrix/net to typescript
This commit is contained in:
Bruno Windels 2021-12-09 18:51:33 +01:00 committed by GitHub
commit 589a002d67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 384 additions and 184 deletions

View file

@ -16,7 +16,7 @@ limitations under the License.
import {ViewModel} from "../ViewModel.js"; import {ViewModel} from "../ViewModel.js";
import {createEnum} from "../../utils/enum"; import {createEnum} from "../../utils/enum";
import {ConnectionStatus} from "../../matrix/net/Reconnector.js"; import {ConnectionStatus} from "../../matrix/net/Reconnector";
import {SyncStatus} from "../../matrix/Sync.js"; import {SyncStatus} from "../../matrix/Sync.js";
const SessionStatus = createEnum( const SessionStatus = createEnum(

View file

@ -50,6 +50,8 @@ export interface ILogItem {
ensureRefId(): void; ensureRefId(): void;
catch(err: Error): Error; catch(err: Error): Error;
serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined; serialize(filter: LogFilter, parentStartTime: number | undefined, forced: boolean): ISerializedItem | undefined;
finish(): void;
child(labelOrValues: LabelOrValues, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
} }
export interface ILogger { export interface ILogger {

View file

@ -19,11 +19,11 @@ import {createEnum} from "../utils/enum";
import {lookupHomeserver} from "./well-known.js"; import {lookupHomeserver} from "./well-known.js";
import {AbortableOperation} from "../utils/AbortableOperation"; import {AbortableOperation} from "../utils/AbortableOperation";
import {ObservableValue} from "../observable/ObservableValue"; import {ObservableValue} from "../observable/ObservableValue";
import {HomeServerApi} from "./net/HomeServerApi.js"; import {HomeServerApi} from "./net/HomeServerApi";
import {Reconnector, ConnectionStatus} from "./net/Reconnector.js"; import {Reconnector, ConnectionStatus} from "./net/Reconnector";
import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay.js"; import {ExponentialRetryDelay} from "./net/ExponentialRetryDelay";
import {MediaRepository} from "./net/MediaRepository.js"; import {MediaRepository} from "./net/MediaRepository";
import {RequestScheduler} from "./net/RequestScheduler.js"; import {RequestScheduler} from "./net/RequestScheduler";
import {Sync, SyncStatus} from "./Sync.js"; import {Sync, SyncStatus} from "./Sync.js";
import {Session} from "./Session.js"; import {Session} from "./Session.js";
import {PasswordLoginMethod} from "./login/PasswordLoginMethod"; import {PasswordLoginMethod} from "./login/PasswordLoginMethod";

View file

@ -15,18 +15,28 @@ limitations under the License.
*/ */
import {AbortError} from "../../utils/error"; 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 { 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; const start = 2000;
this._start = start; this._start = start;
this._current = start; this._current = start;
this._createTimeout = createTimeout; this._createTimeout = createTimeout;
this._max = 60 * 5 * 1000; //5 min this._max = 60 * 5 * 1000; //5 min
this._timeout = null;
} }
async waitForRetry() { async waitForRetry(): Promise<void> {
this._timeout = this._createTimeout(this._current); this._timeout = this._createTimeout(this._current);
try { try {
await this._timeout.elapsed(); await this._timeout.elapsed();
@ -39,22 +49,22 @@ export class ExponentialRetryDelay {
throw err; throw err;
} }
} finally { } finally {
this._timeout = null; this._timeout = undefined;
} }
} }
abort() { abort(): void {
if (this._timeout) { if (this._timeout) {
this._timeout.abort(); this._timeout.abort();
} }
} }
reset() { reset(): void {
this._current = this._start; this._current = this._start;
this.abort(); this.abort();
} }
get nextValue() { get nextValue(): number {
return this._current; return this._current;
} }
} }

View file

@ -15,14 +15,33 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {encodeQueryParams, encodeBody} from "./common.js"; import {encodeQueryParams, encodeBody} from "./common";
import {HomeServerRequest} from "./HomeServerRequest.js"; 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 CS_R0_PREFIX = "/_matrix/client/r0";
const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2"; const DEHYDRATION_PREFIX = "/_matrix/client/unstable/org.matrix.msc2697.v2";
type Options = {
homeserver: string;
accessToken: string;
request: RequestFunction;
reconnector: Reconnector;
};
export class HomeServerApi { 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? // 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 // one could change the homeserver as well so the token gets sent there, so both must be protected from read/write
this._homeserver = homeserver; this._homeserver = homeserver;
@ -31,14 +50,14 @@ export class HomeServerApi {
this._reconnector = reconnector; this._reconnector = reconnector;
} }
_url(csPath, prefix = CS_R0_PREFIX) { private _url(csPath: string, prefix: string = CS_R0_PREFIX): string {
return this._homeserver + prefix + csPath; return this._homeserver + prefix + csPath;
} }
_baseRequest(method, url, queryParams, body, options, accessToken) { private _baseRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions, accessToken?: string): IHomeServerRequest {
const queryString = encodeQueryParams(queryParams); const queryString = encodeQueryParams(queryParams);
url = `${url}?${queryString}`; url = `${url}?${queryString}`;
let log; let log: ILogItem | undefined;
if (options?.log) { if (options?.log) {
const parent = options?.log; const parent = options?.log;
log = parent.child({ log = parent.child({
@ -47,8 +66,8 @@ export class HomeServerApi {
method, method,
}, parent.level.Info); }, parent.level.Info);
} }
let encodedBody; let encodedBody: EncodedBody["body"];
const headers = new Map(); const headers: Map<string, string | number> = new Map();
if (accessToken) { if (accessToken) {
headers.set("Authorization", `Bearer ${accessToken}`); headers.set("Authorization", `Bearer ${accessToken}`);
} }
@ -56,7 +75,6 @@ export class HomeServerApi {
if (body) { if (body) {
const encoded = encodeBody(body); const encoded = encodeBody(body);
headers.set("Content-Type", encoded.mimeType); headers.set("Content-Type", encoded.mimeType);
headers.set("Content-Length", encoded.length);
encodedBody = encoded.body; encodedBody = encoded.body;
} }
@ -86,63 +104,63 @@ export class HomeServerApi {
return hsRequest; return hsRequest;
} }
_unauthedRequest(method, url, queryParams, body, options) { private _unauthedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._baseRequest(method, url, queryParams, body, options, null); return this._baseRequest(method, url, queryParams, body, options);
} }
_authedRequest(method, url, queryParams, body, options) { private _authedRequest(method: RequestMethod, url: string, queryParams?: Record<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._baseRequest(method, url, queryParams, body, options, this._accessToken); return this._baseRequest(method, url, queryParams, body, options, this._accessToken);
} }
_post(csPath, queryParams, body, options) { private _post(csPath: string, queryParams: Record<string, any>, body: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._authedRequest("POST", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); 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<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._authedRequest("PUT", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); 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<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options); return this._authedRequest("GET", this._url(csPath, options?.prefix || CS_R0_PREFIX), queryParams, body, options);
} }
sync(since, filter, timeout, options = null) { sync(since: string, filter: string, timeout: number, options?: IRequestOptions): IHomeServerRequest {
return this._get("/sync", {since, timeout, filter}, null, options); return this._get("/sync", {since, timeout, filter}, undefined, options);
} }
// params is from, dir and optionally to, limit, filter. // params is from, dir and optionally to, limit, filter.
messages(roomId, params, options = null) { messages(roomId: string, params: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, null, options); return this._get(`/rooms/${encodeURIComponent(roomId)}/messages`, params, undefined, options);
} }
// params is at, membership and not_membership // params is at, membership and not_membership
members(roomId, params, options = null) { members(roomId: string, params: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/members`, params, null, options); 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<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._put(`/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${encodeURIComponent(txnId)}`, {}, content, options); 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<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._put(`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}/${encodeURIComponent(txnId)}`, {}, content, options); 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)}`, return this._post(`/rooms/${encodeURIComponent(roomId)}/receipt/${encodeURIComponent(receiptType)}/${encodeURIComponent(eventId)}`,
{}, {}, options); {}, {}, options);
} }
state(roomId, eventType, stateKey, options = null) { state(roomId: string, eventType: string, stateKey: string, options?: IRequestOptions): IHomeServerRequest {
return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, null, options); return this._get(`/rooms/${encodeURIComponent(roomId)}/state/${encodeURIComponent(eventType)}/${encodeURIComponent(stateKey)}`, {}, undefined, options);
} }
getLoginFlows() { getLoginFlows(): IHomeServerRequest {
return this._unauthedRequest("GET", this._url("/login"), null, null, null); return this._unauthedRequest("GET", this._url("/login"));
} }
passwordLogin(username, password, initialDeviceDisplayName, options = null) { passwordLogin(username: string, password: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest {
return this._unauthedRequest("POST", this._url("/login"), null, { return this._unauthedRequest("POST", this._url("/login"), undefined, {
"type": "m.login.password", "type": "m.login.password",
"identifier": { "identifier": {
"type": "m.id.user", "type": "m.id.user",
@ -153,8 +171,8 @@ export class HomeServerApi {
}, options); }, options);
} }
tokenLogin(loginToken, txnId, initialDeviceDisplayName, options = null) { tokenLogin(loginToken: string, txnId: string, initialDeviceDisplayName: string, options?: IRequestOptions): IHomeServerRequest {
return this._unauthedRequest("POST", this._url("/login"), null, { return this._unauthedRequest("POST", this._url("/login"), undefined, {
"type": "m.login.token", "type": "m.login.token",
"identifier": { "identifier": {
"type": "m.id.user", "type": "m.id.user",
@ -165,91 +183,91 @@ export class HomeServerApi {
}, options); }, options);
} }
createFilter(userId, filter, options = null) { createFilter(userId: string, filter: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._post(`/user/${encodeURIComponent(userId)}/filter`, null, filter, options); return this._post(`/user/${encodeURIComponent(userId)}/filter`, {}, filter, options);
} }
versions(options = null) { versions(options?: IRequestOptions): IHomeServerRequest {
return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, null, null, options); return this._unauthedRequest("GET", `${this._homeserver}/_matrix/client/versions`, undefined, undefined, options);
} }
uploadKeys(dehydratedDeviceId, payload, options = null) { uploadKeys(dehydratedDeviceId: string, payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
let path = "/keys/upload"; let path = "/keys/upload";
if (dehydratedDeviceId) { if (dehydratedDeviceId) {
path = path + `/${encodeURIComponent(dehydratedDeviceId)}`; path = path + `/${encodeURIComponent(dehydratedDeviceId)}`;
} }
return this._post(path, null, payload, options); return this._post(path, {}, payload, options);
} }
queryKeys(queryRequest, options = null) { queryKeys(queryRequest: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._post("/keys/query", null, queryRequest, options); return this._post("/keys/query", {}, queryRequest, options);
} }
claimKeys(payload, options = null) { claimKeys(payload: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._post("/keys/claim", null, payload, options); return this._post("/keys/claim", {}, payload, options);
} }
sendToDevice(type, payload, txnId, options = null) { sendToDevice(type: string, payload: Record<string, any>, txnId: string, options?: IRequestOptions): IHomeServerRequest {
return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, null, payload, options); return this._put(`/sendToDevice/${encodeURIComponent(type)}/${encodeURIComponent(txnId)}`, {}, payload, options);
} }
roomKeysVersion(version = null, options = null) { roomKeysVersion(version?: string, options?: IRequestOptions): IHomeServerRequest {
let versionPart = ""; let versionPart = "";
if (version) { if (version) {
versionPart = `/${encodeURIComponent(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) { roomKeyForRoomAndSession(version: string, roomId: string, sessionId: string, options?: IRequestOptions): IHomeServerRequest {
return this._get(`/room_keys/keys/${encodeURIComponent(roomId)}/${encodeURIComponent(sessionId)}`, {version}, null, options); 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); return this._authedRequest("POST", `${this._homeserver}/_matrix/media/r0/upload`, {filename}, blob, options);
} }
setPusher(pusher, options = null) { setPusher(pusher: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
return this._post("/pushers/set", null, pusher, options); return this._post("/pushers/set", {}, pusher, options);
} }
getPushers(options = null) { getPushers(options?: IRequestOptions): IHomeServerRequest {
return this._get("/pushers", null, null, options); return this._get("/pushers", undefined, undefined, options);
} }
join(roomId, options = null) { join(roomId: string, options?: IRequestOptions): IHomeServerRequest {
return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, null, null, options); return this._post(`/rooms/${encodeURIComponent(roomId)}/join`, {}, {}, options);
} }
joinIdOrAlias(roomIdOrAlias, options = null) { joinIdOrAlias(roomIdOrAlias: string, options?: IRequestOptions): IHomeServerRequest {
return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, null, null, options); return this._post(`/join/${encodeURIComponent(roomIdOrAlias)}`, {}, {}, options);
} }
leave(roomId, options = null) { leave(roomId: string, options?: IRequestOptions): IHomeServerRequest {
return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, null, null, options); return this._post(`/rooms/${encodeURIComponent(roomId)}/leave`, {}, {}, options);
} }
forget(roomId, options = null) { forget(roomId: string, options?: IRequestOptions): IHomeServerRequest {
return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, null, null, options); return this._post(`/rooms/${encodeURIComponent(roomId)}/forget`, {}, {}, options);
} }
logout(options = null) { logout(options?: IRequestOptions): IHomeServerRequest {
return this._post(`/logout`, null, null, options); return this._post(`/logout`, {}, {}, options);
} }
getDehydratedDevice(options = {}) { getDehydratedDevice(options: IRequestOptions): IHomeServerRequest {
options.prefix = DEHYDRATION_PREFIX; 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<string, any>, options: IRequestOptions): IHomeServerRequest {
options.prefix = DEHYDRATION_PREFIX; 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; 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() { export function tests() {
return { return {
"superficial happy path for GET": async assert => { "superficial happy path for GET": async assert => {
// @ts-ignore
const hsApi = new HomeServerApi({ const hsApi = new HomeServerApi({
request: () => new MockRequest().respond(200, 42), 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); assert.strictEqual(result, 42);
} }
} }

View file

@ -16,9 +16,21 @@ limitations under the License.
*/ */
import {HomeServerError, ConnectionError} from "../error.js"; 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 { export interface IHomeServerRequest {
constructor(method, url, sourceRequest, log) { abort(): void;
response(): Promise<any>;
}
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<any>;
constructor(method: string, url: string, sourceRequest: RequestResult, log?: ILogItem) {
this._log = log; this._log = log;
this._sourceRequest = sourceRequest; this._sourceRequest = sourceRequest;
this._promise = sourceRequest.response().then(response => { this._promise = sourceRequest.response().then(response => {
@ -80,16 +92,16 @@ export class HomeServerRequest {
}); });
} }
abort() { abort(): void {
if (this._sourceRequest) { if (this._sourceRequest) {
this._log?.set("aborted", true); this._log?.set("aborted", true);
this._sourceRequest.abort(); this._sourceRequest.abort();
// to mark that it was on purpose in above rejection handler // to mark that it was on purpose in above rejection handler
this._sourceRequest = null; this._sourceRequest = undefined;
} }
} }
response() { response(): Promise<any> {
return this._promise; return this._promise;
} }
} }

View file

@ -14,16 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {encodeQueryParams} from "./common.js"; import {encodeQueryParams} from "./common";
import {decryptAttachment} from "../e2ee/attachment.js"; 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 { 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._homeserver = homeserver;
this._platform = platform; 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); const parts = this._parseMxcUrl(url);
if (parts) { if (parts) {
const [serverName, mediaId] = parts; const [serverName, mediaId] = parts;
@ -33,7 +39,7 @@ export class MediaRepository {
return null; return null;
} }
mxcUrl(url) { mxcUrl(url: string): string | null {
const parts = this._parseMxcUrl(url); const parts = this._parseMxcUrl(url);
if (parts) { if (parts) {
const [serverName, mediaId] = parts; const [serverName, mediaId] = parts;
@ -43,7 +49,7 @@ export class MediaRepository {
} }
} }
_parseMxcUrl(url) { private _parseMxcUrl(url: string): string[] | null {
const prefix = "mxc://"; const prefix = "mxc://";
if (url.startsWith(prefix)) { if (url.startsWith(prefix)) {
return url.substr(prefix.length).split("/", 2); 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<BlobHandle> {
const url = this.mxcUrl(fileEntry.url); const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
const decryptedBuffer = await decryptAttachment(this._platform, encryptedBuffer, fileEntry); const decryptedBuffer = await decryptAttachment(this._platform, encryptedBuffer, fileEntry);
return this._platform.createBlob(decryptedBuffer, fileEntry.mimetype); return this._platform.createBlob(decryptedBuffer, fileEntry.mimetype);
} }
async downloadPlaintextFile(mxcUrl, mimetype, cache = false) { async downloadPlaintextFile(mxcUrl: string, mimetype: string, cache: boolean = false): Promise<BlobHandle> {
const url = this.mxcUrl(mxcUrl); const url = this.mxcUrl(mxcUrl);
const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
return this._platform.createBlob(buffer, mimetype); return this._platform.createBlob(buffer, mimetype);
} }
async downloadAttachment(content, cache = false) { async downloadAttachment(content: Attachment, cache: boolean = false): Promise<BlobHandle> {
if (content.file) { if (content.file) {
return this.downloadEncryptedFile(content.file, cache); return this.downloadEncryptedFile(content.file, cache);
} else { } else {
return this.downloadPlaintextFile(content.url, content.info?.mimetype, cache); return this.downloadPlaintextFile(content.url!, content.info?.mimetype, cache);
} }
} }
} }

View file

@ -14,42 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {createEnum} from "../../utils/enum";
import {ObservableValue} from "../../observable/ObservableValue"; 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", "Waiting",
"Reconnecting", "Reconnecting",
"Online" "Online"
); };
type Ctor = {
retryDelay: ExponentialRetryDelay;
createMeasure: () => TimeMeasure;
onlineStatus: OnlineStatus
};
export class Reconnector { export class Reconnector {
constructor({retryDelay, createMeasure, onlineStatus}) { private readonly _retryDelay: ExponentialRetryDelay;
private readonly _createTimeMeasure: () => TimeMeasure;
private readonly _onlineStatus: OnlineStatus;
private readonly _state: ObservableValue<ConnectionStatus>;
private _isReconnecting: boolean;
private _versionsResponse?: VersionResponse;
private _stateSince: TimeMeasure;
constructor({retryDelay, createMeasure, onlineStatus}: Ctor) {
this._onlineStatus = onlineStatus; this._onlineStatus = onlineStatus;
this._retryDelay = retryDelay; this._retryDelay = retryDelay;
this._createTimeMeasure = createMeasure; this._createTimeMeasure = createMeasure;
// assume online, and do our thing when something fails // assume online, and do our thing when something fails
this._state = new ObservableValue(ConnectionStatus.Online); this._state = new ObservableValue(ConnectionStatus.Online);
this._isReconnecting = false; this._isReconnecting = false;
this._versionsResponse = null;
} }
get lastVersionsResponse() { get lastVersionsResponse(): VersionResponse | undefined {
return this._versionsResponse; return this._versionsResponse;
} }
get connectionStatus() { get connectionStatus(): ObservableValue<ConnectionStatus> {
return this._state; return this._state;
} }
get retryIn() { get retryIn(): number {
if (this._state.get() === ConnectionStatus.Waiting) { if (this._state.get() === ConnectionStatus.Waiting) {
return this._retryDelay.nextValue - this._stateSince.measure(); return this._retryDelay.nextValue - this._stateSince.measure();
} }
return 0; return 0;
} }
async onRequestFailed(hsApi) { async onRequestFailed(hsApi: HomeServerApi): Promise<void> {
if (!this._isReconnecting) { if (!this._isReconnecting) {
this._isReconnecting = true; this._isReconnecting = true;
@ -75,14 +92,14 @@ export class Reconnector {
} }
} }
tryNow() { tryNow(): void {
if (this._retryDelay) { if (this._retryDelay) {
// this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop // this will interrupt this._retryDelay.waitForRetry() in _reconnectLoop
this._retryDelay.abort(); this._retryDelay.abort();
} }
} }
_setState(state) { private _setState(state: ConnectionStatus): void {
if (state !== this._state.get()) { if (state !== this._state.get()) {
if (state === ConnectionStatus.Waiting) { if (state === ConnectionStatus.Waiting) {
this._stateSince = this._createTimeMeasure(); this._stateSince = this._createTimeMeasure();
@ -93,8 +110,8 @@ export class Reconnector {
} }
} }
async _reconnectLoop(hsApi) { private async _reconnectLoop(hsApi: HomeServerApi): Promise<void> {
this._versionsResponse = null; this._versionsResponse = undefined;
this._retryDelay.reset(); this._retryDelay.reset();
while (!this._versionsResponse) { while (!this._versionsResponse) {
@ -120,7 +137,7 @@ export class Reconnector {
import {Clock as MockClock} from "../../mocks/Clock.js"; import {Clock as MockClock} from "../../mocks/Clock.js";
import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js"; import {ExponentialRetryDelay as _ExponentialRetryDelay} from "./ExponentialRetryDelay";
import {ConnectionError} from "../error.js" import {ConnectionError} from "../error.js"
export function tests() { export function tests() {
@ -146,13 +163,14 @@ export function tests() {
const clock = new MockClock(); const clock = new MockClock();
const {createMeasure} = clock; const {createMeasure} = clock;
const onlineStatus = new ObservableValue(false); 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 reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure});
const {connectionStatus} = reconnector; const {connectionStatus} = reconnector;
const statuses = []; const statuses: ConnectionStatus[] = [];
const subscription = reconnector.connectionStatus.subscribe(s => { const subscription = reconnector.connectionStatus.subscribe(s => {
statuses.push(s); statuses.push(s);
}); });
// @ts-ignore
reconnector.onRequestFailed(createHsApiMock(1)); reconnector.onRequestFailed(createHsApiMock(1));
await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise;
clock.elapse(2000); clock.elapse(2000);
@ -170,9 +188,10 @@ export function tests() {
const clock = new MockClock(); const clock = new MockClock();
const {createMeasure} = clock; const {createMeasure} = clock;
const onlineStatus = new ObservableValue(false); 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 reconnector = new Reconnector({retryDelay, onlineStatus, createMeasure});
const {connectionStatus} = reconnector; const {connectionStatus} = reconnector;
// @ts-ignore
reconnector.onRequestFailed(createHsApiMock(1)); reconnector.onRequestFailed(createHsApiMock(1));
await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise; await connectionStatus.waitFor(s => s === ConnectionStatus.Waiting).promise;
onlineStatus.set(true); //skip waiting onlineStatus.set(true); //skip waiting

View file

@ -17,35 +17,45 @@ limitations under the License.
import {AbortError} from "../../utils/error"; import {AbortError} from "../../utils/error";
import {HomeServerError} from "../error.js"; import {HomeServerError} from "../error.js";
import {HomeServerApi} from "./HomeServerApi.js"; import {HomeServerApi} from "./HomeServerApi";
import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js"; import {ExponentialRetryDelay} from "./ExponentialRetryDelay";
import {Clock} from "../../platform/web/dom/Clock.js";
import type {IHomeServerRequest} from "./HomeServerRequest.js";
class Request { class Request implements IHomeServerRequest {
constructor(methodName, args) { public readonly methodName: string;
this._methodName = methodName; public readonly args: any[];
this._args = args; public resolve: (result: any) => void;
public reject: (error: Error) => void;
public requestResult?: IHomeServerRequest;
private readonly _responsePromise: Promise<any>;
constructor(methodName: string, args: any[]) {
this.methodName = methodName;
this.args = args;
this._responsePromise = new Promise((resolve, reject) => { this._responsePromise = new Promise((resolve, reject) => {
this._resolve = resolve; this.resolve = resolve;
this._reject = reject; this.reject = reject;
}); });
this._requestResult = null;
} }
abort() { abort(): void {
if (this._requestResult) { if (this.requestResult) {
this._requestResult.abort(); this.requestResult.abort();
} else { } else {
this._reject(new AbortError()); this.reject(new AbortError());
} }
} }
response() { response(): Promise<any> {
return this._responsePromise; return this._responsePromise;
} }
} }
class HomeServerApiWrapper { class HomeServerApiWrapper {
constructor(scheduler) { private readonly _scheduler: RequestScheduler;
constructor(scheduler: RequestScheduler) {
this._scheduler = scheduler; this._scheduler = scheduler;
} }
} }
@ -60,21 +70,22 @@ for (const methodName of Object.getOwnPropertyNames(HomeServerApi.prototype)) {
} }
export class RequestScheduler { export class RequestScheduler {
constructor({hsApi, clock}) { private readonly _hsApi: HomeServerApi;
private readonly _clock: Clock;
private readonly _requests: Set<Request> = new Set();
private _stopped = false;
private _wrapper = new HomeServerApiWrapper(this);
constructor({ hsApi, clock }: { hsApi: HomeServerApi; clock: Clock }) {
this._hsApi = hsApi; this._hsApi = hsApi;
this._clock = clock; this._clock = clock;
this._requests = new Set();
this._isRateLimited = false;
this._isDrainingRateLimit = false;
this._stopped = true;
this._wrapper = new HomeServerApiWrapper(this);
} }
get hsApi() { get hsApi(): HomeServerApi {
return this._wrapper; return this._wrapper as unknown as HomeServerApi;
} }
stop() { stop(): void {
this._stopped = true; this._stopped = true;
for (const request of this._requests) { for (const request of this._requests) {
request.abort(); request.abort();
@ -82,40 +93,49 @@ export class RequestScheduler {
this._requests.clear(); this._requests.clear();
} }
start() { start(): void {
this._stopped = false; this._stopped = false;
} }
_hsApiRequest(name, args) { private _hsApiRequest(name: string, args: any[]): Request {
const request = new Request(name, args); const request = new Request(name, args);
this._doSend(request); this._doSend(request);
return request; return request;
} }
async _doSend(request) { private async _doSend(request: Request): Promise<void> {
this._requests.add(request); this._requests.add(request);
try { try {
let retryDelay; let retryDelay: ExponentialRetryDelay | undefined;
while (!this._stopped) { while (!this._stopped) {
try { 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 // so the request can be aborted
request._requestResult = requestResult; request.requestResult = requestResult;
const response = await requestResult.response(); const response = await requestResult.response();
request._resolve(response); request.resolve(response);
return; return;
} catch (err) { } 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)) { 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 { } else {
if (!retryDelay) { if (!retryDelay) {
retryDelay = new ExponentialRetryDelay(this._clock.createTimeout); retryDelay = new ExponentialRetryDelay(
this._clock.createTimeout
);
} }
await retryDelay.waitForRetry(); await retryDelay.waitForRetry();
} }
} else { } else {
request._reject(err); request.reject(err);
return; return;
} }
} }

View file

@ -15,7 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License. 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 || {}) return Object.entries(queryParams || {})
.filter(([, value]) => value !== undefined) .filter(([, value]) => value !== undefined)
.map(([name, value]) => { .map(([name, value]) => {
@ -27,21 +34,19 @@ export function encodeQueryParams(queryParams) {
.join("&"); .join("&");
} }
export function encodeBody(body) { export function encodeBody(body: BlobHandle | object): EncodedBody {
if (body.nativeBlob && body.mimeType) { if (body instanceof BlobHandle) {
const blob = body; const blob = body as BlobHandle;
return { return {
mimeType: blob.mimeType, mimeType: blob.mimeType,
body: blob, // will be unwrapped in request fn body: blob // will be unwrapped in request fn
length: blob.size
}; };
} else if (typeof body === "object") { } else if (typeof body === "object") {
const json = JSON.stringify(body); const json = JSON.stringify(body);
return { return {
mimeType: "application/json", mimeType: "application/json",
body: json, body: json
length: body.length }
};
} else { } else {
throw new Error("Unknown body type: " + body); throw new Error("Unknown body type: " + body);
} }

View file

@ -14,29 +14,36 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { import {AbortError, ConnectionError} from "../../error.js";
AbortError, import type {IRequestOptions, RequestFunction} from "../../../platform/types/types";
ConnectionError import type {RequestResult} from "../../../platform/web/dom/request/fetch.js";
} from "../../error.js";
type Options = IRequestOptions & {
method?: any;
delay?: boolean;
}
class RequestLogItem { 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.url = url;
this.options = options; 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.end = performance.now();
this.status = response.status; this.status = response.status;
this.body = response.body; this.body = response.body;
} }
handleError(err) { handleError(err: Error): void {
this.end = performance.now(); this.end = performance.now();
this.error = { this.error = {
aborted: err instanceof AbortError, aborted: err instanceof AbortError,
@ -47,13 +54,15 @@ class RequestLogItem {
} }
export class RecordRequester { export class RecordRequester {
constructor(request) { private readonly _origRequest: RequestFunction;
private readonly _requestLog: RequestLogItem[] = [];
constructor(request: RequestFunction) {
this._origRequest = request; this._origRequest = request;
this._requestLog = [];
this.request = this.request.bind(this); this.request = this.request.bind(this);
} }
request(url, options) { request(url: string, options: Options): RequestResult {
const requestItem = new RequestLogItem(url, options); const requestItem = new RequestLogItem(url, options);
this._requestLog.push(requestItem); this._requestLog.push(requestItem);
try { try {
@ -68,24 +77,27 @@ export class RecordRequester {
} }
} }
log() { log(): RequestLogItem[] {
return this._requestLog; return this._requestLog;
} }
} }
export class ReplayRequester { export class ReplayRequester {
constructor(log, options) { private readonly _log: RequestLogItem[];
private readonly _options: Options;
constructor(log: RequestLogItem[], options: Options) {
this._log = log.slice(); this._log = log.slice();
this._options = options; this._options = options;
this.request = this.request.bind(this); this.request = this.request.bind(this);
} }
request(url, options) { request(url: string, options: Options): ReplayRequestResult {
const idx = this._log.findIndex(item => { const idx = this._log.findIndex((item) => {
return item.url === url && options.method === item.options.method; return item.url === url && options.method === item.options.method;
}); });
if (idx === -1) { if (idx === -1) {
return new ReplayRequestResult({status: 404}, options); return new ReplayRequestResult({ status: 404 } as RequestLogItem, options);
} else { } else {
const [item] = this._log.splice(idx, 1); const [item] = this._log.splice(idx, 1);
return new ReplayRequestResult(item, options); return new ReplayRequestResult(item, options);
@ -94,17 +106,21 @@ export class ReplayRequester {
} }
class ReplayRequestResult { 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._item = item;
this._options = options; this._options = options;
this._aborted = false; this._aborted = false;
} }
abort() { abort(): void {
this._aborted = true; this._aborted = true;
} }
async response() { async response(): Promise<RequestLogItem> {
if (this._options.delay) { if (this._options.delay) {
const delay = this._item.end - this._item.start; const delay = this._item.end - this._item.start;
await new Promise(resolve => setTimeout(resolve, delay)); await new Promise(resolve => setTimeout(resolve, delay));

View file

@ -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<string, boolean>;
}

View file

@ -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<string, string|number>;
cache?: boolean;
log?: ILogItem;
prefix?: string;
method?: string;
format?: string;
}
export type RequestFunction = (url: string, options: IRequestOptions) => RequestResult;

View file

@ -60,7 +60,6 @@ class Interval {
} }
} }
class TimeMeasure { class TimeMeasure {
constructor() { constructor() {
this._start = window.performance.now(); this._start = window.performance.now();

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 {SessionContainer} from "../../matrix/SessionContainer.js";
import {RootViewModel} from "../../domain/RootViewModel.js"; import {RootViewModel} from "../../domain/RootViewModel.js";
import {createNavigation, createRouter} from "../../domain/navigation/index.js"; import {createNavigation, createRouter} from "../../domain/navigation/index.js";