Merge branch 'master' into kegan/syncv3

This commit is contained in:
Kegan Dougal 2021-12-02 10:54:07 +00:00
commit f193418ed1
68 changed files with 533 additions and 394 deletions

View file

@ -1,7 +1,9 @@
# Typescript migration
# Typescript style guide
## Introduce `abstract` & `override`
## Use `type` rather than `interface` for named parameters and POJO return values.
- find all methods and getters that throw or are empty in base classes and turn into abstract method or if all methods are abstract, into an interface.
- change child impls to not call super.method and to add override
- don't allow implicit override in ts config
`type` and `interface` can be used somewhat interchangebly used, but let's use `type` to describe data and `interface` to describe (polymorphic) behaviour.
Good examples of data are option objects to have named parameters, and POJO (plain old javascript objects) without any methods, just fields.
Also see [this playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBACghgJwgO2AeTMAlge2QZygF4oBvAKCiqmTgFsIAuKfYBLZAcwG5LqATCABs4IAPzNkAVzoAjCAl4BfcuVCQoAYQAWWIfwzY8hEvCSpDuAlABkZPlQDGOITgTNW7LstWOR+QjMUYHtqKGcCNilHYDcAChxMK3xmIIsk4wBKewcoFRVyPzgArV19KAgAD2AUfkDEYNDqCM9o2IQEjIJmHT0DLvxsijCw-ClIDsSjAkzeEebjEIYAuE5oEgADABJSKeSAOloGJSgsQh29433nVwQlDbnqfKA)

View file

@ -39,7 +39,7 @@
"eslint": "^7.32.0",
"fake-indexeddb": "^3.1.2",
"finalhandler": "^1.1.1",
"impunity": "^1.0.8",
"impunity": "^1.0.9",
"mdn-polyfills": "^5.20.0",
"postcss": "^8.1.1",
"postcss-css-variables": "^0.17.0",

View file

