diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index cf1fc3bf..ae7233ed 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -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"] } }; diff --git a/src/domain/SessionPickerViewModel.js b/src/domain/SessionPickerViewModel.js index e486c64f..f4a16f1c 100644 --- a/src/domain/SessionPickerViewModel.js +++ b/src/domain/SessionPickerViewModel.js @@ -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"; diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index ce9aa0ee..652aa57c 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -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) { diff --git a/src/domain/session/rightpanel/MemberListViewModel.js b/src/domain/session/rightpanel/MemberListViewModel.js index b75a3d1c..1c878c85 100644 --- a/src/domain/session/rightpanel/MemberListViewModel.js +++ b/src/domain/session/rightpanel/MemberListViewModel.js @@ -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); }; diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 1977b6f4..c5ea1224 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -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) { diff --git a/src/lib.ts b/src/lib.ts index 4d1f906f..df96bfcd 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -78,8 +78,7 @@ export { MappedList, AsyncMappedList, ConcatList, - ObservableMap -} from "./observable/index"; +} from "./observable"; export { BaseObservableValue, ObservableValue, diff --git a/src/matrix/Session.js b/src/matrix/Session.js index ae1dea61..048ddbc8 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -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) { diff --git a/src/matrix/room/members/MemberList.js b/src/matrix/room/members/MemberList.js index f32a63d3..74b3fe7d 100644 --- a/src/matrix/room/members/MemberList.js +++ b/src/matrix/room/members/MemberList.js @@ -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 { diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3332a5b0..a721092e 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -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); diff --git a/src/observable/BaseObservable.ts b/src/observable/BaseObservable.ts index 44d716ac..edbdd8bc 100644 --- a/src/observable/BaseObservable.ts +++ b/src/observable/BaseObservable.ts @@ -34,7 +34,7 @@ export abstract class BaseObservable { if (this._handlers.size === 1) { this.onSubscribeFirst(); } - return () => { + return (): undefined => { return this.unsubscribe(handler); }; } @@ -63,22 +63,23 @@ export abstract class BaseObservable { // 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); } - } + }; } diff --git a/src/observable/ObservableValue.ts b/src/observable/ObservableValue.ts index ad0a226d..96791f91 100644 --- a/src/observable/ObservableValue.ts +++ b/src/observable/ObservableValue.ts @@ -20,7 +20,7 @@ import type {SubscriptionHandle} from "./BaseObservable"; // like an EventEmitter, but doesn't have an event type export abstract class BaseObservableValue 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 implements IWaitHandle { return this._promise; } - dispose() { + dispose(): void { if (this._subscription) { this._subscription(); this._subscription = null; @@ -82,7 +82,7 @@ class WaitForHandle implements IWaitHandle { class ResolvedWaitForHandle implements IWaitHandle { constructor(public promise: Promise) {} - dispose() {} + dispose(): void {} } export class ObservableValue extends BaseObservableValue { @@ -113,7 +113,7 @@ export class RetainedObservableValue extends ObservableValue { this._freeCallback = freeCallback; } - onUnsubscribeLast() { + onUnsubscribeLast(): void { super.onUnsubscribeLast(); this._freeCallback(); } @@ -130,7 +130,7 @@ export class FlatMapObservableValue extends BaseObservableValue extends BaseObservableValue { this.updateTargetSubscription(); @@ -147,7 +147,7 @@ export class FlatMapObservableValue extends BaseObservableValue extends BaseObservableValue { + "set emits an update": (assert): void => { const a = new ObservableValue(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 => { 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 => { const a = new ObservableValue(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); 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); 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); 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]); } - } + }; } diff --git a/src/observable/index.js b/src/observable/index.js deleted file mode 100644 index 6057174b..00000000 --- a/src/observable/index.js +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -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)); - } -}); diff --git a/src/observable/index.ts b/src/observable/index.ts new file mode 100644 index 00000000..25af50b8 --- /dev/null +++ b/src/observable/index.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020 Bruno Windels + +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"; \ No newline at end of file diff --git a/src/observable/list/AsyncMappedList.ts b/src/observable/list/AsyncMappedList.ts index 0a919cdc..2c5ef63f 100644 --- a/src/observable/list/AsyncMappedList.ts +++ b/src/observable/list/AsyncMappedList.ts @@ -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 extends BaseMappedList> implements IListObserver { private _eventQueue: AsyncEvent[] | null = null; @@ -31,7 +31,7 @@ export class AsyncMappedList extends BaseMappedList> impleme this._eventQueue.push(new AddEvent(idx, item)); idx += 1; } - this._flush(); + void this._flush(); } async _flush(): Promise { @@ -52,35 +52,35 @@ export class AsyncMappedList extends BaseMappedList> 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 { 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 => { + const double = (n: number): number => n * n; const source = new ObservableArray(); 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); } } - } + }; } diff --git a/src/observable/list/BaseMappedList.ts b/src/observable/list/BaseMappedList.ts index 4e3d05e0..8646153e 100644 --- a/src/observable/list/BaseMappedList.ts +++ b/src/observable/list/BaseMappedList.ts @@ -37,15 +37,15 @@ export class BaseMappedList extends BaseObservableList { 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 { return this._mappedValues!.values(); } } diff --git a/src/observable/list/BaseObservableList.ts b/src/observable/list/BaseObservableList.ts index d103eb64..1fd82c25 100644 --- a/src/observable/list/BaseObservableList.ts +++ b/src/observable/list/BaseObservableList.ts @@ -26,17 +26,17 @@ export interface IListObserver { export function defaultObserverWith(overrides: { [key in keyof IListObserver]?: IListObserver[key] }): IListObserver { 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 extends BaseObservable> implements Iterable { - emitReset() { + emitReset(): void { for(let h of this._handlers) { h.onReset(this); } diff --git a/src/observable/list/ConcatList.ts b/src/observable/list/ConcatList.ts index 5822468a..80accb81 100644 --- a/src/observable/list/ConcatList.ts +++ b/src/observable/list/ConcatList.ts @@ -47,7 +47,7 @@ export class ConcatList extends BaseObservableList 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 extends BaseObservableList implements IListObserve return len; } - [Symbol.iterator]() { + [Symbol.iterator](): Iterator { let sourceListIdx = 0; let it = this._sourceLists[0][Symbol.iterator](); return { - next: () => { + next: (): IteratorResult => { let result = it.next(); while (result.done) { sourceListIdx += 1; @@ -102,22 +102,25 @@ export class ConcatList extends BaseObservableList 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); diff --git a/src/observable/list/MappedList.ts b/src/observable/list/MappedList.ts index ebb418d3..2ddae698 100644 --- a/src/observable/list/MappedList.ts +++ b/src/observable/list/MappedList.ts @@ -19,7 +19,7 @@ import {IListObserver} from "./BaseObservableList"; import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList"; export class MappedList extends BaseMappedList implements IListObserver { - 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 { - 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( 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); }, - + }; } diff --git a/src/observable/list/ObservableArray.ts b/src/observable/list/ObservableArray.ts index 0771d0f6..1b962e81 100644 --- a/src/observable/list/ObservableArray.ts +++ b/src/observable/list/ObservableArray.ts @@ -75,7 +75,7 @@ export class ObservableArray extends BaseObservableList { return this._items.length; } - [Symbol.iterator]() { + [Symbol.iterator](): IterableIterator { return this._items.values(); } } diff --git a/src/observable/list/SortedArray.ts b/src/observable/list/SortedArray.ts index c85cca27..e4723db1 100644 --- a/src/observable/list/SortedArray.ts +++ b/src/observable/list/SortedArray.ts @@ -87,7 +87,7 @@ export class SortedArray extends BaseObservableList { 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 extends BaseObservableList { return this._items.length; } - [Symbol.iterator]() { + [Symbol.iterator](): Iterator { return new Iterator(this); } } // iterator that works even if the current value is removed while iterating class Iterator { - private _sortedArray: SortedArray | null - private _current: T | null | undefined + private _sortedArray: SortedArray; + private _current: T | null | undefined; + private _consumed: boolean = false; constructor(sortedArray: SortedArray) { 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 { + 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; } } +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export function tests() { return { - "setManyUnsorted": assert => { + "setManyUnsorted": (assert): void => { const sa = new SortedArray((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((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); } - } + }; } diff --git a/src/observable/list/SortedMapList.js b/src/observable/list/SortedMapList.js index d74dbade..6f4be123 100644 --- a/src/observable/list/SortedMapList.js +++ b/src/observable/list/SortedMapList.js @@ -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]); }, - } + }; } diff --git a/src/observable/list/common.ts b/src/observable/list/common.ts index c67a841b..20f3a8bf 100644 --- a/src/observable/list/common.ts +++ b/src/observable/list/common.ts @@ -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(predicate: (value: T) => boolean, array: T[], observable: BaseObservableList, updater: (value: T) => any | false) { +export function findAndUpdateInArray( + predicate: (value: T) => boolean, + array: T[], + observable: BaseObservableList, + updater: (value: T) => any | false +): boolean { const index = array.findIndex(predicate); if (index !== -1) { const value = array[index]; diff --git a/src/observable/map/ApplyMap.js b/src/observable/map/ApplyMap.ts similarity index 59% rename from src/observable/map/ApplyMap.js rename to src/observable/map/ApplyMap.ts index 6be7278a..0c4962c8 100644 --- a/src/observable/map/ApplyMap.js +++ b/src/observable/map/ApplyMap.ts @@ -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 extends BaseObservableMap { + private _source: BaseObservableMap; + private _subscription?: SubscriptionHandle; + private _apply?: Apply; + + constructor(source: BaseObservableMap, apply?: Apply) { super(); this._source = source; this._apply = apply; - this._subscription = null; } - hasApply() { + hasApply(): boolean { return !!this._apply; } - setApply(apply) { + setApply(apply?: Apply): void { this._apply = apply; - if (apply) { + if (this._apply) { this.applyOnce(this._apply); } } - applyOnce(apply) { + applyOnce(apply: Apply): 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 = (key: K, value: V, params?: any) => void; \ No newline at end of file diff --git a/src/observable/map/BaseObservableMap.ts b/src/observable/map/BaseObservableMap.ts index 694c017e..9b501285 100644 --- a/src/observable/map/BaseObservableMap.ts +++ b/src/observable/map/BaseObservableMap.ts @@ -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 { onReset(): void; @@ -23,33 +28,70 @@ export interface IMapObserver { 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 extends BaseObservable> { - 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): JoinedMap { + return new JoinedMap([this].concat(otherMaps)); + } + + mapValues(mapper: Mapper, updater?: Updater): MappedMap { + return new MappedMap(this, mapper, updater); + } + + sortValues(comparator: Comparator): SortedMapList { + return new SortedMapList(this, comparator); + } + + filterValues(filter: Filter): FilteredMap { + return new FilteredMap(this, filter); + } + abstract [Symbol.iterator](): Iterator<[K, V]>; abstract get size(): number; abstract get(key: K): V | undefined; } + +export type Mapper = ( + value: V, + emitSpontaneousUpdate: any, +) => MappedV; + +export type Updater = (params: any, mappedValue?: MappedV, value?: V) => void; + +export type Comparator = (a: V, b: V) => number; + +export type Filter = (v: V, k: K) => boolean; \ No newline at end of file diff --git a/src/observable/map/FilteredMap.js b/src/observable/map/FilteredMap.ts similarity index 59% rename from src/observable/map/FilteredMap.js rename to src/observable/map/FilteredMap.ts index d7e11fbe..c97bc48a 100644 --- a/src/observable/map/FilteredMap.js +++ b/src/observable/map/FilteredMap.ts @@ -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 extends BaseObservableMap { + private _source: BaseObservableMap; + private _filter: Filter; + private _included?: Map; + private _subscription?: SubscriptionHandle; + + constructor(source: BaseObservableMap, filter: Filter) { super(); this._source = source; this._filter = filter; - /** @type {Map} */ - this._included = null; - this._subscription = null; } - setFilter(filter) { + setFilter(filter: Filter): 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 { + return new FilterIterator(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 { + private _included?: Map + private _sourceIterator: Iterator<[K, V], any, undefined> + constructor(map: BaseObservableMap, included?: Map) { + 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); } - } + }; } diff --git a/src/observable/map/JoinedMap.js b/src/observable/map/JoinedMap.ts similarity index 65% rename from src/observable/map/JoinedMap.js rename to src/observable/map/JoinedMap.ts index d97c5677..c125f4da 100644 --- a/src/observable/map/JoinedMap.js +++ b/src/observable/map/JoinedMap.ts @@ -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 extends BaseObservableMap { + protected _sources: BaseObservableMap[]; + private _subscriptions?: SourceSubscriptionHandler[]; + + constructor(sources: BaseObservableMap[]) { super(); this._sources = sources; - this._subscriptions = null; } - onAdd(source, key, value) { + onAdd(source: BaseObservableMap, 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, 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, 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, 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, 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(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 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 { + 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 { + private _source: BaseObservableMap; + private _joinedMap: JoinedMap; + private _subscription?: SubscriptionHandle; + + constructor(source: BaseObservableMap, joinedMap: JoinedMap) { 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): { 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([[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]); diff --git a/src/observable/map/LogMap.js b/src/observable/map/LogMap.js deleted file mode 100644 index 1beb4846..00000000 --- a/src/observable/map/LogMap.js +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -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); - } -} diff --git a/src/observable/map/LogMap.ts b/src/observable/map/LogMap.ts new file mode 100644 index 00000000..ce9d343e --- /dev/null +++ b/src/observable/map/LogMap.ts @@ -0,0 +1,86 @@ +/* +Copyright 2020 Bruno Windels + +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 extends BaseObservableMap { + private _source: BaseObservableMap; + private _subscription?: SubscriptionHandle; + private _log: ILogItem; + + constructor(source: BaseObservableMap, 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); + } +} diff --git a/src/observable/map/MappedMap.js b/src/observable/map/MappedMap.ts similarity index 64% rename from src/observable/map/MappedMap.js rename to src/observable/map/MappedMap.ts index a6b65c41..6ada079f 100644 --- a/src/observable/map/MappedMap.js +++ b/src/observable/map/MappedMap.ts @@ -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 extends BaseObservableMap { + private _source: BaseObservableMap; + private _mapper: Mapper; + private _updater?: Updater; + private _mappedValues: Map; + private _subscription?: SubscriptionHandle; + + + constructor( + source: BaseObservableMap, + mapper: Mapper, + updater?: Updater + ) { super(); this._source = source; this._mapper = mapper; this._updater = updater; - this._mappedValues = new Map(); + this._mappedValues = new Map(); } - _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); } -} +} \ No newline at end of file diff --git a/src/observable/map/ObservableMap.ts b/src/observable/map/ObservableMap.ts index d604ab0a..f0d4c77a 100644 --- a/src/observable/map/ObservableMap.ts +++ b/src/observable/map/ObservableMap.ts @@ -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 extends BaseObservableMap { private readonly _values: Map; @@ -61,7 +67,7 @@ export class ObservableMap extends BaseObservableMap { // 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 extends BaseObservableMap { } } +// 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(); 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(); 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(); 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(); 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(); 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(); 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(); map.add(1, {number: 5}); map.add(2, {number: 6}); assert.equal(map.size, 2); }, - } + }; } diff --git a/src/observable/map/index.ts b/src/observable/map/index.ts new file mode 100644 index 00000000..a78446c4 --- /dev/null +++ b/src/observable/map/index.ts @@ -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'; \ No newline at end of file