refactors ObservableMap

This commit is contained in:
Isaiah Becker-Mayer 2022-07-04 19:14:34 -04:00
parent c898bcb46a
commit b33db1df36
12 changed files with 234 additions and 171 deletions

View File

@ -1,25 +1,25 @@
module.exports = { module.exports = {
"env": { root: true,
env: {
"browser": true, "browser": true,
"es6": true "es6": true
}, },
"extends": "eslint:recommended", extends: [
"parserOptions": { // "plugin:@typescript-eslint/recommended",
// "plugin:@typescript-eslint/recommended-requiring-type-checking",
],
parser: '@typescript-eslint/parser',
parserOptions: {
"ecmaVersion": 2020, "ecmaVersion": 2020,
"sourceType": "module" "sourceType": "module",
"project": "./tsconfig.json"
}, },
"rules": { plugins: [
"no-console": "off", '@typescript-eslint',
"no-empty": "off", ],
"no-prototype-builtins": "off", rules: {
"no-unused-vars": "warn" "@typescript-eslint/no-floating-promises": 2,
}, "@typescript-eslint/no-misused-promises": 2,
"globals": { "semi": ["error", "always"]
"DEFINE_VERSION": "readonly",
"DEFINE_GLOBAL_HASH": "readonly",
// only available in sw.js
"DEFINE_UNHASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_PRECACHED_ASSETS": "readonly",
"DEFINE_HASHED_CACHED_ON_REQUEST_ASSETS": "readonly"
} }
}; };

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {SortedArray} from "../observable/index.js"; import {SortedArray} from "../observable";
import {ViewModel} from "./ViewModel"; import {ViewModel} from "./ViewModel";
import {avatarInitials, getIdentifierColorNumber} from "./avatar"; import {avatarInitials, getIdentifierColorNumber} from "./avatar";

