Merge pull request #793 from ibeckermayer/ibeckermayer/ts-conversion-observable-map

Typescript conversion for `src/observable/map`
This commit is contained in:
R Midhun Suresh 2022-10-10 11:31:25 +05:30 committed by GitHub
commit 8ef163353a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 554 additions and 387 deletions

View File

@ -20,6 +20,10 @@ module.exports = {
rules: {
"@typescript-eslint/no-floating-promises": 2,
"@typescript-eslint/no-misused-promises": 2,
"semi": ["error", "always"]
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["warn"],
"no-undef": "off",
"semi": ["error", "always"],
"@typescript-eslint/explicit-function-return-type": ["error"]
}
};

View File

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

View File

@ -20,8 +20,8 @@ import {RoomTileViewModel} from "./RoomTileViewModel.js";
import {InviteTileViewModel} from "./InviteTileViewModel.js";
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
import {RoomFilter} from "./RoomFilter.js";
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
import {addPanelIfNeeded} from "../../navigation/index";
import {ApplyMap} from "../../../observable";
import {addPanelIfNeeded} from "../../navigation";
export class LeftPanelViewModel extends ViewModel {
constructor(options) {

View File

@ -46,8 +46,8 @@ export class MemberListViewModel extends ViewModel {
const vm = new MemberTileViewModel(this.childOptions({member, emitChange, mediaRepository}));
this.nameDisambiguator.disambiguate(vm);
return vm;
}
const updater = (vm, params, newMember) => {
};
const updater = (params, vm, newMember) => {
vm.updateFrom(newMember);
this.nameDisambiguator.disambiguate(vm);
};

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
limitations under the License.
*/
import {ObservableMap} from "../../../../observable/map/ObservableMap";
import {ObservableMap} from "../../../../observable";
export class ReactionsViewModel {
constructor(parentTile) {

View File

@ -78,8 +78,7 @@ export {
MappedList,
AsyncMappedList,
ConcatList,
ObservableMap
} from "./observable/index";
} from "./observable";
export {
BaseObservableValue,
ObservableValue,

View File

@ -21,7 +21,7 @@ import {RoomStatus} from "./room/common";
import {RoomBeingCreated} from "./room/RoomBeingCreated";
import {Invite} from "./room/Invite.js";
import {Pusher} from "./push/Pusher";
import { ObservableMap } from "../observable/index.js";
import {ObservableMap} from "../observable";
import {User} from "./User.js";
import {DeviceMessageHandler} from "./DeviceMessageHandler.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.
* 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} credential either the passphrase or the recovery key, depending on the type
* @return {Promise} resolves or rejects after having tried to enable secret storage
@ -663,7 +663,7 @@ export class Session {
if (this._e2eeAccount && deviceOneTimeKeysCount) {
changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log);
}
const deviceLists = syncResponse.device_lists;
if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) {
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
loaded already, assuming sync will either remove it (when rejoining) or
write a full summary adopting it from the joined room when leaving
@internal
*/
createOrGetArchivedRoomForSync(roomId) {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {ObservableMap} from "../../../observable/map/ObservableMap";
import {ObservableMap} from "../../../observable";
import {RetainedValue} from "../../../utils/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.
*/
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable";
import {Disposables} from "../../../utils/Disposables";
import {Direction} from "./Direction";
import {TimelineReader} from "./persistence/TimelineReader.js";
@ -45,7 +45,7 @@ export class Timeline {
});
this._readerRequest = 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();
/** Only used to decrypt non-persisted context entries fetched from the homeserver */
this._decryptEntries = null;
@ -189,7 +189,7 @@ export class Timeline {
// before it has any subscriptions, we bail out if this isn't
// the case yet. This can happen when sync adds or replaces entries
// before load has finished and the view has subscribed to the timeline.
//
//
// Once the subscription is setup, MappedList will set up the local
// relations as needed with _applyAndEmitLocalRelationChange,
// so we're not missing anything by bailing out.
@ -239,7 +239,7 @@ export class Timeline {
if (err.name === "CompareError") {
// see FragmentIdComparer, if the replacing entry is on a fragment
// 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
// 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.
@ -311,7 +311,7 @@ export class Timeline {
* - timeline
* - storage
* - homeserver
* @param {EventEntry[]} entries
* @param {EventEntry[]} entries
*/
async _loadContextEntriesWhereNeeded(entries) {
for (const entry of entries) {
@ -392,7 +392,7 @@ export class Timeline {
* [loadAtTop description]
* @param {[type]} amount [description]
* @return {boolean} true if the top of the timeline has been reached
*
*
*/
async loadAtTop(amount) {
if (this._disposables.isDisposed) {
@ -547,7 +547,7 @@ export function tests() {
content: {},
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());
// 5. check the local relation got correctly aggregated
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);

View File

@ -34,7 +34,7 @@ export abstract class BaseObservable<T> {
if (this._handlers.size === 1) {
this.onSubscribeFirst();
}
return () => {
return (): undefined => {
return this.unsubscribe(handler);
};
}
@ -63,22 +63,23 @@ export abstract class BaseObservable<T> {
// Add iterator over handlers here
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
class Collection extends BaseObservable<{}> {
firstSubscribeCalls: number = 0;
firstUnsubscribeCalls: number = 0;
onSubscribeFirst() { this.firstSubscribeCalls += 1; }
onUnsubscribeLast() { this.firstUnsubscribeCalls += 1; }
onSubscribeFirst(): void { this.firstSubscribeCalls += 1; }
onUnsubscribeLast(): void { this.firstUnsubscribeCalls += 1; }
}
return {
test_unsubscribe(assert) {
test_unsubscribe(assert): void {
const c = new Collection();
const unsubscribe = c.subscribe({});
unsubscribe();
assert.equal(c.firstSubscribeCalls, 1);
assert.equal(c.firstUnsubscribeCalls, 1);
}
}
};
}

View File

@ -20,7 +20,7 @@ import type {SubscriptionHandle} from "./BaseObservable";
// like an EventEmitter, but doesn't have an event type
export abstract class BaseObservableValue<T> extends BaseObservable<(value: T) => void> {
emit(argument: T) {
emit(argument: T): void {
for (const h of this._handlers) {
h(argument);
}
@ -68,7 +68,7 @@ class WaitForHandle<T> implements IWaitHandle<T> {
return this._promise;
}
dispose() {
dispose(): void {
if (this._subscription) {
this._subscription();
this._subscription = null;
@ -82,7 +82,7 @@ class WaitForHandle<T> implements IWaitHandle<T> {
class ResolvedWaitForHandle<T> implements IWaitHandle<T> {
constructor(public promise: Promise<T>) {}
dispose() {}
dispose(): void {}
}
export class ObservableValue<T> extends BaseObservableValue<T> {
@ -113,7 +113,7 @@ export class RetainedObservableValue<T> extends ObservableValue<T> {
this._freeCallback = freeCallback;
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
this._freeCallback();
}
@ -130,7 +130,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
super();
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
this.sourceSubscription = this.sourceSubscription!();
if (this.targetSubscription) {
@ -138,7 +138,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
}
}
onSubscribeFirst() {
onSubscribeFirst(): void {
super.onSubscribeFirst();
this.sourceSubscription = this.source.subscribe(() => {
this.updateTargetSubscription();
@ -147,7 +147,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
this.updateTargetSubscription();
}
private updateTargetSubscription() {
private updateTargetSubscription(): void {
const sourceValue = this.source.get();
if (sourceValue) {
const target = this.mapper(sourceValue);
@ -174,9 +174,10 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
return {
"set emits an update": assert => {
"set emits an update": (assert): void => {
const a = new ObservableValue<number>(0);
let fired = false;
const subscription = a.subscribe(v => {
@ -187,7 +188,7 @@ export function tests() {
assert(fired);
subscription();
},
"set doesn't emit if value hasn't changed": assert => {
"set doesn't emit if value hasn't changed": (assert): void => {
const a = new ObservableValue(5);
let fired = false;
const subscription = a.subscribe(() => {
@ -198,24 +199,24 @@ export function tests() {
assert(!fired);
subscription();
},
"waitFor promise resolves on matching update": async assert => {
"waitFor promise resolves on matching update": async (assert): Promise<void> => {
const a = new ObservableValue(5);
const handle = a.waitFor(v => v === 6);
Promise.resolve().then(() => {
await Promise.resolve().then(() => {
a.set(6);
});
await handle.promise;
assert.strictEqual(a.get(), 6);
},
"waitFor promise rejects when disposed": async assert => {
"waitFor promise rejects when disposed": async (assert): Promise<void> => {
const a = new ObservableValue<number>(0);
const handle = a.waitFor(() => false);
Promise.resolve().then(() => {
await Promise.resolve().then(() => {
handle.dispose();
});
await assert.rejects(handle.promise, AbortError);
},
"flatMap.get": assert => {
"flatMap.get": (assert): void => {
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
const countProxy = a.flatMap(a => a!.count);
assert.strictEqual(countProxy.get(), undefined);
@ -223,7 +224,7 @@ export function tests() {
a.set({count});
assert.strictEqual(countProxy.get(), 0);
},
"flatMap update from source": assert => {
"flatMap update from source": (assert): void => {
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
const updates: (number | undefined)[] = [];
a.flatMap(a => a!.count).subscribe(count => {
@ -233,7 +234,7 @@ export function tests() {
a.set({count});
assert.deepEqual(updates, [0]);
},
"flatMap update from target": assert => {
"flatMap update from target": (assert): void => {
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
const updates: (number | undefined)[] = [];
a.flatMap(a => a!.count).subscribe(count => {
@ -244,5 +245,5 @@ export function tests() {
count.set(5);
assert.deepEqual(updates, [0, 5]);
}
}
};
}

View File

@ -1,48 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {SortedMapList} from "./list/SortedMapList.js";
import {FilteredMap} from "./map/FilteredMap.js";
import {MappedMap} from "./map/MappedMap.js";
import {JoinedMap} from "./map/JoinedMap.js";
import {BaseObservableMap} from "./map/BaseObservableMap";
// re-export "root" (of chain) collections
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";
// avoid circular dependency between these classes
// and BaseObservableMap (as they extend it)
Object.assign(BaseObservableMap.prototype, {
sortValues(comparator) {
return new SortedMapList(this, comparator);
},
mapValues(mapper, updater) {
return new MappedMap(this, mapper, updater);
},
filterValues(filter) {
return new FilteredMap(this, filter);
},
join(...otherMaps) {
return new JoinedMap([this].concat(otherMaps));
}
});

24
src/observable/index.ts Normal file
View File

@ -0,0 +1,24 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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.
*/
// re-export "root" (of chain) collection
export { ObservableMap, ApplyMap, FilteredMap, JoinedMap, LogMap, MappedMap } from "./map";
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";

View File

@ -16,7 +16,7 @@ limitations under the License.
*/
import {IListObserver} from "./BaseObservableList";
import {BaseMappedList, Mapper, Updater, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> implements IListObserver<F> {
private _eventQueue: AsyncEvent<F>[] | null = null;
@ -31,7 +31,7 @@ export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> impleme
this._eventQueue.push(new AddEvent(idx, item));
idx += 1;
}
this._flush();
void this._flush();
}
async _flush(): Promise<void> {
@ -52,35 +52,35 @@ export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> impleme
onReset(): void {
if (this._eventQueue) {
this._eventQueue.push(new ResetEvent());
this._flush();
void this._flush();
}
}
onAdd(index: number, value: F): void {
if (this._eventQueue) {
this._eventQueue.push(new AddEvent(index, value));
this._flush();
void this._flush();
}
}
onUpdate(index: number, value: F, params: any): void {
if (this._eventQueue) {
this._eventQueue.push(new UpdateEvent(index, value, params));
this._flush();
void this._flush();
}
}
onRemove(index: number): void {
if (this._eventQueue) {
this._eventQueue.push(new RemoveEvent(index));
this._flush();
void this._flush();
}
}
onMove(fromIdx: number, toIdx: number): void {
if (this._eventQueue) {
this._eventQueue.push(new MoveEvent(fromIdx, toIdx));
this._flush();
void this._flush();
}
}
@ -135,10 +135,12 @@ class ResetEvent<F> {
import {ObservableArray} from "./ObservableArray";
import {ListObserver} from "../../mocks/ListObserver.js";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
return {
"events are emitted in order": async assert => {
const double = n => n * n;
"events are emitted in order": async (assert): Promise<void> => {
const double = (n: number): number => n * n;
const source = new ObservableArray<number>();
const mapper = new AsyncMappedList(source, async n => {
await new Promise(r => setTimeout(r, n));
@ -150,7 +152,7 @@ export function tests() {
mapper.subscribe(observer);
source.append(2); // will sleep this amount, so second append would take less time
source.append(1);
source.update(0, 7, "lucky seven")
source.update(0, 7, "lucky seven");
source.remove(0);
{
const {type, index, value} = await observer.next();
@ -182,5 +184,5 @@ export function tests() {
assert.equal(value.n, 49);
}
}
}
};
}

View File

@ -37,15 +37,15 @@ export class BaseMappedList<F,T,R = T> extends BaseObservableList<T> {
this._removeCallback = removeCallback;
}
findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false) {
findAndUpdate(predicate: (value: T) => boolean, updater: (value: T) => any | false): boolean {
return findAndUpdateInArray(predicate, this._mappedValues!, this, updater);
}
get length() {
get length(): number {
return this._mappedValues!.length;
}
[Symbol.iterator]() {
[Symbol.iterator](): IterableIterator<T> {
return this._mappedValues!.values();
}
}

View File

@ -26,17 +26,17 @@ export interface IListObserver<T> {
export function defaultObserverWith<T>(overrides: { [key in keyof IListObserver<T>]?: IListObserver<T>[key] }): IListObserver<T> {
const defaults = {
onReset(){},
onAdd(){},
onUpdate(){},
onRemove(){},
onMove(){},
}
onReset(): void {},
onAdd(): void {},
onUpdate(): void {},
onRemove(): void {},
onMove(): void {},
};
return Object.assign(defaults, overrides);
}
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> implements Iterable<T> {
emitReset() {
emitReset(): void {
for(let h of this._handlers) {
h.onReset(this);
}

View File

@ -47,7 +47,7 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
onReset(): void {
// TODO: not ideal if other source lists are large
// but working impl for now
// reset, and
// reset, and
this.emitReset();
let idx = 0;
for(const item of this) {
@ -86,11 +86,11 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
return len;
}
[Symbol.iterator]() {
[Symbol.iterator](): Iterator<T> {
let sourceListIdx = 0;
let it = this._sourceLists[0][Symbol.iterator]();
return {
next: () => {
next: (): IteratorResult<T> => {
let result = it.next();
while (result.done) {
sourceListIdx += 1;
@ -102,22 +102,25 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
}
return result;
}
}
};
}
}
import {ObservableArray} from "./ObservableArray";
import {defaultObserverWith} from "./BaseObservableList";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function tests() {
return {
test_length(assert) {
test_length(assert): void {
const all = new ConcatList(
new ObservableArray([1, 2, 3]),
new ObservableArray([11, 12, 13])
);
assert.equal(all.length, 6);
},
test_iterator(assert) {
test_iterator(assert): void {
const all = new ConcatList(
new ObservableArray([1, 2, 3]),
new ObservableArray([11, 12, 13])
@ -131,7 +134,7 @@ export async function tests() {
assert.equal(it.next().value, 13);
assert(it.next().done);
},
test_add(assert) {
test_add(assert): void {
const list1 = new ObservableArray([1, 2, 3]);
const list2 = new ObservableArray([11, 12, 13]);
const all = new ConcatList(list1, list2);
@ -146,7 +149,7 @@ export async function tests() {
list2.insert(1, 11.5);
assert(fired);
},
test_update(assert) {
test_update(assert): void {
const list1 = new ObservableArray([1, 2, 3]);
const list2 = new ObservableArray([11, 12, 13]);
const all = new ConcatList(list1, list2);

View File

@ -19,7 +19,7 @@ import {IListObserver} from "./BaseObservableList";
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
export class MappedList<F,T> extends BaseMappedList<F,T> implements IListObserver<F> {
onSubscribeFirst() {
onSubscribeFirst(): void {
this._sourceUnsubscribe = this._sourceList.subscribe(this);
this._mappedValues = [];
for (const item of this._sourceList) {
@ -61,18 +61,21 @@ import {ObservableArray} from "./ObservableArray";
import {BaseObservableList} from "./BaseObservableList";
import {defaultObserverWith} from "./BaseObservableList";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function tests() {
class MockList extends BaseObservableList<number> {
get length() {
get length(): 0 {
return 0;
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
[Symbol.iterator]() {
return [].values();
}
}
return {
test_add(assert) {
test_add(assert): void {
const source = new MockList();
const mapped = new MappedList(source, n => {return {n: n*n};});
let fired = false;
@ -87,7 +90,7 @@ export async function tests() {
assert(fired);
unsubscribe();
},
test_update(assert) {
test_update(assert): void {
const source = new MockList();
const mapped = new MappedList<number, { n: number, m?: number }>(
source,
@ -109,7 +112,7 @@ export async function tests() {
assert(fired);
unsubscribe();
},
"test findAndUpdate not found": assert => {
"test findAndUpdate not found": (assert): void => {
const source = new ObservableArray([1, 3, 4]);
const mapped = new MappedList(
source,
@ -123,7 +126,7 @@ export async function tests() {
() => assert.fail()
), false);
},
"test findAndUpdate found but updater bails out of update": assert => {
"test findAndUpdate found but updater bails out of update": (assert): void => {
const source = new ObservableArray([1, 3, 4]);
const mapped = new MappedList(
source,
@ -143,7 +146,7 @@ export async function tests() {
), true);
assert.equal(fired, true);
},
"test findAndUpdate emits update": assert => {
"test findAndUpdate emits update": (assert): void => {
const source = new ObservableArray([1, 3, 4]);
const mapped = new MappedList(
source,
@ -161,6 +164,6 @@ export async function tests() {
assert.equal(mapped.findAndUpdate(n => n === 9, () => "param"), true);
assert.equal(fired, true);
},
};
}

View File

@ -75,7 +75,7 @@ export class ObservableArray<T> extends BaseObservableList<T> {
return this._items.length;
}
[Symbol.iterator]() {
[Symbol.iterator](): IterableIterator<T> {
return this._items.values();
}
}

View File

@ -87,7 +87,7 @@ export class SortedArray<T> extends BaseObservableList<T> {
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);
this.emitAdd(idx, item)
this.emitAdd(idx, item);
} else {
this._items[idx] = item;
this.emitUpdate(idx, item, updateParams);
@ -112,52 +112,46 @@ export class SortedArray<T> extends BaseObservableList<T> {
return this._items.length;
}
[Symbol.iterator]() {
[Symbol.iterator](): Iterator<T> {
return new Iterator(this);
}
}
// iterator that works even if the current value is removed while iterating
class Iterator<T> {
private _sortedArray: SortedArray<T> | null
private _current: T | null | undefined
private _sortedArray: SortedArray<T>;
private _current: T | null | undefined;
private _consumed: boolean = false;
constructor(sortedArray: SortedArray<T>) {
this._sortedArray = sortedArray;
this._current = null;
}
next() {
if (this._sortedArray) {
if (this._current) {
this._current = this._sortedArray._getNext(this._current);
} else {
this._current = this._sortedArray.get(0);
}
if (this._current) {
return {value: this._current};
} else {
// cause done below
this._sortedArray = null;
}
next(): IteratorResult<T> {
if (this._consumed) {
return {value: undefined, done: true};
}
if (!this._sortedArray) {
return {done: true};
this._current = this._current? this._sortedArray._getNext(this._current): this._sortedArray.get(0);
if (!this._current) {
this._consumed = true;
}
return { value: this._current, done: this._consumed } as IteratorResult<T>;
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
return {
"setManyUnsorted": assert => {
"setManyUnsorted": (assert): void => {
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");
assert.equal(sa.get(1), "b");
assert.equal(sa.get(2), "c");
},
"_getNext": assert => {
},
"_getNext": (assert): void => {
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
sa.setManyUnsorted(["b", "a", "f"]);
assert.equal(sa._getNext("a"), "b");
@ -166,7 +160,7 @@ export function tests() {
assert.equal(sa._getNext("c"), "f");
assert.equal(sa._getNext("f"), undefined);
},
"iterator with removals": assert => {
"iterator with removals": (assert): void => {
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]();
@ -183,5 +177,5 @@ export function tests() {
// check done persists
assert.equal(it.next().done, true);
}
}
};
}

View File

@ -53,7 +53,7 @@ export class SortedMapList extends BaseObservableList {
this._sortedPairs = null;
this._mapSubscription = null;
}
onAdd(key, value) {
const pair = {key, value};
const idx = sortedIndex(this._sortedPairs, pair, this._comparator);
@ -129,11 +129,11 @@ export class SortedMapList extends BaseObservableList {
}
return v;
}
}
};
}
}
import {ObservableMap} from "../map/ObservableMap";
import {ObservableMap} from "../";
export function tests() {
return {
@ -267,5 +267,5 @@ export function tests() {
assert.equal(updateFired, 1);
assert.deepEqual(Array.from(list).map(v => v.number), [1, 3, 11]);
},
}
};
}

View File

@ -17,7 +17,12 @@ limitations under the License.
import {BaseObservableList} from "./BaseObservableList";
/* inline update of item in collection backed by array, without replacing the preexising item */
export function findAndUpdateInArray<T>(predicate: (value: T) => boolean, array: T[], observable: BaseObservableList<T>, updater: (value: T) => any | false) {
export function findAndUpdateInArray<T>(
predicate: (value: T) => boolean,
array: T[],
observable: BaseObservableList<T>,
updater: (value: T) => any | false
): boolean {
const index = array.findIndex(predicate);
if (index !== -1) {
const value = array[index];

View File

@ -14,52 +14,62 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap";
import {BaseObservableMap} from "./index";
import {SubscriptionHandle} from "../BaseObservable";
export class ApplyMap extends BaseObservableMap {
constructor(source, apply) {
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class ApplyMap<K, V> extends BaseObservableMap<K, V> {
private _source: BaseObservableMap<K, V>;
private _subscription?: SubscriptionHandle;
private _apply?: Apply<K, V>;
constructor(source: BaseObservableMap<K, V>, apply?: Apply<K, V>) {
super();
this._source = source;
this._apply = apply;
this._subscription = null;
}
hasApply() {
hasApply(): boolean {
return !!this._apply;
}
setApply(apply) {
setApply(apply?: Apply<K, V>): void {
this._apply = apply;
if (apply) {
if (this._apply) {
this.applyOnce(this._apply);
}
}
applyOnce(apply) {
applyOnce(apply: Apply<K, V>): void {
for (const [key, value] of this._source) {
apply(key, value);
}
}
onAdd(key, value) {
onAdd(key: K, value: V): void {
if (this._apply) {
this._apply(key, value);
}
this.emitAdd(key, value);
}
onRemove(key, value) {
onRemove(key: K, value: V): void {
this.emitRemove(key, value);
}
onUpdate(key, value, params) {
onUpdate(key: K, value: V, params: any): void {
if (this._apply) {
this._apply(key, value, params);
}
this.emitUpdate(key, value, params);
}
onSubscribeFirst() {
onSubscribeFirst(): void {
this._subscription = this._source.subscribe(this);
if (this._apply) {
this.applyOnce(this._apply);
@ -67,27 +77,31 @@ export class ApplyMap extends BaseObservableMap {
super.onSubscribeFirst();
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
this._subscription = this._subscription();
if (this._subscription) {
this._subscription = this._subscription();
}
}
onReset() {
onReset(): void {
if (this._apply) {
this.applyOnce(this._apply);
}
this.emitReset();
}
[Symbol.iterator]() {
[Symbol.iterator](): Iterator<[K, V]> {
return this._source[Symbol.iterator]();
}
get size() {
get size(): number {
return this._source.size;
}
get(key) {
get(key: K): V | undefined {
return this._source.get(key);
}
}
type Apply<K, V> = (key: K, value: V, params?: any) => void;

View File

@ -15,6 +15,11 @@ limitations under the License.
*/
import {BaseObservable} from "../BaseObservable";
import {JoinedMap} from "./index";
import {MappedMap} from "./index";
import {FilteredMap} from "./index";
import {SortedMapList} from "../list/SortedMapList.js";
export interface IMapObserver<K, V> {
onReset(): void;
@ -23,33 +28,70 @@ export interface IMapObserver<K, V> {
onRemove(key: K, value: V): void
}
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserver<K, V>> {
emitReset() {
constructor() {
super();
}
emitReset(): void {
for(let h of this._handlers) {
h.onReset();
}
}
// we need batch events, mostly on index based collection though?
// maybe we should get started without?
emitAdd(key: K, value: V) {
emitAdd(key: K, value: V): void {
for(let h of this._handlers) {
h.onAdd(key, value);
}
}
emitUpdate(key, value, params) {
emitUpdate(key: K, value: V, params: any): void {
for(let h of this._handlers) {
h.onUpdate(key, value, params);
}
}
emitRemove(key, value) {
emitRemove(key: K, value: V): void {
for(let h of this._handlers) {
h.onRemove(key, value);
}
}
join(...otherMaps: Array<typeof this>): JoinedMap<K, V> {
return new JoinedMap([this].concat(otherMaps));
}
mapValues<MappedV>(mapper: Mapper<V, MappedV>, updater?: Updater<V, MappedV>): MappedMap<K, V, MappedV> {
return new MappedMap(this, mapper, updater);
}
sortValues(comparator: Comparator<V>): SortedMapList {
return new SortedMapList(this, comparator);
}
filterValues(filter: Filter<K, V>): FilteredMap<K, V> {
return new FilteredMap(this, filter);
}
abstract [Symbol.iterator](): Iterator<[K, V]>;
abstract get size(): number;
abstract get(key: K): V | undefined;
}
export type Mapper<V, MappedV> = (
value: V,
emitSpontaneousUpdate: any,
) => MappedV;
export type Updater<V, MappedV> = (params: any, mappedValue?: MappedV, value?: V) => void;
export type Comparator<V> = (a: V, b: V) => number;
export type Filter<K, V> = (v: V, k: K) => boolean;

View File

@ -14,19 +14,28 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap";
import {BaseObservableMap, Filter} from "./index";
import {SubscriptionHandle} from "../BaseObservable";
export class FilteredMap extends BaseObservableMap {
constructor(source, filter) {
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class FilteredMap<K, V> extends BaseObservableMap<K, V> {
private _source: BaseObservableMap<K, V>;
private _filter: Filter<K, V>;
private _included?: Map<K, boolean>;
private _subscription?: SubscriptionHandle;
constructor(source: BaseObservableMap<K, V>, filter: Filter<K, V>) {
super();
this._source = source;
this._filter = filter;
/** @type {Map<string, bool>} */
this._included = null;
this._subscription = null;
}
setFilter(filter) {
setFilter(filter: Filter<K, V>): void {
this._filter = filter;
if (this._subscription) {
this._reapplyFilter();
@ -36,7 +45,7 @@ export class FilteredMap extends BaseObservableMap {
/**
* reapply the filter
*/
_reapplyFilter(silent = false) {
_reapplyFilter(silent = false): void {
if (this._filter) {
const oldIncluded = this._included;
this._included = this._included || new Map();
@ -58,30 +67,38 @@ export class FilteredMap extends BaseObservableMap {
}
}
}
this._included = null;
this._included = undefined;
}
}
onAdd(key, value) {
onAdd(key: K, value: V): void {
if (this._filter) {
const included = this._filter(value, key);
this._included.set(key, included);
if (!included) {
return;
if (this._included) {
const included = this._filter(value, key);
this._included.set(key, included);
if (!included) {
return;
}
} else {
throw new Error("Internal logic error: FilteredMap._included used before initialized");
}
}
this.emitAdd(key, value);
}
onRemove(key, value) {
const wasIncluded = !this._filter || this._included.get(key);
this._included.delete(key);
if (wasIncluded) {
this.emitRemove(key, value);
onRemove(key: K, value: V): void {
const wasIncluded = !this._filter || this._included?.get(key);
if (this._included) {
this._included.delete(key);
if (wasIncluded) {
this.emitRemove(key, value);
}
} else {
throw new Error("Internal logic error: FilteredMap._included used before initialized");
}
}
onUpdate(key, value, params) {
onUpdate(key: K, value: V, params: any): void {
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
if (!this._included) {
return;
@ -96,7 +113,7 @@ export class FilteredMap extends BaseObservableMap {
}
}
_emitForUpdate(wasIncluded, isIncluded, key, value, params = null) {
_emitForUpdate(wasIncluded: boolean | undefined, isIncluded: boolean, key: K, value: V, params: any = null): void {
if (wasIncluded && !isIncluded) {
this.emitRemove(key, value);
} else if (!wasIncluded && isIncluded) {
@ -106,30 +123,32 @@ export class FilteredMap extends BaseObservableMap {
}
}
onSubscribeFirst() {
onSubscribeFirst(): void {
this._subscription = this._source.subscribe(this);
this._reapplyFilter(true);
super.onSubscribeFirst();
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
this._included = null;
this._subscription = this._subscription();
this._included = undefined;
if (this._subscription) {
this._subscription = this._subscription();
}
}
onReset() {
onReset(): void {
this._reapplyFilter();
this.emitReset();
}
[Symbol.iterator]() {
return new FilterIterator(this._source, this._included);
[Symbol.iterator](): FilterIterator<K, V> {
return new FilterIterator<K, V>(this._source, this._included);
}
get size() {
get size(): number {
let count = 0;
this._included.forEach(included => {
this._included?.forEach(included => {
if (included) {
count += 1;
}
@ -137,7 +156,7 @@ export class FilteredMap extends BaseObservableMap {
return count;
}
get(key) {
get(key: K): V | undefined {
const value = this._source.get(key);
if (value && this._filter(value, key)) {
return value;
@ -145,13 +164,15 @@ export class FilteredMap extends BaseObservableMap {
}
}
class FilterIterator {
constructor(map, _included) {
this._included = _included;
class FilterIterator<K, V> {
private _included?: Map<K, boolean>
private _sourceIterator: Iterator<[K, V], any, undefined>
constructor(map: BaseObservableMap<K, V>, included?: Map<K, boolean>) {
this._included = included;
this._sourceIterator = map[Symbol.iterator]();
}
next() {
next(): IteratorResult<[K, V]> {
// eslint-disable-next-line no-constant-condition
while (true) {
const sourceResult = this._sourceIterator.next();
@ -159,47 +180,62 @@ class FilterIterator {
return sourceResult;
}
const key = sourceResult.value[0];
if (this._included.get(key)) {
if (this._included?.get(key)) {
return sourceResult;
}
}
}
}
import {ObservableMap} from "./ObservableMap";
import {ObservableMap} from "..";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
return {
"filter preloaded list": assert => {
"filter preloaded list": (assert): void => {
const source = new ObservableMap();
source.add("one", 1);
source.add("two", 2);
source.add("three", 3);
const oddNumbers = new FilteredMap(source, x => x % 2 !== 0);
const oddNumbers = new FilteredMap(source, (x: number) => x % 2 !== 0);
// can only iterate after subscribing
oddNumbers.subscribe({});
oddNumbers.subscribe({
onAdd() {
return;
},
onRemove() {
return;
},
onUpdate() {
return;
},
onReset() {
return;
},
});
assert.equal(oddNumbers.size, 2);
const it = oddNumbers[Symbol.iterator]();
assert.deepEqual(it.next().value, ["one", 1]);
assert.deepEqual(it.next().value, ["three", 3]);
assert.equal(it.next().done, true);
},
// "filter added values": assert => {
// "filter added values": (assert): void => {
// },
// "filter removed values": assert => {
// "filter removed values": (assert): void => {
// },
// "filter changed values": assert => {
// "filter changed values": (assert): void => {
// },
"emits must trigger once": assert => {
"emits must trigger once": (assert): void => {
const source = new ObservableMap();
let count_add = 0, count_update = 0, count_remove = 0;
source.add("num1", 1);
source.add("num2", 2);
source.add("num3", 3);
const oddMap = new FilteredMap(source, x => x % 2 !== 0);
const oddMap = new FilteredMap(source, (x: number) => x % 2 !== 0);
oddMap.subscribe({
onAdd() {
count_add += 1;
@ -209,6 +245,9 @@ export function tests() {
},
onUpdate() {
count_update += 1;
},
onReset() {
return;
}
});
source.set("num3", 4);
@ -218,5 +257,5 @@ export function tests() {
assert.strictEqual(count_update, 1);
assert.strictEqual(count_remove, 1);
}
}
};
}

View File

@ -14,16 +14,25 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap";
import {BaseObservableMap} from ".";
import {SubscriptionHandle} from "../BaseObservable";
export class JoinedMap extends BaseObservableMap {
constructor(sources) {
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class JoinedMap<K, V> extends BaseObservableMap<K, V> {
protected _sources: BaseObservableMap<K, V>[];
private _subscriptions?: SourceSubscriptionHandler<K, V>[];
constructor(sources: BaseObservableMap<K, V>[]) {
super();
this._sources = sources;
this._subscriptions = null;
}
onAdd(source, key, value) {
onAdd(source: BaseObservableMap<K, V>, key: K, value: V): void {
if (!this._isKeyAtSourceOccluded(source, key)) {
const occludingValue = this._getValueFromOccludedSources(source, key);
if (occludingValue !== undefined) {
@ -35,7 +44,7 @@ export class JoinedMap extends BaseObservableMap {
}
}
onRemove(source, key, value) {
onRemove(source: BaseObservableMap<K, V>, key: K, value: V): void {
if (!this._isKeyAtSourceOccluded(source, key)) {
this.emitRemove(key, value);
const occludedValue = this._getValueFromOccludedSources(source, key);
@ -47,7 +56,7 @@ export class JoinedMap extends BaseObservableMap {
}
}
onUpdate(source, key, value, params) {
onUpdate(source: BaseObservableMap<K, V>, key: K, value: V, params: any): void {
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
if (!this._subscriptions) {
return;
@ -57,16 +66,16 @@ export class JoinedMap extends BaseObservableMap {
}
}
onReset() {
onReset(): void {
this.emitReset();
}
onSubscribeFirst() {
onSubscribeFirst(): void {
this._subscriptions = this._sources.map(source => new SourceSubscriptionHandler(source, this).subscribe());
super.onSubscribeFirst();
}
_isKeyAtSourceOccluded(source, key) {
_isKeyAtSourceOccluded(source: BaseObservableMap<K, V>, key: K): boolean {
// sources that come first in the sources array can
// hide the keys in later sources, to prevent events
// being emitted for the same key and different values,
@ -81,7 +90,7 @@ export class JoinedMap extends BaseObservableMap {
}
// get the value that the given source and key occlude, if any
_getValueFromOccludedSources(source, key) {
_getValueFromOccludedSources(source: BaseObservableMap<K, V>, key: K): V | undefined{
// sources that come first in the sources array can
// hide the keys in later sources, to prevent events
// being emitted for the same key and different values,
@ -97,53 +106,58 @@ export class JoinedMap extends BaseObservableMap {
return undefined;
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
for (const s of this._subscriptions) {
s.dispose();
if (this._subscriptions) {
for (const s of this._subscriptions) {
s.dispose();
}
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
[Symbol.iterator]() {
return new JoinedIterator(this._sources);
return new JoinedIterator<K, V>(this._sources);
}
get size() {
get size(): number {
return this._sources.reduce((sum, s) => sum + s.size, 0);
}
get(key) {
get(key: K): V | undefined {
for (const s of this._sources) {
const value = s.get(key);
if (value) {
return value;
}
}
return null;
return undefined;
}
}
class JoinedIterator {
constructor(sources) {
class JoinedIterator<K, V> implements Iterator<[K, V]> {
private _sources: {[Symbol.iterator](): Iterator<[K, V]>}[];
private _sourceIndex = -1;
private _encounteredKeys = new Set();
private _currentIterator?: Iterator<[K, V]>
constructor(sources: {[Symbol.iterator](): Iterator<[K, V]>}[]) {
this._sources = sources;
this._sourceIndex = -1;
this._currentIterator = null;
this._encounteredKeys = new Set();
}
next() {
let result;
next(): IteratorYieldResult<[K, V]> | IteratorReturnResult<null> {
let result: IteratorYieldResult<[K, V]> | undefined = undefined;
while (!result) {
if (!this._currentIterator) {
this._sourceIndex += 1;
if (this._sources.length <= this._sourceIndex) {
return {done: true};
return {done: true, value: null};
}
this._currentIterator = this._sources[this._sourceIndex][Symbol.iterator]();
}
const sourceResult = this._currentIterator.next();
if (sourceResult.done) {
this._currentIterator = null;
const sourceResult = this._currentIterator?.next();
if (!sourceResult || sourceResult.done) {
this._currentIterator = undefined;
continue;
} else {
const key = sourceResult.value[0];
@ -157,66 +171,73 @@ class JoinedIterator {
}
}
class SourceSubscriptionHandler {
constructor(source, joinedMap) {
class SourceSubscriptionHandler<K, V> {
private _source: BaseObservableMap<K, V>;
private _joinedMap: JoinedMap<K, V>;
private _subscription?: SubscriptionHandle;
constructor(source: BaseObservableMap<K, V>, joinedMap: JoinedMap<K, V>) {
this._source = source;
this._joinedMap = joinedMap;
this._subscription = null;
this._subscription = undefined;
}
subscribe() {
subscribe(): this {
this._subscription = this._source.subscribe(this);
return this;
}
dispose() {
this._subscription = this._subscription();
dispose(): void {
if (this._subscription) this._subscription = this._subscription();
}
onAdd(key, value) {
onAdd(key: K, value: V): void {
this._joinedMap.onAdd(this._source, key, value);
}
onRemove(key, value) {
onRemove(key: K, value: V): void {
this._joinedMap.onRemove(this._source, key, value);
}
onUpdate(key, value, params) {
onUpdate(key: K, value: V, params: any): void {
this._joinedMap.onUpdate(this._source, key, value, params);
}
onReset() {
this._joinedMap.onReset(this._source);
onReset(): void {
this._joinedMap.onReset();
}
}
import { ObservableMap } from "./ObservableMap";
import {ObservableMap} from "..";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
function observeMap(map) {
const events = [];
function observeMap(map: JoinedMap<any, any>): { type: string; key: any; value: any; params?: any; }[] {
const events: { type: string, key: any, value: any, params?: any }[] = [];
map.subscribe({
onAdd(key, value) { events.push({type: "add", key, value}); },
onRemove(key, value) { events.push({type: "remove", key, value}); },
onUpdate(key, value, params) { events.push({type: "update", key, value, params}); }
onAdd(key, value) { events.push({ type: "add", key, value }); },
onRemove(key, value) { events.push({ type: "remove", key, value }); },
onUpdate(key, value, params) { events.push({ type: "update", key, value, params }); },
onReset: function (): void {
return;
}
});
return events;
}
return {
"joined iterator": assert => {
const firstKV = ["a", 1];
const secondKV = ["b", 2];
const thirdKV = ["c", 3];
const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]);
"joined iterator": (assert): void => {
const firstKV: [string, number] = ["a", 1];
const secondKV: [string, number] = ["b", 2];
const thirdKV: [string, number] = ["c", 3];
const it = new JoinedIterator<string, number>([[firstKV, secondKV], [thirdKV]]);
assert.equal(it.next().value, firstKV);
assert.equal(it.next().value, secondKV);
assert.equal(it.next().value, thirdKV);
assert.equal(it.next().done, true);
},
"prevent key collision during iteration": assert => {
"prevent key collision during iteration": (assert): void => {
const first = new ObservableMap();
const second = new ObservableMap();
const join = new JoinedMap([first, second]);
@ -228,7 +249,7 @@ export function tests() {
assert.deepEqual(it.next().value, ["b", 3]);
assert.equal(it.next().done, true);
},
"adding occluded key doesn't emit add": assert => {
"adding occluded key doesn't emit add": (assert): void => {
const first = new ObservableMap();
const second = new ObservableMap();
const join = new JoinedMap([first, second]);
@ -240,7 +261,7 @@ export function tests() {
assert.equal(events[0].key, "a");
assert.equal(events[0].value, 1);
},
"updating occluded key doesn't emit update": assert => {
"updating occluded key doesn't emit update": (assert): void => {
const first = new ObservableMap();
const second = new ObservableMap();
const join = new JoinedMap([first, second]);
@ -250,7 +271,7 @@ export function tests() {
second.update("a", 3);
assert.equal(events.length, 0);
},
"removal of occluding key emits add after remove": assert => {
"removal of occluding key emits add after remove": (assert): void => {
const first = new ObservableMap();
const second = new ObservableMap();
const join = new JoinedMap([first, second]);
@ -266,7 +287,7 @@ export function tests() {
assert.equal(events[1].key, "a");
assert.equal(events[1].value, 2);
},
"adding occluding key emits remove first": assert => {
"adding occluding key emits remove first": (assert): void => {
const first = new ObservableMap();
const second = new ObservableMap();
const join = new JoinedMap([first, second]);

View File

@ -1,70 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableMap} from "./BaseObservableMap";
export class LogMap extends BaseObservableMap {
constructor(source, log) {
super();
this._source = source;
this.log = log;
this._subscription = null;
}
onAdd(key, value) {
this.log("add", key, value);
this.emitAdd(key, value);
}
onRemove(key, value) {
this.log("remove", key, value);
this.emitRemove(key, value);
}
onUpdate(key, value, params) {
this.log("update", key, value, params);
this.emitUpdate(key, value, params);
}
onSubscribeFirst() {
this.log("subscribeFirst");
this._subscription = this._source.subscribe(this);
super.onSubscribeFirst();
}
onUnsubscribeLast() {
super.onUnsubscribeLast();
this._subscription = this._subscription();
this.log("unsubscribeLast");
}
onReset() {
this.log("reset");
this.emitReset();
}
[Symbol.iterator]() {
return this._source[Symbol.iterator]();
}
get size() {
return this._source.size;
}
get(key) {
return this._source.get(key);
}
}

View File

@ -0,0 +1,86 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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 {BaseObservableMap} from "./index";
import {SubscriptionHandle} from "../BaseObservable";
import {ILogItem, LabelOrValues} from "../../logging/types";
import {LogLevel} from "../../logging/LogFilter";
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class LogMap<K, V> extends BaseObservableMap<K, V> {
private _source: BaseObservableMap<K, V>;
private _subscription?: SubscriptionHandle;
private _log: ILogItem;
constructor(source: BaseObservableMap<K, V>, log: ILogItem) {
super();
this._source = source;
this._log = log;
}
private log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem {
return this._log.log(labelOrValues, logLevel);
}
onAdd(key: K, value: V): void {
this.log("add " + JSON.stringify({key, value}));
this.emitAdd(key, value);
}
onRemove(key: K, value: V): void {
this.log("remove " + JSON.stringify({key, value}));
this.emitRemove(key, value);
}
onUpdate(key: K, value: V, params: any): void {
this.log("update" + JSON.stringify({key, value, params}));
this.emitUpdate(key, value, params);
}
onSubscribeFirst(): void {
this.log("subscribeFirst");
this._subscription = this._source.subscribe(this);
super.onSubscribeFirst();
}
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
if (this._subscription) this._subscription = this._subscription();
this.log("unsubscribeLast");
}
onReset(): void {
this.log("reset");
this.emitReset();
}
[Symbol.iterator](): Iterator<[K, V]> {
return this._source[Symbol.iterator]();
}
get size(): number {
return this._source.size;
}
get(key: K): V | undefined{
return this._source.get(key);
}
}

View File

@ -14,55 +14,76 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap";
import {BaseObservableMap, Mapper, Updater} from "./index";
import {SubscriptionHandle} from "../BaseObservable";
/*
so a mapped value can emit updates on it's own with this._emitSpontaneousUpdate that is passed in the mapping function
how should the mapped value be notified of an update though? and can it then decide to not propagate the update?
*/
export class MappedMap extends BaseObservableMap {
constructor(source, mapper, updater) {
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class MappedMap<K, V, MappedV> extends BaseObservableMap<K, MappedV> {
private _source: BaseObservableMap<K, V>;
private _mapper: Mapper<V, MappedV>;
private _updater?: Updater<V, MappedV>;
private _mappedValues: Map<K, MappedV>;
private _subscription?: SubscriptionHandle;
constructor(
source: BaseObservableMap<K, V>,
mapper: Mapper<V, MappedV>,
updater?: Updater<V, MappedV>
) {
super();
this._source = source;
this._mapper = mapper;
this._updater = updater;
this._mappedValues = new Map();
this._mappedValues = new Map<K, MappedV>();
}
_emitSpontaneousUpdate(key, params) {
_emitSpontaneousUpdate(key: K, params: any): void {
const value = this._mappedValues.get(key);
if (value) {
this.emitUpdate(key, value, params);
}
}
onAdd(key, value) {
onAdd(key: K, value: V): void {
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
const mappedValue = this._mapper(value, emitSpontaneousUpdate);
this._mappedValues.set(key, mappedValue);
this.emitAdd(key, mappedValue);
}
onRemove(key/*, _value*/) {
onRemove(key: K/*, _value*/): void {
const mappedValue = this._mappedValues.get(key);
if (this._mappedValues.delete(key)) {
this.emitRemove(key, mappedValue);
if (mappedValue) {
this.emitRemove(key, mappedValue);
}
}
}
onUpdate(key, value, params) {
onUpdate(key: K, value: V, params: any): void {
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
if (!this._mappedValues) {
return;
}
const mappedValue = this._mappedValues.get(key);
if (mappedValue !== undefined) {
this._updater?.(mappedValue, params, value);
this._updater?.(params, mappedValue, value);
// TODO: map params somehow if needed?
this.emitUpdate(key, mappedValue, params);
}
}
onSubscribeFirst() {
onSubscribeFirst(): void {
this._subscription = this._source.subscribe(this);
for (let [key, value] of this._source) {
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
@ -72,26 +93,28 @@ export class MappedMap extends BaseObservableMap {
super.onSubscribeFirst();
}
onUnsubscribeLast() {
onUnsubscribeLast(): void {
super.onUnsubscribeLast();
this._subscription = this._subscription();
if (this._subscription) {
this._subscription = this._subscription();
}
this._mappedValues.clear();
}
onReset() {
onReset(): void {
this._mappedValues.clear();
this.emitReset();
}
[Symbol.iterator]() {
[Symbol.iterator](): IterableIterator<[K, MappedV]> {
return this._mappedValues.entries();
}
get size() {
get size(): number {
return this._mappedValues.size;
}
get(key) {
get(key: K): MappedV | undefined {
return this._mappedValues.get(key);
}
}
}

View File

@ -14,8 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {BaseObservableMap} from "./BaseObservableMap";
import {BaseObservableMap} from "./index";
/*
This class MUST never be imported directly from here.
Instead, it MUST be imported from index.ts. See the
top level comment in index.ts for details.
*/
export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
private readonly _values: Map<K, V>;
@ -61,7 +67,7 @@ export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
// We set the value here because update only supports inline updates
this._values.set(key, value);
return this.update(key, undefined);
}
}
else {
return this.add(key, value);
}
@ -93,9 +99,10 @@ export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
}
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function tests() {
return {
test_initial_values(assert) {
test_initial_values(assert): void {
const map = new ObservableMap([
["a", 5],
["b", 10]
@ -105,14 +112,14 @@ export function tests() {
assert.equal(map.get("b"), 10);
},
test_add(assert) {
test_add(assert): void {
let fired = 0;
const map = new ObservableMap<number, {value: number}>();
map.subscribe({
onAdd(key, value) {
fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 5});
assert.deepEqual(value, {value: 5});
},
onUpdate() {},
onRemove() {},
@ -123,7 +130,7 @@ export function tests() {
assert.equal(fired, 1);
},
test_update(assert) {
test_update(assert): void {
let fired = 0;
const map = new ObservableMap<number, {number: number}>();
const value = {number: 5};
@ -132,7 +139,7 @@ export function tests() {
onUpdate(key, value, params) {
fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {number: 6});
assert.deepEqual(value, {number: 6});
assert.equal(params, "test");
},
onAdd() {},
@ -144,7 +151,7 @@ export function tests() {
assert.equal(fired, 1);
},
test_update_unknown(assert) {
test_update_unknown(assert): void {
let fired = 0;
const map = new ObservableMap<number, {number: number}>();
map.subscribe({
@ -158,19 +165,19 @@ export function tests() {
assert.equal(result, false);
},
test_set(assert) {
test_set(assert): void {
let add_fired = 0, update_fired = 0;
const map = new ObservableMap<number, {value: number}>();
map.subscribe({
onAdd(key, value) {
add_fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 5});
assert.deepEqual(value, {value: 5});
},
onUpdate(key, value/*, params*/) {
update_fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 7});
assert.deepEqual(value, {value: 7});
},
onRemove() {},
onReset() {}
@ -185,7 +192,7 @@ export function tests() {
assert.equal(update_fired, 1);
},
test_remove(assert) {
test_remove(assert): void {
let fired = 0;
const map = new ObservableMap<number, {value: number}>();
const value = {value: 5};
@ -194,7 +201,7 @@ export function tests() {
onRemove(key, value) {
fired += 1;
assert.equal(key, 1);
assert.deepEqual(value, {value: 5});
assert.deepEqual(value, {value: 5});
},
onAdd() {},
onUpdate() {},
@ -205,7 +212,7 @@ export function tests() {
assert.equal(fired, 1);
},
test_iterate(assert) {
test_iterate(assert): void {
const results: any[] = [];
const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5});
@ -219,11 +226,11 @@ export function tests() {
assert.equal(results.find(([key]) => key === 2)[1].number, 6);
assert.equal(results.find(([key]) => key === 3)[1].number, 7);
},
test_size(assert) {
test_size(assert): void {
const map = new ObservableMap<number, {number: number}>();
map.add(1, {number: 5});
map.add(2, {number: 6});
assert.equal(map.size, 2);
},
}
};
}

View File

@ -0,0 +1,17 @@
// In order to avoid a circular dependency problem at runtime between BaseObservableMap
// and the classes that extend it, it's important that:
//
// 1) It always remain the first module exported below.
// 2) Anything that imports any of the classes in this module
// ONLY import them from this index.ts file.
//
// See https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
// for more on why this discipline is necessary.
export {BaseObservableMap} from './BaseObservableMap';
export type {Mapper, Updater, Comparator, Filter} from './BaseObservableMap';
export {ApplyMap} from './ApplyMap';
export {FilteredMap} from './FilteredMap';
export {JoinedMap} from './JoinedMap';
export {LogMap} from './LogMap';
export {MappedMap} from './MappedMap';
export {ObservableMap} from './ObservableMap';