forked from mystiq/hydrogen-web
Merge pull request #588 from vector-im/ts-conversion-matrix-net
Convert /matrix/net to typescript
This commit is contained in:
commit
589a002d67
15 changed files with 384 additions and 184 deletions
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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));
|
58
src/matrix/net/types/response.ts
Normal file
58
src/matrix/net/types/response.ts
Normal 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>;
|
||||||
|
}
|
33
src/platform/types/types.ts
Normal file
33
src/platform/types/types.ts
Normal 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;
|
|
@ -60,7 +60,6 @@ class Interval {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TimeMeasure {
|
class TimeMeasure {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._start = window.performance.now();
|
this._start = window.performance.now();
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Reference in a new issue