View File

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ObservableMap} from "../../../../observable/map/ObservableMap"; import {ObservableMap} from "../../../../observable";
export class ReactionsViewModel { export class ReactionsViewModel {
constructor(parentTile) { constructor(parentTile) {

View File

@ -21,7 +21,7 @@ import {RoomStatus} from "./room/common";
import {RoomBeingCreated} from "./room/RoomBeingCreated"; import {RoomBeingCreated} from "./room/RoomBeingCreated";
import {Invite} from "./room/Invite.js"; import {Invite} from "./room/Invite.js";
import {Pusher} from "./push/Pusher"; import {Pusher} from "./push/Pusher";
import { ObservableMap } from "../observable/index.js"; import {ObservableMap} from "../observable";
import {User} from "./User.js"; import {User} from "./User.js";
import {DeviceMessageHandler} from "./DeviceMessageHandler.js"; import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
import {Account as E2EEAccount} from "./e2ee/Account.js"; import {Account as E2EEAccount} from "./e2ee/Account.js";
@ -192,7 +192,7 @@ export class Session {
/** /**
* Enable secret storage by providing the secret storage credential. * Enable secret storage by providing the secret storage credential.
* This will also see if there is a megolm key backup and try to enable that if so. * This will also see if there is a megolm key backup and try to enable that if so.
* *
* @param {string} type either "passphrase" or "recoverykey" * @param {string} type either "passphrase" or "recoverykey"
* @param {string} credential either the passphrase or the recovery key, depending on the type * @param {string} credential either the passphrase or the recovery key, depending on the type
* @return {Promise} resolves or rejects after having tried to enable secret storage * @return {Promise} resolves or rejects after having tried to enable secret storage
@ -663,7 +663,7 @@ export class Session {
if (this._e2eeAccount && deviceOneTimeKeysCount) { if (this._e2eeAccount && deviceOneTimeKeysCount) {
changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log); changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log);
} }
const deviceLists = syncResponse.device_lists; const deviceLists = syncResponse.device_lists;
if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) { if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) {
await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log)); await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log));
@ -908,7 +908,7 @@ export class Session {
Creates an empty (summary isn't loaded) the archived room if it isn't Creates an empty (summary isn't loaded) the archived room if it isn't
loaded already, assuming sync will either remove it (when rejoining) or loaded already, assuming sync will either remove it (when rejoining) or
write a full summary adopting it from the joined room when leaving write a full summary adopting it from the joined room when leaving
@internal @internal
*/ */
createOrGetArchivedRoomForSync(roomId) { createOrGetArchivedRoomForSync(roomId) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ObservableMap} from "../../../observable/map/ObservableMap"; import {ObservableMap} from "../../../observable";
import {RetainedValue} from "../../../utils/RetainedValue"; import {RetainedValue} from "../../../utils/RetainedValue";
export class MemberList extends RetainedValue { export class MemberList extends RetainedValue {

View File

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js"; import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable";
import {Disposables} from "../../../utils/Disposables"; import {Disposables} from "../../../utils/Disposables";
import {Direction} from "./Direction"; import {Direction} from "./Direction";
import {TimelineReader} from "./persistence/TimelineReader.js"; import {TimelineReader} from "./persistence/TimelineReader.js";
@ -45,7 +45,7 @@ export class Timeline {
}); });
this._readerRequest = null; this._readerRequest = null;
this._allEntries = null; this._allEntries = null;
/** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */ /** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */
this._contextEntriesNotInTimeline = new Map(); this._contextEntriesNotInTimeline = new Map();
/** Only used to decrypt non-persisted context entries fetched from the homeserver */ /** Only used to decrypt non-persisted context entries fetched from the homeserver */
this._decryptEntries = null; this._decryptEntries = null;
@ -189,7 +189,7 @@ export class Timeline {
// before it has any subscriptions, we bail out if this isn't // before it has any subscriptions, we bail out if this isn't
// the case yet. This can happen when sync adds or replaces entries // the case yet. This can happen when sync adds or replaces entries
// before load has finished and the view has subscribed to the timeline. // before load has finished and the view has subscribed to the timeline.
// //
// Once the subscription is setup, MappedList will set up the local // Once the subscription is setup, MappedList will set up the local
// relations as needed with _applyAndEmitLocalRelationChange, // relations as needed with _applyAndEmitLocalRelationChange,
// so we're not missing anything by bailing out. // so we're not missing anything by bailing out.
@ -239,7 +239,7 @@ export class Timeline {
if (err.name === "CompareError") { if (err.name === "CompareError") {
// see FragmentIdComparer, if the replacing entry is on a fragment // see FragmentIdComparer, if the replacing entry is on a fragment
// that is currently not loaded into the FragmentIdComparer, it will // that is currently not loaded into the FragmentIdComparer, it will
// throw a CompareError, and it means that the event is not loaded // throw a CompareError, and it means that the event is not loaded
// in the timeline (like when receiving a relation for an event // in the timeline (like when receiving a relation for an event
// that is not loaded in memory) so we can just drop this error as // that is not loaded in memory) so we can just drop this error as
// replacing an event that is not already loaded is a no-op. // replacing an event that is not already loaded is a no-op.
@ -311,7 +311,7 @@ export class Timeline {
* - timeline * - timeline
* - storage * - storage
* - homeserver * - homeserver
* @param {EventEntry[]} entries * @param {EventEntry[]} entries
*/ */
async _loadContextEntriesWhereNeeded(entries) { async _loadContextEntriesWhereNeeded(entries) {
for (const entry of entries) { for (const entry of entries) {
@ -392,7 +392,7 @@ export class Timeline {
* [loadAtTop description] * [loadAtTop description]
* @param {[type]} amount [description] * @param {[type]} amount [description]
* @return {boolean} true if the top of the timeline has been reached * @return {boolean} true if the top of the timeline has been reached
* *
*/ */
async loadAtTop(amount) { async loadAtTop(amount) {
if (this._disposables.isDisposed) { if (this._disposables.isDisposed) {
@ -547,7 +547,7 @@ export function tests() {
content: {}, content: {},
relatedEventId: event2.event_id relatedEventId: event2.event_id
}})); }}));
// 4. subscribe (it's now safe to iterate timeline.entries) // 4. subscribe (it's now safe to iterate timeline.entries)
timeline.entries.subscribe(new ListObserver()); timeline.entries.subscribe(new ListObserver());
// 5. check the local relation got correctly aggregated // 5. check the local relation got correctly aggregated
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting); const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);

View File

@ -18,14 +18,40 @@ import {SortedMapList} from "./list/SortedMapList.js";
import {FilteredMap} from "./map/FilteredMap.js"; import {FilteredMap} from "./map/FilteredMap.js";
import {MappedMap} from "./map/MappedMap.js"; import {MappedMap} from "./map/MappedMap.js";
import {JoinedMap} from "./map/JoinedMap.js"; import {JoinedMap} from "./map/JoinedMap.js";
import {BaseObservableMap} from "./map/BaseObservableMap"; import {BaseObservableMap, BaseObservableMapConfig} from "./map/BaseObservableMap";
import {ObservableMapInternal} from "./map/ObservableMap";
// re-export "root" (of chain) collections // re-export "root" (of chain) collections
export { ObservableArray } from "./list/ObservableArray"; export { ObservableArray } from "./list/ObservableArray";
export { SortedArray } from "./list/SortedArray"; export { SortedArray } from "./list/SortedArray";
export { MappedList } from "./list/MappedList"; export { MappedList } from "./list/MappedList";
export { AsyncMappedList } from "./list/AsyncMappedList"; export { AsyncMappedList } from "./list/AsyncMappedList";
export { ConcatList } from "./list/ConcatList"; export { ConcatList } from "./list/ConcatList";
export { ObservableMap } from "./map/ObservableMap";
// avoid circular dependency between these classes
// and BaseObservableMap (as they extend it)
function config<K, V>(): BaseObservableMapConfig<K, V> {
return {
join: (_this: BaseObservableMap<K, V>, ...otherMaps: Array<BaseObservableMap<K, V>>): JoinedMap => {
return new JoinedMap([_this].concat(otherMaps));
},
mapValues: (_this: BaseObservableMap<K, V>, mapper: any, updater?: (params: any) => void): MappedMap => {
return new MappedMap(_this, mapper, updater);
},
sortValues: (_this: BaseObservableMap<K, V>, comparator?: (a: any, b: any) => number): SortedMapList => {
return new SortedMapList(_this, comparator);
},
filterValues: (_this: BaseObservableMap<K, V>, filter: (v: V, k: K) => boolean): FilteredMap => {
return new FilteredMap(_this, filter);
}
};
};
export class ObservableMap<K, V> extends ObservableMapInternal<K, V> {
constructor(initialValues?: (readonly [K, V])[]) {
super(config<K, V>(), initialValues);
}
}
// avoid circular dependency between these classes // avoid circular dependency between these classes
// and BaseObservableMap (as they extend it) // and BaseObservableMap (as they extend it)
@ -45,4 +71,4 @@ Object.assign(BaseObservableMap.prototype, {
join(...otherMaps) { join(...otherMaps) {
return new JoinedMap([this].concat(otherMaps)); return new JoinedMap([this].concat(otherMaps));
} }
}); });

View File

@ -53,7 +53,7 @@ export class SortedMapList extends BaseObservableList {
this._sortedPairs = null; this._sortedPairs = null;
this._mapSubscription = null; this._mapSubscription = null;
} }
onAdd(key, value) { onAdd(key, value) {
const pair = {key, value}; const pair = {key, value};
const idx = sortedIndex(this._sortedPairs, pair, this._comparator); const idx = sortedIndex(this._sortedPairs, pair, this._comparator);
@ -133,7 +133,7 @@ export class SortedMapList extends BaseObservableList {
} }
} }
import {ObservableMap} from "../map/ObservableMap"; import {ObservableMap} from "../";
export function tests() { export function tests() {
return { return {

View File

@ -15,6 +15,10 @@ limitations under the License.
*/ */
import {BaseObservable} from "../BaseObservable"; import {BaseObservable} from "../BaseObservable";
import {JoinedMap} from "../map/JoinedMap.js";
import {MappedMap} from "../map/MappedMap.js";
import {FilteredMap} from "../map/FilteredMap.js";
import {SortedMapList} from "../list/SortedMapList.js";
export interface IMapObserver<K, V> { export interface IMapObserver<K, V> {
onReset(): void; onReset(): void;
@ -23,6 +27,13 @@ export interface IMapObserver<K, V> {
onRemove(key: K, value: V): void onRemove(key: K, value: V): void
} }
export type BaseObservableMapConfig<K, V> = {
join(_this: BaseObservableMap<K, V>, ...otherMaps: Array<BaseObservableMap<K, V>>): JoinedMap;
mapValues(_this: BaseObservableMap<K, V>, mapper: any, updater?: (params: any) => void): MappedMap;
sortValues(_this: BaseObservableMap<K, V>, comparator?: (a: any, b: any) => number): SortedMapList;
filterValues(_this: BaseObservableMap<K, V>, filter: (v: V, k: K) => boolean): FilteredMap;
}
export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserver<K, V>> { export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserver<K, V>> {
emitReset() { emitReset() {
for(let h of this._handlers) { for(let h of this._handlers) {
@ -49,6 +60,10 @@ export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserve
} }
} }
abstract join(...otherMaps: Array<typeof this>): JoinedMap;
abstract mapValues(mapper: any, updater?: (params: any) => void): MappedMap;
abstract sortValues(comparator?: (a: any, b: any) => number): SortedMapList;
abstract filterValues(filter: (v: V, k: K) => boolean): FilteredMap;
abstract [Symbol.iterator](): Iterator<[K, V]>; abstract [Symbol.iterator](): Iterator<[K, V]>;
abstract get size(): number; abstract get size(): number;
abstract get(key: K): V | undefined; abstract get(key: K): V | undefined;

View File

@ -166,7 +166,7 @@ class FilterIterator {
} }
} }
import {ObservableMap} from "./ObservableMap"; import {ObservableMap} from "../";
export function tests() { export function tests() {
return { return {
"filter preloaded list": assert => { "filter preloaded list": assert => {

View File

@ -191,7 +191,7 @@ class SourceSubscriptionHandler {
} }
import { ObservableMap } from "./ObservableMap"; import {ObservableMap} from "../";
export function tests() { export function tests() {

View File

@ -14,16 +14,38 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseObservableMap} from "./BaseObservableMap"; import {BaseObservableMap, BaseObservableMapConfig} from "./BaseObservableMap";
import {JoinedMap} from "../map/JoinedMap.js";
import {MappedMap} from "../map/MappedMap.js";
import {FilteredMap} from "../map/FilteredMap.js";
import {SortedMapList} from "../list/SortedMapList.js";
export class ObservableMap<K, V> extends BaseObservableMap<K, V> { export class ObservableMapInternal<K, V> extends BaseObservableMap<K, V> {
private _config: BaseObservableMapConfig<K, V>
private readonly _values: Map<K, V>; private readonly _values: Map<K, V>;
constructor(initialValues?: (readonly [K, V])[]) { constructor(config: BaseObservableMapConfig<K, V>, initialValues?: (readonly [K, V])[]) {
super(); super();
this._config = config;
this._values = new Map(initialValues); this._values = new Map(initialValues);
} }
join(...otherMaps: Array<typeof this>): JoinedMap {
return this._config.join(this, ...otherMaps);
}
mapValues(mapper: any, updater?: (params: any) => void): MappedMap{
return this._config.mapValues(this, mapper, updater);
}
sortValues(comparator?: (a: any, b: any) => number): SortedMapList {
return this._config.sortValues(this, comparator);
}
filterValues(filter: (v: V, k: K) => boolean): FilteredMap {
return this._config.filterValues(this, filter);
}
update(key: K, params?: any): boolean { update(key: K, params?: any): boolean {
const value = this._values.get(key); const value = this._values.get(key);
if (value !== undefined) { if (value !== undefined) {
@ -61,7 +83,7 @@ export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
// We set the value here because update only supports inline updates // We set the value here because update only supports inline updates
this._values.set(key, value); this._values.set(key, value);
return this.update(key, undefined); return this.update(key, undefined);
} }
else { else {
return this.add(key, value); return this.add(key, value);
} }
@ -91,139 +113,139 @@ export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
keys(): Iterator<K> { keys(): Iterator<K> {
return this._values.keys(); return this._values.keys();
} }
} };
export function tests() { // export function tests() {
return { // return {
test_initial_values(assert) { // test_initial_values(assert) {
const map = new ObservableMap([ // const map = new ObservableMap([
["a", 5], // ["a", 5],
["b", 10] // ["b", 10]
]); // ]);
assert.equal(map.size, 2); // assert.equal(map.size, 2);
assert.equal(map.get("a"), 5); // assert.equal(map.get("a"), 5);
assert.equal(map.get("b"), 10); // assert.equal(map.get("b"), 10);
}, // },
test_add(assert) { // test_add(assert) {
let fired = 0; // let fired = 0;
const map = new ObservableMap<number, {value: number}>(); // const map = new ObservableMap<number, {value: number}>();
map.subscribe({ // map.subscribe({
onAdd(key, value) { // onAdd(key, value) {
fired += 1; // fired += 1;
assert.equal(key, 1); // assert.equal(key, 1);
assert.deepEqual(value, {value: 5}); // assert.deepEqual(value, {value: 5});
}, // },
onUpdate() {}, // onUpdate() {},
onRemove() {}, // onRemove() {},
onReset() {} // onReset() {}
}); // });
map.add(1, {value: 5}); // map.add(1, {value: 5});
assert.equal(map.size, 1); // assert.equal(map.size, 1);
assert.equal(fired, 1); // assert.equal(fired, 1);
}, // },
test_update(assert) { // test_update(assert) {
let fired = 0; // let fired = 0;
const map = new ObservableMap<number, {number: number}>(); // const map = new ObservableMap<number, {number: number}>();
const value = {number: 5}; // const value = {number: 5};
map.add(1, value); // map.add(1, value);
map.subscribe({ // map.subscribe({
onUpdate(key, value, params) { // onUpdate(key, value, params) {
fired += 1; // fired += 1;
assert.equal(key, 1); // assert.equal(key, 1);
assert.deepEqual(value, {number: 6}); // assert.deepEqual(value, {number: 6});
assert.equal(params, "test"); // assert.equal(params, "test");
}, // },
onAdd() {}, // onAdd() {},
onRemove() {}, // onRemove() {},
onReset() {} // onReset() {}
}); // });
value.number = 6; // value.number = 6;
map.update(1, "test"); // map.update(1, "test");
assert.equal(fired, 1); // assert.equal(fired, 1);
}, // },
test_update_unknown(assert) { // test_update_unknown(assert) {
let fired = 0; // let fired = 0;
const map = new ObservableMap<number, {number: number}>(); // const map = new ObservableMap<number, {number: number}>();
map.subscribe({ // map.subscribe({
onUpdate() { fired += 1; }, // onUpdate() { fired += 1; },
onAdd() {}, // onAdd() {},
onRemove() {}, // onRemove() {},
onReset() {} // onReset() {}
}); // });
const result = map.update(1); // const result = map.update(1);
assert.equal(fired, 0); // assert.equal(fired, 0);
assert.equal(result, false); // assert.equal(result, false);
}, // },
test_set(assert) { // test_set(assert) {
let add_fired = 0, update_fired = 0; // let add_fired = 0, update_fired = 0;
const map = new ObservableMap<number, {value: number}>(); // const map = new ObservableMap<number, {value: number}>();
map.subscribe({ // map.subscribe({
onAdd(key, value) { // onAdd(key, value) {
add_fired += 1; // add_fired += 1;
assert.equal(key, 1); // assert.equal(key, 1);
assert.deepEqual(value, {value: 5}); // assert.deepEqual(value, {value: 5});
}, // },
onUpdate(key, value/*, params*/) { // onUpdate(key, value/*, params*/) {
update_fired += 1; // update_fired += 1;
assert.equal(key, 1); // assert.equal(key, 1);
assert.deepEqual(value, {value: 7}); // assert.deepEqual(value, {value: 7});
}, // },
onRemove() {}, // onRemove() {},
onReset() {} // onReset() {}
}); // });
// Add // // Add
map.set(1, {value: 5}); // map.set(1, {value: 5});
assert.equal(map.size, 1); // assert.equal(map.size, 1);
assert.equal(add_fired, 1); // assert.equal(add_fired, 1);
// Update // // Update
map.set(1, {value: 7}); // map.set(1, {value: 7});
assert.equal(map.size, 1); // assert.equal(map.size, 1);
assert.equal(update_fired, 1); // assert.equal(update_fired, 1);
}, // },
test_remove(assert) { // test_remove(assert) {
let fired = 0; // let fired = 0;
const map = new ObservableMap<number, {value: number}>(); // const map = new ObservableMap<number, {value: number}>();
const value = {value: 5}; // const value = {value: 5};
map.add(1, value); // map.add(1, value);
map.subscribe({ // map.subscribe({
onRemove(key, value) { // onRemove(key, value) {
fired += 1; // fired += 1;
assert.equal(key, 1); // assert.equal(key, 1);
assert.deepEqual(value, {value: 5}); // assert.deepEqual(value, {value: 5});
}, // },
onAdd() {}, // onAdd() {},
onUpdate() {}, // onUpdate() {},
onReset() {} // onReset() {}
}); // });
map.remove(1); // map.remove(1);
assert.equal(map.size, 0); // assert.equal(map.size, 0);
assert.equal(fired, 1); // assert.equal(fired, 1);
}, // },
test_iterate(assert) { // test_iterate(assert) {
const results: any[] = []; // const results: any[] = [];
const map = new ObservableMap<number, {number: number}>(); // const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5}); // map.add(1, {number: 5});
map.add(2, {number: 6}); // map.add(2, {number: 6});
map.add(3, {number: 7}); // map.add(3, {number: 7});
for (let e of map) { // for (let e of map) {
results.push(e); // results.push(e);
} // }
assert.equal(results.length, 3); // assert.equal(results.length, 3);
assert.equal(results.find(([key]) => key === 1)[1].number, 5); // assert.equal(results.find(([key]) => key === 1)[1].number, 5);
assert.equal(results.find(([key]) => key === 2)[1].number, 6); // assert.equal(results.find(([key]) => key === 2)[1].number, 6);
assert.equal(results.find(([key]) => key === 3)[1].number, 7); // assert.equal(results.find(([key]) => key === 3)[1].number, 7);
}, // },
test_size(assert) { // test_size(assert) {
const map = new ObservableMap<number, {number: number}>(); // const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5}); // map.add(1, {number: 5});
map.add(2, {number: 6}); // map.add(2, {number: 6});
assert.equal(map.size, 2); // assert.equal(map.size, 2);
}, // },
} // }
} // }