@ -19,7 +19,7 @@ limitations under the License.
// we do need to return a disposable from EventEmitter.on, or at least have a method here to easily track a subscription to an EventEmitter
import {EventEmitter} from "../utils/EventEmitter";
import {Disposables} from "../utils/Disposables.js";
import {Disposables} from "../utils/Disposables";
export class ViewModel extends EventEmitter {
constructor(options = {}) {

View file

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

View file

@ -188,7 +188,7 @@ import {NullLogItem, NullLogger} from "../../../../logging/NullLogger";
import {HomeServer as MockHomeServer} from "../../../../mocks/HomeServer.js";
// other imports
import {BaseMessageTile} from "./tiles/BaseMessageTile.js";
import {MappedList} from "../../../../observable/list/MappedList.js";
import {MappedList} from "../../../../observable/list/MappedList";
import {ObservableValue} from "../../../../observable/ObservableValue";
import {PowerLevels} from "../../../../matrix/room/PowerLevels.js";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {BaseObservableList} from "../../../../observable/list/BaseObservableList";
import {sortedIndex} from "../../../../utils/sortedIndex.js";
import {sortedIndex} from "../../../../utils/sortedIndex";
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary
// for now, tileCreator should be stable in whether it returns a tile or not.
@ -253,7 +253,7 @@ export class TilesCollection extends BaseObservableList {
}
}
import {ObservableArray} from "../../../../observable/list/ObservableArray.js";
import {ObservableArray} from "../../../../observable/list/ObservableArray";
import {UpdateAction} from "./UpdateAction.js";
export function tests() {

View file

@ -16,7 +16,7 @@ limitations under the License.
import {BaseMessageTile} from "./BaseMessageTile.js";
import {stringAsBody} from "../MessageBody.js";
import {createEnum} from "../../../../../utils/enum.js";
import {createEnum} from "../../../../../utils/enum";
export const BodyFormat = createEnum("Plain", "Html");

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import {BaseMessageTile} from "./BaseMessageTile.js";
import {formatSize} from "../../../../../utils/formatSize.js";
import {formatSize} from "../../../../../utils/formatSize";
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
export class FileTile extends BaseMessageTile {

View file

@ -16,7 +16,7 @@ limitations under the License.
import {ViewModel} from "../../ViewModel.js";
import {KeyType} from "../../../matrix/ssss/index.js";
import {createEnum} from "../../../utils/enum.js";
import {createEnum} from "../../../utils/enum";
export const Status = createEnum("Enabled", "SetupKey", "SetupPhrase", "Pending");

View file

@ -17,15 +17,17 @@ limitations under the License.
import {LogItem} from "./LogItem";
import {LogLevel, LogFilter} from "./LogFilter";
import type {ILogger, ILogExport, FilterCreator, LabelOrValues, LogCallback, ILogItem} from "./types";
import type {ILogger, ILogExport, FilterCreator, LabelOrValues, LogCallback, ILogItem, ISerializedItem} from "./types";
import type {Platform} from "../platform/web/Platform.js";
export abstract class BaseLogger implements ILogger {
protected _openItems: Set<LogItem> = new Set();
protected _platform: Platform;
protected _serializedTransformer: (item: ISerializedItem) => ISerializedItem;
constructor({platform}) {
constructor({platform, serializedTransformer = (item: ISerializedItem) => item}) {
this._platform = platform;
this._serializedTransformer = serializedTransformer;
}
log(labelOrValues: LabelOrValues, logLevel: LogLevel = LogLevel.Info): void {

View file

@ -26,7 +26,7 @@ import {BaseLogger} from "./BaseLogger";
import type {Interval} from "../platform/web/dom/Clock";
import type {Platform} from "../platform/web/Platform.js";
import type {BlobHandle} from "../platform/web/dom/BlobHandle.js";
import type {ILogItem, ILogExport} from "./types";
import type {ILogItem, ILogExport, ISerializedItem} from "./types";
import type {LogFilter} from "./LogFilter";
type QueuedItem = {
@ -40,7 +40,7 @@ export class IDBLogger extends BaseLogger {
private readonly _flushInterval: Interval;
private _queuedItems: QueuedItem[];
constructor(options: {name: string, flushInterval?: number, limit?: number, platform: Platform}) {
constructor(options: {name: string, flushInterval?: number, limit?: number, platform: Platform, serializedTransformer?: (item: ISerializedItem) => ISerializedItem}) {
super(options);
const {name, flushInterval = 60 * 1000, limit = 3000} = options;
this._name = name;
@ -119,10 +119,13 @@ export class IDBLogger extends BaseLogger {
_persistItem(logItem: ILogItem, filter: LogFilter, forced: boolean): void {
const serializedItem = logItem.serialize(filter, undefined, forced);
if (serializedItem) {
const transformedSerializedItem = this._serializedTransformer(serializedItem);
this._queuedItems.push({
json: JSON.stringify(serializedItem)
json: JSON.stringify(transformedSerializedItem)
});
}
}
_persistQueuedItems(items: QueuedItem[]): void {
try {

View file

@ -19,7 +19,7 @@ import {Room} from "./room/Room.js";
import {ArchivedRoom} from "./room/ArchivedRoom.js";
import {RoomStatus} from "./room/RoomStatus.js";
import {Invite} from "./room/Invite.js";
import {Pusher} from "./push/Pusher.js";
import {Pusher} from "./push/Pusher";
import { ObservableMap } from "../observable/index.js";
import {User} from "./User.js";
import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
@ -34,7 +34,7 @@ import {Encryption as MegOlmEncryption} from "./e2ee/megolm/Encryption.js";
import {MEGOLM_ALGORITHM} from "./e2ee/common.js";
import {RoomEncryption} from "./e2ee/RoomEncryption.js";
import {DeviceTracker} from "./e2ee/DeviceTracker.js";
import {LockMap} from "../utils/LockMap.js";
import {LockMap} from "../utils/LockMap";
import {groupBy} from "../utils/groupBy";
import {
keyFromCredential as ssssKeyFromCredential,

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {createEnum} from "../utils/enum.js";
import {createEnum} from "../utils/enum";
import {lookupHomeserver} from "./well-known.js";
import {AbortableOperation} from "../utils/AbortableOperation";
import {ObservableValue} from "../observable/ObservableValue";
@ -27,9 +27,9 @@ import {RequestScheduler} from "./net/RequestScheduler.js";
// import {Sync, SyncStatus} from "./Sync.js";
import {Sync3, SyncStatus} from "./Sync3";
import {Session} from "./Session.js";
import {PasswordLoginMethod} from "./login/PasswordLoginMethod.js";
import {TokenLoginMethod} from "./login/TokenLoginMethod.js";
import {SSOLoginHelper} from "./login/SSOLoginHelper.js";
import {PasswordLoginMethod} from "./login/PasswordLoginMethod";
import {TokenLoginMethod} from "./login/TokenLoginMethod";
import {SSOLoginHelper} from "./login/SSOLoginHelper";
import {getDehydratedDevice} from "./e2ee/Dehydration.js";
export const LoadStatus = createEnum(

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import {ObservableValue} from "../observable/ObservableValue";
import {createEnum} from "../utils/enum.js";
import {createEnum} from "../utils/enum";
const INCREMENTAL_TIMEOUT = 30000;

View file

@ -16,7 +16,7 @@ limitations under the License.
import {MEGOLM_ALGORITHM, DecryptionSource} from "./common.js";
import {groupEventsBySession} from "./megolm/decryption/utils";
import {mergeMap} from "../../utils/mergeMap.js";
import {mergeMap} from "../../utils/mergeMap";
import {groupBy} from "../../utils/groupBy";
import {makeTxnId} from "../common.js";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import anotherjson from "../../../lib/another-json/index.js";
import {createEnum} from "../../utils/enum.js";
import {createEnum} from "../../utils/enum";
export const DecryptionSource = createEnum("Sync", "Timeline", "Retry");

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {DecryptionChanges} from "./DecryptionChanges.js";
import {mergeMap} from "../../../../utils/mergeMap.js";
import {mergeMap} from "../../../../utils/mergeMap";
/**
* Class that contains all the state loaded from storage to decrypt the given events

View file

@ -16,7 +16,7 @@ limitations under the License.
import {DecryptionError} from "../common.js";
import {groupBy} from "../../../utils/groupBy";
import {MultiLock} from "../../../utils/Lock.js";
import {MultiLock} from "../../../utils/Lock";
import {Session} from "./Session.js";
import {DecryptionResult} from "../DecryptionResult.js";

View file

@ -38,7 +38,7 @@ export class HomeServerError extends Error {
}
}
export {AbortError} from "../utils/error.js";
export {AbortError} from "../utils/error";
export class ConnectionError extends Error {
constructor(message, isTimeout) {

View file

@ -14,17 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class LoginMethod {
constructor({homeserver}) {
this.homeserver = homeserver;
}
import type {ILogItem} from "../../logging/types";
import type {HomeServerApi} from "../net/HomeServerApi.js";
// eslint-disable-next-line no-unused-vars
async login(hsApi, deviceName, log) {
/*
Regardless of the login method, SessionContainer.startWithLogin()
can do SomeLoginMethod.login()
*/
throw("Not Implemented");
}
export interface ILoginMethod {
homeserver: string;
login(hsApi: HomeServerApi, deviceName: string, log: ILogItem): Promise<Record<string, any>>;
}

View file

@ -1,29 +0,0 @@
/*
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 {LoginMethod} from "./LoginMethod.js";
export class PasswordLoginMethod extends LoginMethod {
constructor(options) {
super(options);
this.username = options.username;
this.password = options.password;
}
async login(hsApi, deviceName, log) {
return await hsApi.passwordLogin(this.username, this.password, deviceName, {log}).response();
}
}

View file

@ -0,0 +1,35 @@
/*
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 {ILogItem} from "../../logging/types";
import {ILoginMethod} from "./LoginMethod";
import {HomeServerApi} from "../net/HomeServerApi.js";
export class PasswordLoginMethod implements ILoginMethod {
private readonly _username: string;
private readonly _password: string;
public readonly homeserver: string;
constructor({username, password, homeserver}: {username: string, password: string, homeserver: string}) {
this._username = username;
this._password = password;
this.homeserver = homeserver;
}
async login(hsApi: HomeServerApi, deviceName: string, log: ILogItem): Promise<Record<string, any>> {
return await hsApi.passwordLogin(this._username, this._password, deviceName, {log}).response();
}
}

View file

@ -15,13 +15,15 @@ limitations under the License.
*/
export class SSOLoginHelper{
constructor(homeserver) {
private _homeserver: string;
constructor(homeserver: string) {
this._homeserver = homeserver;
}
get homeserver() { return this._homeserver; }
get homeserver(): string { return this._homeserver; }
createSSORedirectURL(returnURL) {
createSSORedirectURL(returnURL: string): string {
return `${this._homeserver}/_matrix/client/r0/login/sso/redirect?redirectUrl=${returnURL}`;
}
}

View file

@ -14,16 +14,21 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {LoginMethod} from "./LoginMethod.js";
import {makeTxnId} from "../common.js";
import {ILogItem} from "../../logging/types";
import {ILoginMethod} from "./LoginMethod";
import {HomeServerApi} from "../net/HomeServerApi.js";
export class TokenLoginMethod extends LoginMethod {
constructor(options) {
super(options);
this._loginToken = options.loginToken;
export class TokenLoginMethod implements ILoginMethod {
private readonly _loginToken: string;
public readonly homeserver: string;
constructor({ homeserver, loginToken }: { homeserver: string, loginToken: string}) {
this.homeserver = homeserver;
this._loginToken = loginToken;
}
async login(hsApi, deviceName, log) {
async login(hsApi: HomeServerApi, deviceName: string, log: ILogItem): Promise<Record<string, any>> {
return await hsApi.tokenLogin(this._loginToken, makeTxnId(), deviceName, {log}).response();
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../../utils/error.js";
import {AbortError} from "../../utils/error";
export class ExponentialRetryDelay {
constructor(createTimeout) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {createEnum} from "../../utils/enum.js";
import {createEnum} from "../../utils/enum";
import {ObservableValue} from "../../observable/ObservableValue";
export const ConnectionStatus = createEnum(

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../../utils/error.js";
import {AbortError} from "../../utils/error";
import {HomeServerError} from "../error.js";
import {HomeServerApi} from "./HomeServerApi.js";
import {ExponentialRetryDelay} from "./ExponentialRetryDelay.js";

View file

@ -1,50 +0,0 @@
export class Pusher {
constructor(description) {
this._description = description;
}
static httpPusher(host, appId, pushkey, data) {
return new Pusher({
kind: "http",
append: true, // as pushkeys are shared between multiple users on one origin
data: Object.assign({}, data, {url: host + "/_matrix/push/v1/notify"}),
pushkey,
app_id: appId,
app_display_name: "Hydrogen",
device_display_name: "Hydrogen",
lang: "en"
});
}
static createDefaultPayload(sessionId) {
return {session_id: sessionId};
}
async enable(hsApi, log) {
try {
log.set("endpoint", new URL(this._description.data.endpoint).host);
} catch {
log.set("endpoint", null);
}
await hsApi.setPusher(this._description, {log}).response();
}
async disable(hsApi, log) {
const deleteDescription = Object.assign({}, this._description, {kind: null});
await hsApi.setPusher(deleteDescription, {log}).response();
}
serialize() {
return this._description;
}
equals(pusher) {
if (this._description.app_id !== pusher._description.app_id) {
return false;
}
if (this._description.pushkey !== pusher._description.pushkey) {
return false;
}
return JSON.stringify(this._description.data) === JSON.stringify(pusher._description.data);
}
}

90
src/matrix/push/Pusher.ts Normal file
View file

@ -0,0 +1,90 @@
/*
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 {HomeServerApi} from "../net/HomeServerApi.js";
import type {ILogItem} from "../../logging/types";
export interface IPusherDescription {
kind: "http" | "email" | "null";
lang: string;
device_display_name: string;
app_display_name: string;
app_id: string;
pushkey: string;
data: IPusherData;
append?: boolean;
profile_tag?: string;
}
interface IPusherData {
format?: string;
url?: string;
endpoint?: PushSubscriptionJSON["endpoint"];
keys?: PushSubscriptionJSON["keys"];
}
export class Pusher {
private readonly _description: IPusherDescription;
constructor(description: IPusherDescription) {
this._description = description;
}
static httpPusher(host: string, appId: string, pushkey: string, data: IPusherData): Pusher {
return new Pusher({
kind: "http",
append: true, // as pushkeys are shared between multiple users on one origin
data: Object.assign({}, data, {url: host + "/_matrix/push/v1/notify"}),
pushkey,
app_id: appId,
app_display_name: "Hydrogen",
device_display_name: "Hydrogen",
lang: "en"
});
}
static createDefaultPayload(sessionId: string): {session_id: string} {
return {session_id: sessionId};
}
async enable(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
try {
log.set("endpoint", new URL(this._description.data.endpoint!).host);
} catch {
log.set("endpoint", null);
}
await hsApi.setPusher(this._description, {log}).response();
}
async disable(hsApi: HomeServerApi, log: ILogItem): Promise<void> {
const deleteDescription = Object.assign({}, this._description, {kind: null});
await hsApi.setPusher(deleteDescription, {log}).response();
}
serialize(): IPusherDescription {
return this._description;
}
equals(pusher): boolean {
if (this._description.app_id !== pusher._description.app_id) {
return false;
}
if (this._description.pushkey !== pusher._description.pushkey) {
return false;
}
return JSON.stringify(this._description.data) === JSON.stringify(pusher._description.data);
}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {ObservableMap} from "../../../observable/map/ObservableMap.js";
import {RetainedValue} from "../../../utils/RetainedValue.js";
import {RetainedValue} from "../../../utils/RetainedValue";
export class MemberList extends RetainedValue {
constructor({members, closeCallback}) {

View file

@ -13,8 +13,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {createEnum} from "../../../utils/enum.js";
import {AbortError} from "../../../utils/error.js";
import {createEnum} from "../../../utils/enum";
import {AbortError} from "../../../utils/error";
import {REDACTION_TYPE} from "../common.js";
import {getRelationFromContent, getRelationTarget, setRelationTarget} from "../timeline/relations.js";

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {SortedArray} from "../../../observable/list/SortedArray.js";
import {SortedArray} from "../../../observable/list/SortedArray";
import {ConnectionError} from "../../error.js";
import {PendingEvent, SendStatus} from "./PendingEvent.js";
import {makeTxnId, isTxnId} from "../../common.js";

View file

@ -16,7 +16,7 @@ limitations under the License.
*/
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
import {Disposables} from "../../../utils/Disposables.js";
import {Disposables} from "../../../utils/Disposables";
import {Direction} from "./Direction";
import {TimelineReader} from "./persistence/TimelineReader.js";
import {PendingEventEntry} from "./entries/PendingEventEntry.js";

View file

@ -14,12 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export class SessionInfoStorage {
constructor(name) {
interface ISessionInfo {
id: string;
deviceId: string;
userId: string;
homeserver: string;
homeServer: string; // deprecate this over time
accessToken: string;
lastUsed: number;
}
// todo: this should probably be in platform/types?
interface ISessionInfoStorage {
getAll(): Promise<ISessionInfo[]>;
updateLastUsed(id: string, timestamp: number): Promise<void>;
get(id: string): Promise<ISessionInfo | undefined>;
add(sessionInfo: ISessionInfo): Promise<void>;
delete(sessionId: string): Promise<void>;
}
export class SessionInfoStorage implements ISessionInfoStorage {
private readonly _name: string;
constructor(name: string) {
this._name = name;
}
getAll() {
getAll(): Promise<ISessionInfo[]> {
const sessionsJson = localStorage.getItem(this._name);
if (sessionsJson) {
const sessions = JSON.parse(sessionsJson);
@ -30,7 +51,7 @@ export class SessionInfoStorage {
return Promise.resolve([]);
}
async updateLastUsed(id, timestamp) {
async updateLastUsed(id: string, timestamp: number): Promise<void> {
const sessions = await this.getAll();
if (sessions) {
const session = sessions.find(session => session.id === id);
@ -41,20 +62,20 @@ export class SessionInfoStorage {
}
}
async get(id) {
async get(id: string): Promise<ISessionInfo | undefined> {
const sessions = await this.getAll();
if (sessions) {
return sessions.find(session => session.id === id);
}
}
async add(sessionInfo) {
async add(sessionInfo: ISessionInfo): Promise<void> {
const sessions = await this.getAll();
sessions.push(sessionInfo);
localStorage.setItem(this._name, JSON.stringify(sessions));
}
async delete(sessionId) {
async delete(sessionId: string): Promise<void> {
let sessions = await this.getAll();
sessions = sessions.filter(s => s.id !== sessionId);
localStorage.setItem(this._name, JSON.stringify(sessions));

View file

@ -18,7 +18,7 @@ import {KeyDescription, Key} from "./common.js";
import {keyFromPassphrase} from "./passphrase.js";
import {keyFromRecoveryKey} from "./recoveryKey.js";
import {SESSION_E2EE_KEY_PREFIX} from "../e2ee/common.js";
import {createEnum} from "../../utils/enum.js";
import {createEnum} from "../../utils/enum";
const SSSS_KEY = `${SESSION_E2EE_KEY_PREFIX}ssssKey`;

View file

@ -17,7 +17,7 @@ limitations under the License.
import { IDBRequestError } from "./error";
import { StorageError } from "../common";
import { AbortError } from "../../../utils/error.js";
import { AbortError } from "../../../utils/error";
let needsSyncPromise = false;

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../utils/error.js";
import {AbortError} from "../utils/error";
export class BaseRequest {
constructor() {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../utils/error.js";
import {AbortError} from "../utils/error";
import {BaseObservable} from "./BaseObservable";
// like an EventEmitter, but doesn't have an event type

View file

@ -20,11 +20,11 @@ import {MappedMap} from "./map/MappedMap.js";
import {JoinedMap} from "./map/JoinedMap.js";
import {BaseObservableMap} from "./map/BaseObservableMap.js";
// re-export "root" (of chain) collections
export { ObservableArray } from "./list/ObservableArray.js";
export { SortedArray } from "./list/SortedArray.js";
export { MappedList } from "./list/MappedList.js";
export { AsyncMappedList } from "./list/AsyncMappedList.js";
export { ConcatList } from "./list/ConcatList.js";
export { ObservableArray } from "./list/ObservableArray";
export { SortedArray } from "./list/SortedArray";
export { MappedList } from "./list/MappedList";
export { AsyncMappedList } from "./list/AsyncMappedList";
export { ConcatList } from "./list/ConcatList";
export { ObservableMap } from "./map/ObservableMap.js";
// avoid circular dependency between these classes

View file

@ -15,15 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
import {IListObserver} from "./BaseObservableList";
import {BaseMappedList, Mapper, Updater, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class AsyncMappedList extends BaseMappedList {
constructor(sourceList, mapper, updater, removeCallback) {
super(sourceList, mapper, updater, removeCallback);
this._eventQueue = null;
}
export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> implements IListObserver<F> {
private _eventQueue: AsyncEvent<F>[] | null = null;
private _flushing: boolean = false;
onSubscribeFirst() {
onSubscribeFirst(): void {
this._sourceUnsubscribe = this._sourceList.subscribe(this);
this._eventQueue = [];
this._mappedValues = [];
@ -35,122 +34,112 @@ export class AsyncMappedList extends BaseMappedList {
this._flush();
}
async _flush() {
async _flush(): Promise<void> {
if (this._flushing) {
return;
}
this._flushing = true;
try {
while (this._eventQueue.length) {
const event = this._eventQueue.shift();
await event.run(this);
while (this._eventQueue!.length) {
const event = this._eventQueue!.shift();
await event!.run(this);
}
} finally {
this._flushing = false;
}
}
onReset() {
onReset(): void {
if (this._eventQueue) {
this._eventQueue.push(new ResetEvent());
this._flush();
}
}
onAdd(index, value) {
onAdd(index: number, value: F): void {
if (this._eventQueue) {
this._eventQueue.push(new AddEvent(index, value));
this._flush();
}
}
onUpdate(index, value, params) {
onUpdate(index: number, value: F, params: any): void {
if (this._eventQueue) {
this._eventQueue.push(new UpdateEvent(index, value, params));
this._flush();
}
}
onRemove(index) {
onRemove(index: number): void {
if (this._eventQueue) {
this._eventQueue.push(new RemoveEvent(index));
this._flush();
}
}
onMove(fromIdx, toIdx) {
onMove(fromIdx: number, toIdx: number): void {
if (this._eventQueue) {
this._eventQueue.push(new MoveEvent(fromIdx, toIdx));
this._flush();
}
}
onUnsubscribeLast() {
this._sourceUnsubscribe();
onUnsubscribeLast(): void {
this._sourceUnsubscribe!();
this._eventQueue = null;
this._mappedValues = null;
}
}
class AddEvent {
constructor(index, value) {
this.index = index;
this.value = value;
}
type AsyncEvent<F> = AddEvent<F> | UpdateEvent<F> | RemoveEvent<F> | MoveEvent<F> | ResetEvent<F>
async run(list) {
class AddEvent<F> {
constructor(public index: number, public value: F) {}
async run<T>(list: AsyncMappedList<F,T>): Promise<void> {
const mappedValue = await list._mapper(this.value);
runAdd(list, this.index, mappedValue);
}
}
class UpdateEvent {
constructor(index, value, params) {
this.index = index;
this.value = value;
this.params = params;
}
class UpdateEvent<F> {
constructor(public index: number, public value: F, public params: any) {}
async run(list) {
async run<T>(list: AsyncMappedList<F,T>): Promise<void> {
runUpdate(list, this.index, this.value, this.params);
}
}
class RemoveEvent {
constructor(index) {
this.index = index;
}
class RemoveEvent<F> {
constructor(public index: number) {}
async run(list) {
async run<T>(list: AsyncMappedList<F,T>): Promise<void> {
runRemove(list, this.index);
}
}
class MoveEvent {
constructor(fromIdx, toIdx) {
this.fromIdx = fromIdx;
this.toIdx = toIdx;
}
class MoveEvent<F> {
constructor(public fromIdx: number, public toIdx: number) {}
async run(list) {
async run<T>(list: AsyncMappedList<F,T>): Promise<void> {
runMove(list, this.fromIdx, this.toIdx);
}
}
class ResetEvent {
async run(list) {
class ResetEvent<F> {
async run<T>(list: AsyncMappedList<F,T>): Promise<void> {
runReset(list);
}
}
import {ObservableArray} from "./ObservableArray.js";
import {ObservableArray} from "./ObservableArray";
import {ListObserver} from "../../mocks/ListObserver.js";
export function tests() {
return {
"events are emitted in order": async assert => {
const double = n => n * n;
const source = new ObservableArray();
const source = new ObservableArray<number>();
const mapper = new AsyncMappedList(source, async n => {
await new Promise(r => setTimeout(r, n));
return {n: double(n)};

View file

@ -21,15 +21,15 @@ import {findAndUpdateInArray} from "./common";
export type Mapper<F,T> = (value: F) => T
export type Updater<F,T> = (mappedValue: T, params: any, value: F) => void;
export class BaseMappedList<F,T> extends BaseObservableList<T> {
export class BaseMappedList<F,T,R = T> extends BaseObservableList<T> {
protected _sourceList: BaseObservableList<F>;
protected _sourceUnsubscribe: (() => void) | null = null;
_mapper: Mapper<F,T>;
_updater: Updater<F,T>;
_mapper: Mapper<F,R>;
_updater?: Updater<F,T>;
_removeCallback?: (value: T) => void;
_mappedValues: T[] | null = null;
constructor(sourceList: BaseObservableList<F>, mapper: Mapper<F,T>, updater: Updater<F,T>, removeCallback?: (value: T) => void) {
constructor(sourceList: BaseObservableList<F>, mapper: Mapper<F,R>, updater?: Updater<F,T>, removeCallback?: (value: T) => void) {
super();
this._sourceList = sourceList;
this._mapper = mapper;
@ -50,12 +50,12 @@ export class BaseMappedList<F,T> extends BaseObservableList<T> {
}
}
export function runAdd<F,T>(list: BaseMappedList<F,T>, index: number, mappedValue: T): void {
export function runAdd<F,T,R>(list: BaseMappedList<F,T,R>, index: number, mappedValue: T): void {
list._mappedValues!.splice(index, 0, mappedValue);
list.emitAdd(index, mappedValue);
}
export function runUpdate<F,T>(list: BaseMappedList<F,T>, index: number, value: F, params: any): void {
export function runUpdate<F,T,R>(list: BaseMappedList<F,T,R>, index: number, value: F, params: any): void {
const mappedValue = list._mappedValues![index];
if (list._updater) {
list._updater(mappedValue, params, value);
@ -63,7 +63,7 @@ export function runUpdate<F,T>(list: BaseMappedList<F,T>, index: number, value:
list.emitUpdate(index, mappedValue, params);
}
export function runRemove<F,T>(list: BaseMappedList<F,T>, index: number): void {
export function runRemove<F,T,R>(list: BaseMappedList<F,T,R>, index: number): void {
const mappedValue = list._mappedValues![index];
list._mappedValues!.splice(index, 1);
if (list._removeCallback) {
@ -72,14 +72,14 @@ export function runRemove<F,T>(list: BaseMappedList<F,T>, index: number): void {
list.emitRemove(index, mappedValue);
}
export function runMove<F,T>(list: BaseMappedList<F,T>, fromIdx: number, toIdx: number): void {
export function runMove<F,T,R>(list: BaseMappedList<F,T,R>, fromIdx: number, toIdx: number): void {
const mappedValue = list._mappedValues![fromIdx];
list._mappedValues!.splice(fromIdx, 1);
list._mappedValues!.splice(toIdx, 0, mappedValue);
list.emitMove(fromIdx, toIdx, mappedValue);
}
export function runReset<F,T>(list: BaseMappedList<F,T>): void {
export function runReset<F,T,R>(list: BaseMappedList<F,T,R>): void {
list._mappedValues = [];
list.emitReset();
}

View file

@ -24,7 +24,18 @@ export interface IListObserver<T> {
onMove(from: number, to: number, value: T, list: BaseObservableList<T>): void
}
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> {
export function defaultObserverWith<T>(overrides: { [key in keyof IListObserver<T>]?: IListObserver<T>[key] }): IListObserver<T> {
const defaults = {
onReset(){},
onAdd(){},
onUpdate(){},
onRemove(){},
onMove(){},
}
return Object.assign(defaults, overrides);
}
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> implements Iterable<T> {
emitReset() {
for(let h of this._handlers) {
h.onReset(this);
@ -38,7 +49,7 @@ export abstract class BaseObservableList<T> extends BaseObservable<IListObserver
}
}
emitUpdate(index: number, value: T, params: any): void {
emitUpdate(index: number, value: T, params?: any): void {
for(let h of this._handlers) {
h.onUpdate(index, value, params, this);
}
@ -58,6 +69,6 @@ export abstract class BaseObservableList<T> extends BaseObservable<IListObserver
}
}
abstract [Symbol.iterator](): IterableIterator<T>;
abstract [Symbol.iterator](): Iterator<T>;
abstract get length(): number;
}

View file

@ -14,16 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList";
import {BaseObservableList, IListObserver} from "./BaseObservableList";
export class ConcatList extends BaseObservableList {
constructor(...sourceLists) {
export class ConcatList<T> extends BaseObservableList<T> implements IListObserver<T> {
protected _sourceLists: BaseObservableList<T>[];
protected _sourceUnsubscribes: (() => void)[] | null = null;
constructor(...sourceLists: BaseObservableList<T>[]) {
super();
this._sourceLists = sourceLists;
this._sourceUnsubscribes = null;
}
_offsetForSource(sourceList) {
_offsetForSource(sourceList: BaseObservableList<T>): number {
const listIdx = this._sourceLists.indexOf(sourceList);
let offset = 0;
for (let i = 0; i < listIdx; ++i) {
@ -32,17 +34,17 @@ export class ConcatList extends BaseObservableList {
return offset;
}
onSubscribeFirst() {
onSubscribeFirst(): void {
this._sourceUnsubscribes = this._sourceLists.map(sourceList => sourceList.subscribe(this));
}
onUnsubscribeLast() {
for (const sourceUnsubscribe of this._sourceUnsubscribes) {
onUnsubscribeLast(): void {
for (const sourceUnsubscribe of this._sourceUnsubscribes!) {
sourceUnsubscribe();
}
}
onReset() {
onReset(): void {
// TODO: not ideal if other source lists are large
// but working impl for now
// reset, and
@ -54,11 +56,11 @@ export class ConcatList extends BaseObservableList {
}
}
onAdd(index, value, sourceList) {
onAdd(index: number, value: T, sourceList: BaseObservableList<T>): void {
this.emitAdd(this._offsetForSource(sourceList) + index, value);
}
onUpdate(index, value, params, sourceList) {
onUpdate(index: number, value: T, params: any, sourceList: BaseObservableList<T>): void {
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
// as we are not supposed to call `length` on any uninitialized list
if (!this._sourceUnsubscribes) {
@ -67,16 +69,16 @@ export class ConcatList extends BaseObservableList {
this.emitUpdate(this._offsetForSource(sourceList) + index, value, params);
}
onRemove(index, value, sourceList) {
onRemove(index: number, value: T, sourceList: BaseObservableList<T>): void {
this.emitRemove(this._offsetForSource(sourceList) + index, value);
}
onMove(fromIdx, toIdx, value, sourceList) {
onMove(fromIdx: number, toIdx: number, value: T, sourceList: BaseObservableList<T>): void {
const offset = this._offsetForSource(sourceList);
this.emitMove(offset + fromIdx, offset + toIdx, value);
}
get length() {
get length(): number {
let len = 0;
for (let i = 0; i < this._sourceLists.length; ++i) {
len += this._sourceLists[i].length;
@ -104,7 +106,8 @@ export class ConcatList extends BaseObservableList {
}
}
import {ObservableArray} from "./ObservableArray.js";
import {ObservableArray} from "./ObservableArray";
import {defaultObserverWith} from "./BaseObservableList";
export async function tests() {
return {
test_length(assert) {
@ -133,13 +136,13 @@ export async function tests() {
const list2 = new ObservableArray([11, 12, 13]);
const all = new ConcatList(list1, list2);
let fired = false;
all.subscribe({
all.subscribe(defaultObserverWith({
onAdd(index, value) {
fired = true;
assert.equal(index, 4);
assert.equal(value, 11.5);
}
});
}));
list2.insert(1, 11.5);
assert(fired);
},
@ -148,13 +151,13 @@ export async function tests() {
const list2 = new ObservableArray([11, 12, 13]);
const all = new ConcatList(list1, list2);
let fired = false;
all.subscribe({
all.subscribe(defaultObserverWith({
onUpdate(index, value) {
fired = true;
assert.equal(index, 4);
assert.equal(value, 10);
}
});
}));
list2.emitUpdate(1, 10);
assert(fired);
},

View file

@ -15,9 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {IListObserver} from "./BaseObservableList";
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class MappedList extends BaseMappedList {
export class MappedList<F,T> extends BaseMappedList<F,T> implements IListObserver<F> {
onSubscribeFirst() {
this._sourceUnsubscribe = this._sourceList.subscribe(this);
this._mappedValues = [];
@ -26,16 +27,16 @@ export class MappedList extends BaseMappedList {
}
}
onReset() {
onReset(): void {
runReset(this);
}
onAdd(index, value) {
onAdd(index: number, value: F): void {
const mappedValue = this._mapper(value);
runAdd(this, index, mappedValue);
}
onUpdate(index, value, params) {
onUpdate(index: number, value: F, params: any): void {
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
if (!this._mappedValues) {
return;
@ -43,24 +44,25 @@ export class MappedList extends BaseMappedList {
runUpdate(this, index, value, params);
}
onRemove(index) {
onRemove(index: number): void {
runRemove(this, index);
}
onMove(fromIdx, toIdx) {
onMove(fromIdx: number, toIdx: number): void {
runMove(this, fromIdx, toIdx);
}
onUnsubscribeLast() {
this._sourceUnsubscribe();
onUnsubscribeLast(): void {
this._sourceUnsubscribe!();
}
}
import {ObservableArray} from "./ObservableArray.js";
import {ObservableArray} from "./ObservableArray";
import {BaseObservableList} from "./BaseObservableList";
import {defaultObserverWith} from "./BaseObservableList";
export async function tests() {
class MockList extends BaseObservableList {
class MockList extends BaseObservableList<number> {
get length() {
return 0;
}
@ -74,26 +76,26 @@ export async function tests() {
const source = new MockList();
const mapped = new MappedList(source, n => {return {n: n*n};});
let fired = false;
const unsubscribe = mapped.subscribe({
const unsubscribe = mapped.subscribe(defaultObserverWith({
onAdd(idx, value) {
fired = true;
assert.equal(idx, 0);
assert.equal(value.n, 36);
}
});
}));
source.emitAdd(0, 6);
assert(fired);
unsubscribe();
},
test_update(assert) {
const source = new MockList();
const mapped = new MappedList(
const mapped = new MappedList<number, { n: number, m?: number }>(
source,
n => {return {n: n*n};},
(o, p, n) => o.m = n*n
);
let fired = false;
const unsubscribe = mapped.subscribe({
const unsubscribe = mapped.subscribe(defaultObserverWith({
onAdd() {},
onUpdate(idx, value) {
fired = true;
@ -101,7 +103,7 @@ export async function tests() {
assert.equal(value.n, 36);
assert.equal(value.m, 49);
}
});
}));
source.emitAdd(0, 6);
source.emitUpdate(0, 7);
assert(fired);
@ -113,9 +115,9 @@ export async function tests() {
source,
n => {return n*n;}
);
mapped.subscribe({
mapped.subscribe(defaultObserverWith({
onUpdate() { assert.fail(); }
});
}));
assert.equal(mapped.findAndUpdate(
n => n === 100,
() => assert.fail()
@ -127,9 +129,9 @@ export async function tests() {
source,
n => {return n*n;}
);
mapped.subscribe({
mapped.subscribe(defaultObserverWith({
onUpdate() { assert.fail(); }
});
}));
let fired = false;
assert.equal(mapped.findAndUpdate(
n => n === 9,
@ -148,14 +150,14 @@ export async function tests() {
n => {return n*n;}
);
let fired = false;
mapped.subscribe({
mapped.subscribe(defaultObserverWith({
onUpdate(idx, n, params) {
assert.equal(idx, 1);
assert.equal(n, 9);
assert.equal(params, "param");
fired = true;
}
});
}));
assert.equal(mapped.findAndUpdate(n => n === 9, () => "param"), true);
assert.equal(fired, true);
},

View file

@ -16,35 +16,37 @@ limitations under the License.
import {BaseObservableList} from "./BaseObservableList";
export class ObservableArray extends BaseObservableList {
constructor(initialValues = []) {
export class ObservableArray<T> extends BaseObservableList<T> {
private _items: T[];
constructor(initialValues: T[] = []) {
super();
this._items = initialValues;
}
append(item) {
append(item: T): void {
this._items.push(item);
this.emitAdd(this._items.length - 1, item);
}
remove(idx) {
remove(idx: number): void {
const [item] = this._items.splice(idx, 1);
this.emitRemove(idx, item);
}
insertMany(idx, items) {
insertMany(idx: number, items: T[]): void {
for(let item of items) {
this.insert(idx, item);
idx += 1;
}
}
insert(idx, item) {
insert(idx: number, item: T): void {
this._items.splice(idx, 0, item);
this.emitAdd(idx, item);
}
move(fromIdx, toIdx) {
move(fromIdx: number, toIdx: number): void {
if (fromIdx < this._items.length && toIdx < this._items.length) {
const [item] = this._items.splice(fromIdx, 1);
this._items.splice(toIdx, 0, item);
@ -52,24 +54,24 @@ export class ObservableArray extends BaseObservableList {
}
}
update(idx, item, params = null) {
update(idx: number, item: T, params: any = null): void {
if (idx < this._items.length) {
this._items[idx] = item;
this.emitUpdate(idx, item, params);
}
}
get array() {
get array(): Readonly<T[]> {
return this._items;
}
at(idx) {
at(idx: number): T | undefined {
if (this._items && idx >= 0 && idx < this._items.length) {
return this._items[idx];
}
}
get length() {
get length(): number {
return this._items.length;
}

View file

@ -15,21 +15,23 @@ limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList";
import {sortedIndex} from "../../utils/sortedIndex.js";
import {sortedIndex} from "../../utils/sortedIndex";
import {findAndUpdateInArray} from "./common";
export class SortedArray extends BaseObservableList {
constructor(comparator) {
export class SortedArray<T> extends BaseObservableList<T> {
private _comparator: (left: T, right: T) => number;
private _items: T[] = [];
constructor(comparator: (left: T, right: T) => number) {
super();
this._comparator = comparator;
this._items = [];
}
setManyUnsorted(items) {
setManyUnsorted(items: T[]): void {
this.setManySorted(items);
}
setManySorted(items) {
setManySorted(items: T[]): void {
// TODO: we can make this way faster by only looking up the first and last key,
// and merging whatever is inbetween with items
// if items is not sorted, 💩🌀 will follow!
@ -42,11 +44,11 @@ export class SortedArray extends BaseObservableList {
}
}
findAndUpdate(predicate, updater) {
findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false): boolean {
return findAndUpdateInArray(predicate, this._items, this, updater);
}
getAndUpdate(item, updater, updateParams = null) {
getAndUpdate(item: T, updater: (existing: T, item: T) => any, updateParams: any = null): void {
const idx = this.indexOf(item);
if (idx !== -1) {
const existingItem = this._items[idx];
@ -56,7 +58,7 @@ export class SortedArray extends BaseObservableList {
}
}
update(item, updateParams = null) {
update(item: T, updateParams: any = null): void {
const idx = this.indexOf(item);
if (idx !== -1) {
this._items[idx] = item;
@ -64,7 +66,7 @@ export class SortedArray extends BaseObservableList {
}
}
indexOf(item) {
indexOf(item: T): number {
const idx = sortedIndex(this._items, item, this._comparator);
if (idx < this._items.length && this._comparator(this._items[idx], item) === 0) {
return idx;
@ -73,7 +75,7 @@ export class SortedArray extends BaseObservableList {
}
}
_getNext(item) {
_getNext(item: T): T | undefined {
let idx = sortedIndex(this._items, item, this._comparator);
while(idx < this._items.length && this._comparator(this._items[idx], item) <= 0) {
idx += 1;
@ -81,7 +83,7 @@ export class SortedArray extends BaseObservableList {
return this.get(idx);
}
set(item, updateParams = null) {
set(item: T, updateParams: any = null): void {
const idx = sortedIndex(this._items, item, this._comparator);
if (idx >= this._items.length || this._comparator(this._items[idx], item) !== 0) {
this._items.splice(idx, 0, item);
@ -92,21 +94,21 @@ export class SortedArray extends BaseObservableList {
}
}
get(idx) {
get(idx: number): T | undefined {
return this._items[idx];
}
remove(idx) {
remove(idx: number): void {
const item = this._items[idx];
this._items.splice(idx, 1);
this.emitRemove(idx, item);
}
get array() {
get array(): T[] {
return this._items;
}
get length() {
get length(): number {
return this._items.length;
}
@ -116,8 +118,11 @@ export class SortedArray extends BaseObservableList {
}
// iterator that works even if the current value is removed while iterating
class Iterator {
constructor(sortedArray) {
class Iterator<T> {
private _sortedArray: SortedArray<T> | null
private _current: T | null | undefined
constructor(sortedArray: SortedArray<T>) {
this._sortedArray = sortedArray;
this._current = null;
}
@ -145,7 +150,7 @@ class Iterator {
export function tests() {
return {
"setManyUnsorted": assert => {
const sa = new SortedArray((a, b) => a.localeCompare(b));
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
sa.setManyUnsorted(["b", "a", "c"]);
assert.equal(sa.length, 3);
assert.equal(sa.get(0), "a");
@ -153,7 +158,7 @@ export function tests() {
assert.equal(sa.get(2), "c");
},
"_getNext": assert => {
const sa = new SortedArray((a, b) => a.localeCompare(b));
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
sa.setManyUnsorted(["b", "a", "f"]);
assert.equal(sa._getNext("a"), "b");
assert.equal(sa._getNext("b"), "f");
@ -162,7 +167,7 @@ export function tests() {
assert.equal(sa._getNext("f"), undefined);
},
"iterator with removals": assert => {
const queue = new SortedArray((a, b) => a.idx - b.idx);
const queue = new SortedArray<{idx: number}>((a, b) => a.idx - b.idx);
queue.setManyUnsorted([{idx: 5}, {idx: 3}, {idx: 1}, {idx: 4}, {idx: 2}]);
const it = queue[Symbol.iterator]();
assert.equal(it.next().value.idx, 1);

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import {BaseObservableList} from "./BaseObservableList";
import {sortedIndex} from "../../utils/sortedIndex.js";
import {sortedIndex} from "../../utils/sortedIndex";
/*

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import aesjs from "../../../lib/aes-js/index.js";
import {hkdf} from "../../utils/crypto/hkdf.js";
import {hkdf} from "../../utils/crypto/hkdf";
import {Platform as ModernPlatform} from "./Platform.js";
export function Platform(container, paths) {

View file

@ -17,7 +17,7 @@ limitations under the License.
import {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.js";
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory";
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage";
import {SettingsStorage} from "./dom/SettingsStorage.js";
import {Encoding} from "./utils/Encoding.js";
import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js";
@ -35,7 +35,7 @@ import {WorkerPool} from "./dom/WorkerPool.js";
import {BlobHandle} from "./dom/BlobHandle.js";
import {hasReadPixelPermission, ImageHandle, VideoHandle} from "./dom/ImageHandle.js";
import {downloadInIframe} from "./dom/download.js";
import {Disposables} from "../../utils/Disposables.js";
import {Disposables} from "../../utils/Disposables";
import {parseHTML} from "./parsehtml.js";
import {handleAvatarError} from "./ui/avatar.js";
@ -132,11 +132,7 @@ export class Platform {
this.clock = new Clock();
this.encoding = new Encoding();
this.random = Math.random;
if (options?.development) {
this.logger = new ConsoleLogger({platform: this});
} else {
this.logger = new IDBLogger({name: "hydrogen_logs", platform: this});
}
this._createLogger(options?.development);
this.history = new History();
this.onlineStatus = new OnlineStatus();
this._serviceWorkerHandler = null;
@ -162,6 +158,21 @@ export class Platform {
this._disposables = new Disposables();
}
_createLogger(isDevelopment) {
// Make sure that loginToken does not end up in the logs
const transformer = (item) => {
if (item.e?.stack) {
item.e.stack = item.e.stack.replace(/(?<=\/\?loginToken=).+/, "<snip>");
}
return item;
};
if (isDevelopment) {
this.logger = new ConsoleLogger({platform: this});
} else {
this.logger = new IDBLogger({name: "hydrogen_logs", platform: this, serializedTransformer: transformer});
}
}
get updateService() {
return this._serviceWorkerHandler;
}
@ -272,3 +283,30 @@ export class Platform {
this._disposables.dispose();
}
}
import {LogItem} from "../../logging/LogItem";
export function tests() {
return {
"loginToken should not be in logs": (assert) => {
const transformer = (item) => {
if (item.e?.stack) {
item.e.stack = item.e.stack.replace(/(?<=\/\?loginToken=).+/, "<snip>");
}
return item;
};
const logger = {
_queuedItems: [],
_serializedTransformer: transformer,
_now: () => {}
};
logger.persist = IDBLogger.prototype._persistItem.bind(logger);
const logItem = new LogItem("test", 1, logger);
logItem.error = new Error();
logItem.error.stack = "main http://localhost:3000/src/main.js:55\n<anonymous> http://localhost:3000/?loginToken=secret:26"
logger.persist(logItem, null, false);
const item = logger._queuedItems.pop();
console.log(item);
assert.strictEqual(item.json.search("secret"), -1);
}
};
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../../../utils/error.js";
import {AbortError} from "../../../utils/error";
class Timeout {
constructor(ms) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "../../../utils/error.js";
import {AbortError} from "../../../utils/error";
class WorkerState {
constructor(worker) {

View file

@ -19,7 +19,7 @@ import {
AbortError,
ConnectionError
} from "../../../../matrix/error.js";
import {abortOnTimeout} from "../../../../utils/timeout.js";
import {abortOnTimeout} from "../../../../utils/timeout";
import {addCacheBuster} from "./common.js";
import {xhrRequest} from "./xhr.js";

View file

@ -15,6 +15,7 @@ limitations under the License.
*/
import {Range, RangeZone} from "./Range";
import {defaultObserverWith} from "../../../../observable/list/BaseObservableList";
function skipOnIterator<T>(it: Iterator<T>, pos: number): boolean {
let i = 0;
@ -214,7 +215,7 @@ export class ListRange extends Range {
}
}
import {ObservableArray} from "../../../../observable/list/ObservableArray.js";
import {ObservableArray} from "../../../../observable/list/ObservableArray";
export function tests() {
return {
@ -268,7 +269,7 @@ export function tests() {
const list = new ObservableArray(["b", "c", "d", "e"]);
const range = new ListRange(1, 3, list.length);
let added = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onAdd(idx, value) {
added = true;
const result = range.queryAdd(idx, value, list);
@ -280,7 +281,7 @@ export function tests() {
newRange: new ListRange(1, 3, 5)
});
}
});
}));
list.insert(0, "a");
assert(added);
},
@ -288,7 +289,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "d", "e"]);
const range = new ListRange(1, 3, list.length);
let added = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onAdd(idx, value) {
added = true;
const result = range.queryAdd(idx, value, list);
@ -300,7 +301,7 @@ export function tests() {
newRange: new ListRange(1, 3, 5)
});
}
});
}));
list.insert(2, "c");
assert(added);
},
@ -308,7 +309,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d"]);
const range = new ListRange(1, 3, list.length);
let added = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onAdd(idx, value) {
added = true;
const result = range.queryAdd(idx, value, list);
@ -317,7 +318,7 @@ export function tests() {
newRange: new ListRange(1, 3, 5)
});
}
});
}));
list.insert(4, "e");
assert(added);
},
@ -326,7 +327,7 @@ export function tests() {
const viewportItemCount = 4;
const range = new ListRange(0, 3, list.length, viewportItemCount);
let added = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onAdd(idx, value) {
added = true;
const result = range.queryAdd(idx, value, list);
@ -337,7 +338,7 @@ export function tests() {
value: "c"
});
}
});
}));
list.insert(2, "c");
assert(added);
},
@ -345,7 +346,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(1, 3, list.length);
let removed = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onRemove(idx) {
removed = true;
const result = range.queryRemove(idx, list);
@ -357,7 +358,7 @@ export function tests() {
newRange: new ListRange(1, 3, 4)
});
}
});
}));
list.remove(0);
assert(removed);
},
@ -365,7 +366,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(1, 3, list.length);
let removed = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onRemove(idx) {
removed = true;
const result = range.queryRemove(idx, list);
@ -378,7 +379,7 @@ export function tests() {
});
assert.equal(list.length, 4);
}
});
}));
list.remove(2);
assert(removed);
},
@ -386,7 +387,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(1, 3, list.length);
let removed = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onRemove(idx) {
removed = true;
const result = range.queryRemove(idx, list);
@ -395,7 +396,7 @@ export function tests() {
newRange: new ListRange(1, 3, 4)
});
}
});
}));
list.remove(3);
assert(removed);
},
@ -403,7 +404,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c"]);
const range = new ListRange(1, 3, list.length);
let removed = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onRemove(idx) {
removed = true;
const result = range.queryRemove(idx, list);
@ -415,7 +416,7 @@ export function tests() {
value: "a"
});
}
});
}));
list.remove(2);
assert(removed);
},
@ -423,7 +424,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c"]);
const range = new ListRange(0, 3, list.length);
let removed = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onRemove(idx) {
removed = true;
const result = range.queryRemove(idx, list);
@ -433,7 +434,7 @@ export function tests() {
removeIdx: 2,
});
}
});
}));
list.remove(2);
assert(removed);
},
@ -441,7 +442,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(1, 4, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -451,7 +452,7 @@ export function tests() {
toIdx: 3
});
}
});
}));
list.move(2, 3);
assert(moved);
},
@ -459,7 +460,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(2, 5, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -470,7 +471,7 @@ export function tests() {
value: "a"
});
}
});
}));
list.move(0, 3); // move "a" to after "d"
assert(moved);
},
@ -478,7 +479,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(0, 3, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -489,7 +490,7 @@ export function tests() {
value: "e"
});
}
});
}));
list.move(4, 1); // move "e" to before "b"
assert(moved);
},
@ -497,7 +498,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(0, 3, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -508,7 +509,7 @@ export function tests() {
value: "d"
});
}
});
}));
list.move(1, 3); // move "b" to after "d"
assert(moved);
},
@ -516,7 +517,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(2, 5, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -527,7 +528,7 @@ export function tests() {
value: "b"
});
}
});
}));
list.move(3, 0); // move "d" to before "a"
assert(moved);
},
@ -535,7 +536,7 @@ export function tests() {
const list = new ObservableArray(["a", "b", "c", "d", "e"]);
const range = new ListRange(1, 4, list.length);
let moved = false;
list.subscribe({
list.subscribe(defaultObserverWith({
onMove(fromIdx, toIdx, value) {
moved = true;
const result = range.queryMove(fromIdx, toIdx, value, list);
@ -546,7 +547,7 @@ export function tests() {
value: "e"
});
}
});
}));
list.move(0, 4); // move "a" to after "e"
assert(moved);
},

View file

@ -43,7 +43,7 @@ export class Range {
return range.start < this.end && this.start < range.end;
}
forEachInIterator<T>(it: IterableIterator<T>, callback: ((T, i: number) => void)) {
forEachInIterator<T>(it: Iterator<T>, callback: ((T, i: number) => void)) {
let i = 0;
for (i = 0; i < this.start; i += 1) {
it.next();

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
interface IAbortable {
export interface IAbortable {
abort();
}

View file

@ -14,7 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
function disposeValue(value) {
export interface IDisposable {
dispose(): void;
}
type Disposable = IDisposable | (() => void);
function disposeValue(value: Disposable): void {
if (typeof value === "function") {
value();
} else {
@ -22,16 +28,14 @@ function disposeValue(value) {
}
}
function isDisposable(value) {
function isDisposable(value: Disposable): boolean {
return value && (typeof value === "function" || typeof value.dispose === "function");
}
export class Disposables {
constructor() {
this._disposables = [];
}
private _disposables: Disposable[] | null = [];
track(disposable) {
track(disposable: Disposable): Disposable {
if (!isDisposable(disposable)) {
throw new Error("Not a disposable");
}
@ -40,19 +44,23 @@ export class Disposables {
disposeValue(disposable);
return disposable;
}
this._disposables.push(disposable);
this._disposables!.push(disposable);
return disposable;
}
untrack(disposable) {
const idx = this._disposables.indexOf(disposable);
untrack(disposable: Disposable): null {
if (this.isDisposed) {
console.warn("Disposables already disposed, cannot untrack");
return null;
}
const idx = this._disposables!.indexOf(disposable);
if (idx >= 0) {
this._disposables.splice(idx, 1);
this._disposables!.splice(idx, 1);
}
return null;
}
dispose() {
dispose(): void {
if (this._disposables) {
for (const d of this._disposables) {
disposeValue(d);
@ -61,17 +69,17 @@ export class Disposables {
}
}
get isDisposed() {
get isDisposed(): boolean {
return this._disposables === null;
}
disposeTracked(value) {
disposeTracked(value: Disposable): null {
if (value === undefined || value === null || this.isDisposed) {
return null;
}
const idx = this._disposables.indexOf(value);
const idx = this._disposables!.indexOf(value);
if (idx !== -1) {
const [foundValue] = this._disposables.splice(idx, 1);
const [foundValue] = this._disposables!.splice(idx, 1);
disposeValue(foundValue);
} else {
console.warn("disposable not found, did it leak?", value);

View file

@ -15,12 +15,10 @@ limitations under the License.
*/
export class Lock {
constructor() {
this._promise = null;
this._resolve = null;
}
private _promise?: Promise<void>;
private _resolve?: (() => void);
tryTake() {
tryTake(): boolean {
if (!this._promise) {
this._promise = new Promise(resolve => {
this._resolve = resolve;
@ -30,36 +28,36 @@ export class Lock {
return false;
}
async take() {
async take(): Promise<void> {
while(!this.tryTake()) {
await this.released();
}
}
get isTaken() {
get isTaken(): boolean {
return !!this._promise;
}
release() {
release(): void {
if (this._resolve) {
this._promise = null;
this._promise = undefined;
const resolve = this._resolve;
this._resolve = null;
this._resolve = undefined;
resolve();
}
}
released() {
released(): Promise<void> | undefined {
return this._promise;
}
}
export class MultiLock {
constructor(locks) {
this.locks = locks;
constructor(public readonly locks: Lock[]) {
}
release() {
release(): void {
for (const lock of this.locks) {
lock.release();
}
@ -86,9 +84,9 @@ export function tests() {
lock.tryTake();
let first;
lock.released().then(() => first = lock.tryTake());
lock.released()!.then(() => first = lock.tryTake());
let second;
lock.released().then(() => second = lock.tryTake());
lock.released()!.then(() => second = lock.tryTake());
const promise = lock.released();
lock.release();
await promise;

View file

@ -14,14 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Lock} from "./Lock.js";
import {Lock} from "./Lock";
export class LockMap {
constructor() {
this._map = new Map();
}
export class LockMap<T> {
private readonly _map: Map<T, Lock> = new Map();
async takeLock(key) {
async takeLock(key: T): Promise<Lock> {
let lock = this._map.get(key);
if (lock) {
await lock.take();
@ -31,10 +29,10 @@ export class LockMap {
this._map.set(key, lock);
}
// don't leave old locks lying around
lock.released().then(() => {
lock.released()!.then(() => {
// give others a chance to take the lock first
Promise.resolve().then(() => {
if (!lock.isTaken) {
if (!lock!.isTaken) {
this._map.delete(key);
}
});
@ -67,6 +65,7 @@ export function tests() {
ranSecond = true;
assert.equal(returnedLock.isTaken, true);
// peek into internals, naughty
// @ts-ignore
assert.equal(lockMap._map.get("foo"), returnedLock);
});
lock.release();
@ -84,6 +83,7 @@ export function tests() {
// double delay to make sure cleanup logic ran
await Promise.resolve();
await Promise.resolve();
// @ts-ignore
assert.equal(lockMap._map.has("foo"), false);
},

View file

@ -15,16 +15,18 @@ limitations under the License.
*/
export class RetainedValue {
constructor(freeCallback) {
private readonly _freeCallback: () => void;
private _retentionCount: number = 1;
constructor(freeCallback: () => void) {
this._freeCallback = freeCallback;
this._retentionCount = 1;
}
retain() {
retain(): void {
this._retentionCount += 1;
}
release() {
release(): void {
this._retentionCount -= 1;
if (this._retentionCount === 0) {
this._freeCallback();

View file

@ -6,8 +6,10 @@
* Based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-hkdf/src/hkdf.ts
*/
import type {Crypto} from "../../platform/web/dom/Crypto.js";
// forked this code to make it use the cryptoDriver for HMAC that is more backwards-compatible
export async function hkdf(cryptoDriver, key, salt, info, hash, length) {
export async function hkdf(cryptoDriver: Crypto, key: Uint8Array, salt: Uint8Array, info: Uint8Array, hash: "SHA-256" | "SHA-512", length: number): Promise<Uint8Array> {
length = length / 8;
const len = cryptoDriver.digestSize(hash);

View file

@ -6,17 +6,19 @@
* Based on https://github.com/junkurihara/jscu/blob/develop/packages/js-crypto-pbkdf/src/pbkdf.ts
*/
import type {Crypto} from "../../platform/web/dom/Crypto.js";
// not used atm, but might in the future
// forked this code to make it use the cryptoDriver for HMAC that is more backwards-compatible
const nwbo = (num, len) => {
const nwbo = (num: number, len: number): Uint8Array => {
const arr = new Uint8Array(len);
for(let i=0; i<len; i++) arr[i] = 0xFF && (num >> ((len - i - 1)*8));
return arr;
};
export async function pbkdf2(cryptoDriver, password, iterations, salt, hash, length) {
export async function pbkdf2(cryptoDriver: Crypto, password: Uint8Array, iterations: number, salt: Uint8Array, hash: "SHA-256" | "SHA-512", length: number): Promise<Uint8Array> {
const dkLen = length / 8;
if (iterations <= 0) {
throw new Error('InvalidIterationCount');
@ -30,7 +32,7 @@ export async function pbkdf2(cryptoDriver, password, iterations, salt, hash, len
const l = Math.ceil(dkLen/hLen);
const r = dkLen - (l-1)*hLen;
const funcF = async (i) => {
const funcF = async (i: number) => {
const seed = new Uint8Array(salt.length + 4);
seed.set(salt);
seed.set(nwbo(i+1, 4), salt.length);
@ -46,7 +48,7 @@ export async function pbkdf2(cryptoDriver, password, iterations, salt, hash, len
return {index: i, value: outputF};
};
const Tis = [];
const Tis: Promise<{index: number, value: Uint8Array}>[] = [];
const DK = new Uint8Array(dkLen);
for(let i = 0; i < l; i++) {
Tis.push(funcF(i));

View file

@ -14,12 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function createEnum(...values) {
export function createEnum(...values: string[]): Readonly<{}> {
const obj = {};
for (const value of values) {
if (typeof value !== "string") {
throw new Error("Invalid enum value name" + value?.toString());
}
obj[value] = value;
}
return Object.freeze(obj);

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
export class AbortError extends Error {
get name() {
get name(): string {
return "AbortError";
}
}

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function formatSize(size, decimals = 2) {
export function formatSize(size: number, decimals: number = 2): string {
if (Number.isSafeInteger(size)) {
const base = Math.min(3, Math.floor(Math.log(size) / Math.log(1024)));
const formattedSize = Math.round(size / Math.pow(1024, base)).toFixed(decimals);
@ -25,4 +26,5 @@ export function formatSize(size, decimals = 2) {
case 3: return `${formattedSize} GB`;
}
}
return "";
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
export function mergeMap(src, dst) {
export function mergeMap<K, V>(src: Map<K, V> | undefined, dst: Map<K, V>): void {
if (src) {
for (const [key, value] of src.entries()) {
dst.set(key, value);

View file

@ -22,7 +22,7 @@ limitations under the License.
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
*/
export function sortedIndex(array, value, comparator) {
export function sortedIndex<T>(array: T[], value: T, comparator: (x:T, y:T) => number): number {
let low = 0;
let high = array.length;

View file

@ -16,9 +16,12 @@ limitations under the License.
*/
import {ConnectionError} from "../matrix/error.js";
import type {Timeout} from "../platform/web/dom/Clock.js"
import type {IAbortable} from "./AbortableOperation";
type TimeoutCreator = (ms: number) => Timeout;
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {
export function abortOnTimeout(createTimeout: TimeoutCreator, timeoutAmount: number, requestResult: IAbortable, responsePromise: Promise<Response>) {
const timeout = createTimeout(timeoutAmount);
// abort request if timeout finishes first
let timedOut = false;

View file

@ -3124,10 +3124,10 @@ import-fresh@^3.0.0, import-fresh@^3.2.1:
parent-module "^1.0.0"
resolve-from "^4.0.0"
impunity@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/impunity/-/impunity-1.0.8.tgz#d6db44e8a15ca2cdaed6a5c478a770853cb5a56e"
integrity sha512-6jMqYrvY2SA/PZ+yheJYd3eJ3zcO8dWmHRVy/BSjnKMEmIVB+lMO30MZOkG+kHH0eJuaGOKv0BrZmgwE6NUDRg==
impunity@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/impunity/-/impunity-1.0.9.tgz#8524d96f07c26987519ec693c4c4d3ab49254b03"
integrity sha512-tfy7GRHeE9JVURKM7dqfTAZItGFeA/DRrlhgMLUuzSig3jF+AYSUV26tGTMGrfCN0Cb9hNz6xrZnNwa5M1hz4Q==
dependencies:
colors "^1.3.3"
commander "^6.1.0"