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 {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(

View file

@ -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 {

View file

@ -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";

View file

@ -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<void> {
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;
}
}

View file

@ -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<string, any>, body?: Record<string, any>, 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<string, string | number> = 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<string, any>, body?: Record<string, any>, 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<string, any>, body?: Record<string, any>, options?: IRequestOptions): IHomeServerRequest {
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);
}
_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);
}
_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);
}
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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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<string, any>, 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);
}
}

View file

@ -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<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._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<any> {
return this._promise;
}
}

View file

@ -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<BlobHandle> {
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<BlobHandle> {
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<BlobHandle> {
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);
}
}
}

View file

@ -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<ConnectionStatus>;
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<ConnectionStatus> {
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<void> {
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<void> {
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

View file

@ -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<any>;
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<any> {
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<Request> = 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<void> {
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;
}
}

View file

@ -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);
}

View file

@ -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<RequestLogItem> {
if (this._options.delay) {
const delay = this._item.end - this._item.start;
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 {
constructor() {
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.
*/
// 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";