mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2024-12-23 03:25:12 +01:00
Merge pull request #793 from ibeckermayer/ibeckermayer/ts-conversion-observable-map
Typescript conversion for `src/observable/map`
This commit is contained in:
commit
8ef163353a
@ -20,6 +20,10 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-floating-promises": 2,
|
"@typescript-eslint/no-floating-promises": 2,
|
||||||
"@typescript-eslint/no-misused-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"]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {SortedArray} from "../observable/index.js";
|
import {SortedArray} from "../observable";
|
||||||
import {ViewModel} from "./ViewModel";
|
import {ViewModel} from "./ViewModel";
|
||||||
import {avatarInitials, getIdentifierColorNumber} from "./avatar";
|
import {avatarInitials, getIdentifierColorNumber} from "./avatar";
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ import {RoomTileViewModel} from "./RoomTileViewModel.js";
|
|||||||
import {InviteTileViewModel} from "./InviteTileViewModel.js";
|
import {InviteTileViewModel} from "./InviteTileViewModel.js";
|
||||||
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
|
import {RoomBeingCreatedTileViewModel} from "./RoomBeingCreatedTileViewModel.js";
|
||||||
import {RoomFilter} from "./RoomFilter.js";
|
import {RoomFilter} from "./RoomFilter.js";
|
||||||
import {ApplyMap} from "../../../observable/map/ApplyMap.js";
|
import {ApplyMap} from "../../../observable";
|
||||||
import {addPanelIfNeeded} from "../../navigation/index";
|
import {addPanelIfNeeded} from "../../navigation";
|
||||||
|
|
||||||
export class LeftPanelViewModel extends ViewModel {
|
export class LeftPanelViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
@ -46,8 +46,8 @@ export class MemberListViewModel extends ViewModel {
|
|||||||
const vm = new MemberTileViewModel(this.childOptions({member, emitChange, mediaRepository}));
|
const vm = new MemberTileViewModel(this.childOptions({member, emitChange, mediaRepository}));
|
||||||
this.nameDisambiguator.disambiguate(vm);
|
this.nameDisambiguator.disambiguate(vm);
|
||||||
return vm;
|
return vm;
|
||||||
}
|
};
|
||||||
const updater = (vm, params, newMember) => {
|
const updater = (params, vm, newMember) => {
|
||||||
vm.updateFrom(newMember);
|
vm.updateFrom(newMember);
|
||||||
this.nameDisambiguator.disambiguate(vm);
|
this.nameDisambiguator.disambiguate(vm);
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {ObservableMap} from "../../../../observable/map/ObservableMap";
|
import {ObservableMap} from "../../../../observable";
|
||||||
|
|
||||||
export class ReactionsViewModel {
|
export class ReactionsViewModel {
|
||||||
constructor(parentTile) {
|
constructor(parentTile) {
|
||||||
|
@ -78,8 +78,7 @@ export {
|
|||||||
MappedList,
|
MappedList,
|
||||||
AsyncMappedList,
|
AsyncMappedList,
|
||||||
ConcatList,
|
ConcatList,
|
||||||
ObservableMap
|
} from "./observable";
|
||||||
} from "./observable/index";
|
|
||||||
export {
|
export {
|
||||||
BaseObservableValue,
|
BaseObservableValue,
|
||||||
ObservableValue,
|
ObservableValue,
|
||||||
|
@ -21,7 +21,7 @@ import {RoomStatus} from "./room/common";
|
|||||||
import {RoomBeingCreated} from "./room/RoomBeingCreated";
|
import {RoomBeingCreated} from "./room/RoomBeingCreated";
|
||||||
import {Invite} from "./room/Invite.js";
|
import {Invite} from "./room/Invite.js";
|
||||||
import {Pusher} from "./push/Pusher";
|
import {Pusher} from "./push/Pusher";
|
||||||
import { ObservableMap } from "../observable/index.js";
|
import {ObservableMap} from "../observable";
|
||||||
import {User} from "./User.js";
|
import {User} from "./User.js";
|
||||||
import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
|
import {DeviceMessageHandler} from "./DeviceMessageHandler.js";
|
||||||
import {Account as E2EEAccount} from "./e2ee/Account.js";
|
import {Account as E2EEAccount} from "./e2ee/Account.js";
|
||||||
@ -192,7 +192,7 @@ export class Session {
|
|||||||
/**
|
/**
|
||||||
* Enable secret storage by providing the secret storage credential.
|
* Enable secret storage by providing the secret storage credential.
|
||||||
* This will also see if there is a megolm key backup and try to enable that if so.
|
* This will also see if there is a megolm key backup and try to enable that if so.
|
||||||
*
|
*
|
||||||
* @param {string} type either "passphrase" or "recoverykey"
|
* @param {string} type either "passphrase" or "recoverykey"
|
||||||
* @param {string} credential either the passphrase or the recovery key, depending on the type
|
* @param {string} credential either the passphrase or the recovery key, depending on the type
|
||||||
* @return {Promise} resolves or rejects after having tried to enable secret storage
|
* @return {Promise} resolves or rejects after having tried to enable secret storage
|
||||||
@ -663,7 +663,7 @@ export class Session {
|
|||||||
if (this._e2eeAccount && deviceOneTimeKeysCount) {
|
if (this._e2eeAccount && deviceOneTimeKeysCount) {
|
||||||
changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log);
|
changes.e2eeAccountChanges = this._e2eeAccount.writeSync(deviceOneTimeKeysCount, txn, log);
|
||||||
}
|
}
|
||||||
|
|
||||||
const deviceLists = syncResponse.device_lists;
|
const deviceLists = syncResponse.device_lists;
|
||||||
if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) {
|
if (this._deviceTracker && Array.isArray(deviceLists?.changed) && deviceLists.changed.length) {
|
||||||
await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log));
|
await log.wrap("deviceLists", log => this._deviceTracker.writeDeviceChanges(deviceLists.changed, txn, log));
|
||||||
@ -908,7 +908,7 @@ export class Session {
|
|||||||
Creates an empty (summary isn't loaded) the archived room if it isn't
|
Creates an empty (summary isn't loaded) the archived room if it isn't
|
||||||
loaded already, assuming sync will either remove it (when rejoining) or
|
loaded already, assuming sync will either remove it (when rejoining) or
|
||||||
write a full summary adopting it from the joined room when leaving
|
write a full summary adopting it from the joined room when leaving
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
*/
|
*/
|
||||||
createOrGetArchivedRoomForSync(roomId) {
|
createOrGetArchivedRoomForSync(roomId) {
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ObservableMap} from "../../../observable/map/ObservableMap";
|
import {ObservableMap} from "../../../observable";
|
||||||
import {RetainedValue} from "../../../utils/RetainedValue";
|
import {RetainedValue} from "../../../utils/RetainedValue";
|
||||||
|
|
||||||
export class MemberList extends RetainedValue {
|
export class MemberList extends RetainedValue {
|
||||||
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
|
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable";
|
||||||
import {Disposables} from "../../../utils/Disposables";
|
import {Disposables} from "../../../utils/Disposables";
|
||||||
import {Direction} from "./Direction";
|
import {Direction} from "./Direction";
|
||||||
import {TimelineReader} from "./persistence/TimelineReader.js";
|
import {TimelineReader} from "./persistence/TimelineReader.js";
|
||||||
@ -45,7 +45,7 @@ export class Timeline {
|
|||||||
});
|
});
|
||||||
this._readerRequest = null;
|
this._readerRequest = null;
|
||||||
this._allEntries = null;
|
this._allEntries = null;
|
||||||
/** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */
|
/** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */
|
||||||
this._contextEntriesNotInTimeline = new Map();
|
this._contextEntriesNotInTimeline = new Map();
|
||||||
/** Only used to decrypt non-persisted context entries fetched from the homeserver */
|
/** Only used to decrypt non-persisted context entries fetched from the homeserver */
|
||||||
this._decryptEntries = null;
|
this._decryptEntries = null;
|
||||||
@ -189,7 +189,7 @@ export class Timeline {
|
|||||||
// before it has any subscriptions, we bail out if this isn't
|
// before it has any subscriptions, we bail out if this isn't
|
||||||
// the case yet. This can happen when sync adds or replaces entries
|
// the case yet. This can happen when sync adds or replaces entries
|
||||||
// before load has finished and the view has subscribed to the timeline.
|
// before load has finished and the view has subscribed to the timeline.
|
||||||
//
|
//
|
||||||
// Once the subscription is setup, MappedList will set up the local
|
// Once the subscription is setup, MappedList will set up the local
|
||||||
// relations as needed with _applyAndEmitLocalRelationChange,
|
// relations as needed with _applyAndEmitLocalRelationChange,
|
||||||
// so we're not missing anything by bailing out.
|
// so we're not missing anything by bailing out.
|
||||||
@ -239,7 +239,7 @@ export class Timeline {
|
|||||||
if (err.name === "CompareError") {
|
if (err.name === "CompareError") {
|
||||||
// see FragmentIdComparer, if the replacing entry is on a fragment
|
// see FragmentIdComparer, if the replacing entry is on a fragment
|
||||||
// that is currently not loaded into the FragmentIdComparer, it will
|
// that is currently not loaded into the FragmentIdComparer, it will
|
||||||
// throw a CompareError, and it means that the event is not loaded
|
// throw a CompareError, and it means that the event is not loaded
|
||||||
// in the timeline (like when receiving a relation for an event
|
// in the timeline (like when receiving a relation for an event
|
||||||
// that is not loaded in memory) so we can just drop this error as
|
// that is not loaded in memory) so we can just drop this error as
|
||||||
// replacing an event that is not already loaded is a no-op.
|
// replacing an event that is not already loaded is a no-op.
|
||||||
@ -311,7 +311,7 @@ export class Timeline {
|
|||||||
* - timeline
|
* - timeline
|
||||||
* - storage
|
* - storage
|
||||||
* - homeserver
|
* - homeserver
|
||||||
* @param {EventEntry[]} entries
|
* @param {EventEntry[]} entries
|
||||||
*/
|
*/
|
||||||
async _loadContextEntriesWhereNeeded(entries) {
|
async _loadContextEntriesWhereNeeded(entries) {
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
@ -392,7 +392,7 @@ export class Timeline {
|
|||||||
* [loadAtTop description]
|
* [loadAtTop description]
|
||||||
* @param {[type]} amount [description]
|
* @param {[type]} amount [description]
|
||||||
* @return {boolean} true if the top of the timeline has been reached
|
* @return {boolean} true if the top of the timeline has been reached
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async loadAtTop(amount) {
|
async loadAtTop(amount) {
|
||||||
if (this._disposables.isDisposed) {
|
if (this._disposables.isDisposed) {
|
||||||
@ -547,7 +547,7 @@ export function tests() {
|
|||||||
content: {},
|
content: {},
|
||||||
relatedEventId: event2.event_id
|
relatedEventId: event2.event_id
|
||||||
}}));
|
}}));
|
||||||
// 4. subscribe (it's now safe to iterate timeline.entries)
|
// 4. subscribe (it's now safe to iterate timeline.entries)
|
||||||
timeline.entries.subscribe(new ListObserver());
|
timeline.entries.subscribe(new ListObserver());
|
||||||
// 5. check the local relation got correctly aggregated
|
// 5. check the local relation got correctly aggregated
|
||||||
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);
|
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);
|
||||||
|
@ -34,7 +34,7 @@ export abstract class BaseObservable<T> {
|
|||||||
if (this._handlers.size === 1) {
|
if (this._handlers.size === 1) {
|
||||||
this.onSubscribeFirst();
|
this.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
return () => {
|
return (): undefined => {
|
||||||
return this.unsubscribe(handler);
|
return this.unsubscribe(handler);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -63,22 +63,23 @@ export abstract class BaseObservable<T> {
|
|||||||
// Add iterator over handlers here
|
// Add iterator over handlers here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function tests() {
|
export function tests() {
|
||||||
class Collection extends BaseObservable<{}> {
|
class Collection extends BaseObservable<{}> {
|
||||||
firstSubscribeCalls: number = 0;
|
firstSubscribeCalls: number = 0;
|
||||||
firstUnsubscribeCalls: number = 0;
|
firstUnsubscribeCalls: number = 0;
|
||||||
|
|
||||||
onSubscribeFirst() { this.firstSubscribeCalls += 1; }
|
onSubscribeFirst(): void { this.firstSubscribeCalls += 1; }
|
||||||
onUnsubscribeLast() { this.firstUnsubscribeCalls += 1; }
|
onUnsubscribeLast(): void { this.firstUnsubscribeCalls += 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test_unsubscribe(assert) {
|
test_unsubscribe(assert): void {
|
||||||
const c = new Collection();
|
const c = new Collection();
|
||||||
const unsubscribe = c.subscribe({});
|
const unsubscribe = c.subscribe({});
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
assert.equal(c.firstSubscribeCalls, 1);
|
assert.equal(c.firstSubscribeCalls, 1);
|
||||||
assert.equal(c.firstUnsubscribeCalls, 1);
|
assert.equal(c.firstUnsubscribeCalls, 1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ import type {SubscriptionHandle} from "./BaseObservable";
|
|||||||
|
|
||||||
// like an EventEmitter, but doesn't have an event type
|
// like an EventEmitter, but doesn't have an event type
|
||||||
export abstract class BaseObservableValue<T> extends BaseObservable<(value: T) => void> {
|
export abstract class BaseObservableValue<T> extends BaseObservable<(value: T) => void> {
|
||||||
emit(argument: T) {
|
emit(argument: T): void {
|
||||||
for (const h of this._handlers) {
|
for (const h of this._handlers) {
|
||||||
h(argument);
|
h(argument);
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ class WaitForHandle<T> implements IWaitHandle<T> {
|
|||||||
return this._promise;
|
return this._promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose(): void {
|
||||||
if (this._subscription) {
|
if (this._subscription) {
|
||||||
this._subscription();
|
this._subscription();
|
||||||
this._subscription = null;
|
this._subscription = null;
|
||||||
@ -82,7 +82,7 @@ class WaitForHandle<T> implements IWaitHandle<T> {
|
|||||||
|
|
||||||
class ResolvedWaitForHandle<T> implements IWaitHandle<T> {
|
class ResolvedWaitForHandle<T> implements IWaitHandle<T> {
|
||||||
constructor(public promise: Promise<T>) {}
|
constructor(public promise: Promise<T>) {}
|
||||||
dispose() {}
|
dispose(): void {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObservableValue<T> extends BaseObservableValue<T> {
|
export class ObservableValue<T> extends BaseObservableValue<T> {
|
||||||
@ -113,7 +113,7 @@ export class RetainedObservableValue<T> extends ObservableValue<T> {
|
|||||||
this._freeCallback = freeCallback;
|
this._freeCallback = freeCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this._freeCallback();
|
this._freeCallback();
|
||||||
}
|
}
|
||||||
@ -130,7 +130,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this.sourceSubscription = this.sourceSubscription!();
|
this.sourceSubscription = this.sourceSubscription!();
|
||||||
if (this.targetSubscription) {
|
if (this.targetSubscription) {
|
||||||
@ -138,7 +138,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
this.sourceSubscription = this.source.subscribe(() => {
|
this.sourceSubscription = this.source.subscribe(() => {
|
||||||
this.updateTargetSubscription();
|
this.updateTargetSubscription();
|
||||||
@ -147,7 +147,7 @@ export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefi
|
|||||||
this.updateTargetSubscription();
|
this.updateTargetSubscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTargetSubscription() {
|
private updateTargetSubscription(): void {
|
||||||
const sourceValue = this.source.get();
|
const sourceValue = this.source.get();
|
||||||
if (sourceValue) {
|
if (sourceValue) {
|
||||||
const target = this.mapper(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() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
"set emits an update": assert => {
|
"set emits an update": (assert): void => {
|
||||||
const a = new ObservableValue<number>(0);
|
const a = new ObservableValue<number>(0);
|
||||||
let fired = false;
|
let fired = false;
|
||||||
const subscription = a.subscribe(v => {
|
const subscription = a.subscribe(v => {
|
||||||
@ -187,7 +188,7 @@ export function tests() {
|
|||||||
assert(fired);
|
assert(fired);
|
||||||
subscription();
|
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);
|
const a = new ObservableValue(5);
|
||||||
let fired = false;
|
let fired = false;
|
||||||
const subscription = a.subscribe(() => {
|
const subscription = a.subscribe(() => {
|
||||||
@ -198,24 +199,24 @@ export function tests() {
|
|||||||
assert(!fired);
|
assert(!fired);
|
||||||
subscription();
|
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 a = new ObservableValue(5);
|
||||||
const handle = a.waitFor(v => v === 6);
|
const handle = a.waitFor(v => v === 6);
|
||||||
Promise.resolve().then(() => {
|
await Promise.resolve().then(() => {
|
||||||
a.set(6);
|
a.set(6);
|
||||||
});
|
});
|
||||||
await handle.promise;
|
await handle.promise;
|
||||||
assert.strictEqual(a.get(), 6);
|
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 a = new ObservableValue<number>(0);
|
||||||
const handle = a.waitFor(() => false);
|
const handle = a.waitFor(() => false);
|
||||||
Promise.resolve().then(() => {
|
await Promise.resolve().then(() => {
|
||||||
handle.dispose();
|
handle.dispose();
|
||||||
});
|
});
|
||||||
await assert.rejects(handle.promise, AbortError);
|
await assert.rejects(handle.promise, AbortError);
|
||||||
},
|
},
|
||||||
"flatMap.get": assert => {
|
"flatMap.get": (assert): void => {
|
||||||
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
||||||
const countProxy = a.flatMap(a => a!.count);
|
const countProxy = a.flatMap(a => a!.count);
|
||||||
assert.strictEqual(countProxy.get(), undefined);
|
assert.strictEqual(countProxy.get(), undefined);
|
||||||
@ -223,7 +224,7 @@ export function tests() {
|
|||||||
a.set({count});
|
a.set({count});
|
||||||
assert.strictEqual(countProxy.get(), 0);
|
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 a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
||||||
const updates: (number | undefined)[] = [];
|
const updates: (number | undefined)[] = [];
|
||||||
a.flatMap(a => a!.count).subscribe(count => {
|
a.flatMap(a => a!.count).subscribe(count => {
|
||||||
@ -233,7 +234,7 @@ export function tests() {
|
|||||||
a.set({count});
|
a.set({count});
|
||||||
assert.deepEqual(updates, [0]);
|
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 a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
||||||
const updates: (number | undefined)[] = [];
|
const updates: (number | undefined)[] = [];
|
||||||
a.flatMap(a => a!.count).subscribe(count => {
|
a.flatMap(a => a!.count).subscribe(count => {
|
||||||
@ -244,5 +245,5 @@ export function tests() {
|
|||||||
count.set(5);
|
count.set(5);
|
||||||
assert.deepEqual(updates, [0, 5]);
|
assert.deepEqual(updates, [0, 5]);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -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
24
src/observable/index.ts
Normal 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";
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {IListObserver} from "./BaseObservableList";
|
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> {
|
export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> implements IListObserver<F> {
|
||||||
private _eventQueue: AsyncEvent<F>[] | null = null;
|
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));
|
this._eventQueue.push(new AddEvent(idx, item));
|
||||||
idx += 1;
|
idx += 1;
|
||||||
}
|
}
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _flush(): Promise<void> {
|
async _flush(): Promise<void> {
|
||||||
@ -52,35 +52,35 @@ export class AsyncMappedList<F,T> extends BaseMappedList<F,T,Promise<T>> impleme
|
|||||||
onReset(): void {
|
onReset(): void {
|
||||||
if (this._eventQueue) {
|
if (this._eventQueue) {
|
||||||
this._eventQueue.push(new ResetEvent());
|
this._eventQueue.push(new ResetEvent());
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(index: number, value: F): void {
|
onAdd(index: number, value: F): void {
|
||||||
if (this._eventQueue) {
|
if (this._eventQueue) {
|
||||||
this._eventQueue.push(new AddEvent(index, value));
|
this._eventQueue.push(new AddEvent(index, value));
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(index: number, value: F, params: any): void {
|
onUpdate(index: number, value: F, params: any): void {
|
||||||
if (this._eventQueue) {
|
if (this._eventQueue) {
|
||||||
this._eventQueue.push(new UpdateEvent(index, value, params));
|
this._eventQueue.push(new UpdateEvent(index, value, params));
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(index: number): void {
|
onRemove(index: number): void {
|
||||||
if (this._eventQueue) {
|
if (this._eventQueue) {
|
||||||
this._eventQueue.push(new RemoveEvent(index));
|
this._eventQueue.push(new RemoveEvent(index));
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMove(fromIdx: number, toIdx: number): void {
|
onMove(fromIdx: number, toIdx: number): void {
|
||||||
if (this._eventQueue) {
|
if (this._eventQueue) {
|
||||||
this._eventQueue.push(new MoveEvent(fromIdx, toIdx));
|
this._eventQueue.push(new MoveEvent(fromIdx, toIdx));
|
||||||
this._flush();
|
void this._flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,10 +135,12 @@ class ResetEvent<F> {
|
|||||||
import {ObservableArray} from "./ObservableArray";
|
import {ObservableArray} from "./ObservableArray";
|
||||||
import {ListObserver} from "../../mocks/ListObserver.js";
|
import {ListObserver} from "../../mocks/ListObserver.js";
|
||||||
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function tests() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
"events are emitted in order": async assert => {
|
"events are emitted in order": async (assert): Promise<void> => {
|
||||||
const double = n => n * n;
|
const double = (n: number): number => n * n;
|
||||||
const source = new ObservableArray<number>();
|
const source = new ObservableArray<number>();
|
||||||
const mapper = new AsyncMappedList(source, async n => {
|
const mapper = new AsyncMappedList(source, async n => {
|
||||||
await new Promise(r => setTimeout(r, n));
|
await new Promise(r => setTimeout(r, n));
|
||||||
@ -150,7 +152,7 @@ export function tests() {
|
|||||||
mapper.subscribe(observer);
|
mapper.subscribe(observer);
|
||||||
source.append(2); // will sleep this amount, so second append would take less time
|
source.append(2); // will sleep this amount, so second append would take less time
|
||||||
source.append(1);
|
source.append(1);
|
||||||
source.update(0, 7, "lucky seven")
|
source.update(0, 7, "lucky seven");
|
||||||
source.remove(0);
|
source.remove(0);
|
||||||
{
|
{
|
||||||
const {type, index, value} = await observer.next();
|
const {type, index, value} = await observer.next();
|
||||||
@ -182,5 +184,5 @@ export function tests() {
|
|||||||
assert.equal(value.n, 49);
|
assert.equal(value.n, 49);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -37,15 +37,15 @@ export class BaseMappedList<F,T,R = T> extends BaseObservableList<T> {
|
|||||||
this._removeCallback = removeCallback;
|
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);
|
return findAndUpdateInArray(predicate, this._mappedValues!, this, updater);
|
||||||
}
|
}
|
||||||
|
|
||||||
get length() {
|
get length(): number {
|
||||||
return this._mappedValues!.length;
|
return this._mappedValues!.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): IterableIterator<T> {
|
||||||
return this._mappedValues!.values();
|
return this._mappedValues!.values();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,17 +26,17 @@ export interface IListObserver<T> {
|
|||||||
|
|
||||||
export function defaultObserverWith<T>(overrides: { [key in keyof IListObserver<T>]?: IListObserver<T>[key] }): IListObserver<T> {
|
export function defaultObserverWith<T>(overrides: { [key in keyof IListObserver<T>]?: IListObserver<T>[key] }): IListObserver<T> {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
onReset(){},
|
onReset(): void {},
|
||||||
onAdd(){},
|
onAdd(): void {},
|
||||||
onUpdate(){},
|
onUpdate(): void {},
|
||||||
onRemove(){},
|
onRemove(): void {},
|
||||||
onMove(){},
|
onMove(): void {},
|
||||||
}
|
};
|
||||||
return Object.assign(defaults, overrides);
|
return Object.assign(defaults, overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> implements Iterable<T> {
|
export abstract class BaseObservableList<T> extends BaseObservable<IListObserver<T>> implements Iterable<T> {
|
||||||
emitReset() {
|
emitReset(): void {
|
||||||
for(let h of this._handlers) {
|
for(let h of this._handlers) {
|
||||||
h.onReset(this);
|
h.onReset(this);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
|
|||||||
onReset(): void {
|
onReset(): void {
|
||||||
// TODO: not ideal if other source lists are large
|
// TODO: not ideal if other source lists are large
|
||||||
// but working impl for now
|
// but working impl for now
|
||||||
// reset, and
|
// reset, and
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
for(const item of this) {
|
for(const item of this) {
|
||||||
@ -86,11 +86,11 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
|
|||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): Iterator<T> {
|
||||||
let sourceListIdx = 0;
|
let sourceListIdx = 0;
|
||||||
let it = this._sourceLists[0][Symbol.iterator]();
|
let it = this._sourceLists[0][Symbol.iterator]();
|
||||||
return {
|
return {
|
||||||
next: () => {
|
next: (): IteratorResult<T> => {
|
||||||
let result = it.next();
|
let result = it.next();
|
||||||
while (result.done) {
|
while (result.done) {
|
||||||
sourceListIdx += 1;
|
sourceListIdx += 1;
|
||||||
@ -102,22 +102,25 @@ export class ConcatList<T> extends BaseObservableList<T> implements IListObserve
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {ObservableArray} from "./ObservableArray";
|
import {ObservableArray} from "./ObservableArray";
|
||||||
import {defaultObserverWith} from "./BaseObservableList";
|
import {defaultObserverWith} from "./BaseObservableList";
|
||||||
|
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export async function tests() {
|
export async function tests() {
|
||||||
return {
|
return {
|
||||||
test_length(assert) {
|
test_length(assert): void {
|
||||||
const all = new ConcatList(
|
const all = new ConcatList(
|
||||||
new ObservableArray([1, 2, 3]),
|
new ObservableArray([1, 2, 3]),
|
||||||
new ObservableArray([11, 12, 13])
|
new ObservableArray([11, 12, 13])
|
||||||
);
|
);
|
||||||
assert.equal(all.length, 6);
|
assert.equal(all.length, 6);
|
||||||
},
|
},
|
||||||
test_iterator(assert) {
|
test_iterator(assert): void {
|
||||||
const all = new ConcatList(
|
const all = new ConcatList(
|
||||||
new ObservableArray([1, 2, 3]),
|
new ObservableArray([1, 2, 3]),
|
||||||
new ObservableArray([11, 12, 13])
|
new ObservableArray([11, 12, 13])
|
||||||
@ -131,7 +134,7 @@ export async function tests() {
|
|||||||
assert.equal(it.next().value, 13);
|
assert.equal(it.next().value, 13);
|
||||||
assert(it.next().done);
|
assert(it.next().done);
|
||||||
},
|
},
|
||||||
test_add(assert) {
|
test_add(assert): void {
|
||||||
const list1 = new ObservableArray([1, 2, 3]);
|
const list1 = new ObservableArray([1, 2, 3]);
|
||||||
const list2 = new ObservableArray([11, 12, 13]);
|
const list2 = new ObservableArray([11, 12, 13]);
|
||||||
const all = new ConcatList(list1, list2);
|
const all = new ConcatList(list1, list2);
|
||||||
@ -146,7 +149,7 @@ export async function tests() {
|
|||||||
list2.insert(1, 11.5);
|
list2.insert(1, 11.5);
|
||||||
assert(fired);
|
assert(fired);
|
||||||
},
|
},
|
||||||
test_update(assert) {
|
test_update(assert): void {
|
||||||
const list1 = new ObservableArray([1, 2, 3]);
|
const list1 = new ObservableArray([1, 2, 3]);
|
||||||
const list2 = new ObservableArray([11, 12, 13]);
|
const list2 = new ObservableArray([11, 12, 13]);
|
||||||
const all = new ConcatList(list1, list2);
|
const all = new ConcatList(list1, list2);
|
||||||
|
@ -19,7 +19,7 @@ import {IListObserver} from "./BaseObservableList";
|
|||||||
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
|
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList";
|
||||||
|
|
||||||
export class MappedList<F,T> extends BaseMappedList<F,T> implements IListObserver<F> {
|
export class MappedList<F,T> extends BaseMappedList<F,T> implements IListObserver<F> {
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
this._sourceUnsubscribe = this._sourceList.subscribe(this);
|
this._sourceUnsubscribe = this._sourceList.subscribe(this);
|
||||||
this._mappedValues = [];
|
this._mappedValues = [];
|
||||||
for (const item of this._sourceList) {
|
for (const item of this._sourceList) {
|
||||||
@ -61,18 +61,21 @@ import {ObservableArray} from "./ObservableArray";
|
|||||||
import {BaseObservableList} from "./BaseObservableList";
|
import {BaseObservableList} from "./BaseObservableList";
|
||||||
import {defaultObserverWith} from "./BaseObservableList";
|
import {defaultObserverWith} from "./BaseObservableList";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export async function tests() {
|
export async function tests() {
|
||||||
class MockList extends BaseObservableList<number> {
|
class MockList extends BaseObservableList<number> {
|
||||||
get length() {
|
get length(): 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator]() {
|
||||||
return [].values();
|
return [].values();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
test_add(assert) {
|
test_add(assert): void {
|
||||||
const source = new MockList();
|
const source = new MockList();
|
||||||
const mapped = new MappedList(source, n => {return {n: n*n};});
|
const mapped = new MappedList(source, n => {return {n: n*n};});
|
||||||
let fired = false;
|
let fired = false;
|
||||||
@ -87,7 +90,7 @@ export async function tests() {
|
|||||||
assert(fired);
|
assert(fired);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
},
|
},
|
||||||
test_update(assert) {
|
test_update(assert): void {
|
||||||
const source = new MockList();
|
const source = new MockList();
|
||||||
const mapped = new MappedList<number, { n: number, m?: number }>(
|
const mapped = new MappedList<number, { n: number, m?: number }>(
|
||||||
source,
|
source,
|
||||||
@ -109,7 +112,7 @@ export async function tests() {
|
|||||||
assert(fired);
|
assert(fired);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
},
|
},
|
||||||
"test findAndUpdate not found": assert => {
|
"test findAndUpdate not found": (assert): void => {
|
||||||
const source = new ObservableArray([1, 3, 4]);
|
const source = new ObservableArray([1, 3, 4]);
|
||||||
const mapped = new MappedList(
|
const mapped = new MappedList(
|
||||||
source,
|
source,
|
||||||
@ -123,7 +126,7 @@ export async function tests() {
|
|||||||
() => assert.fail()
|
() => assert.fail()
|
||||||
), false);
|
), 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 source = new ObservableArray([1, 3, 4]);
|
||||||
const mapped = new MappedList(
|
const mapped = new MappedList(
|
||||||
source,
|
source,
|
||||||
@ -143,7 +146,7 @@ export async function tests() {
|
|||||||
), true);
|
), true);
|
||||||
assert.equal(fired, true);
|
assert.equal(fired, true);
|
||||||
},
|
},
|
||||||
"test findAndUpdate emits update": assert => {
|
"test findAndUpdate emits update": (assert): void => {
|
||||||
const source = new ObservableArray([1, 3, 4]);
|
const source = new ObservableArray([1, 3, 4]);
|
||||||
const mapped = new MappedList(
|
const mapped = new MappedList(
|
||||||
source,
|
source,
|
||||||
@ -161,6 +164,6 @@ export async function tests() {
|
|||||||
assert.equal(mapped.findAndUpdate(n => n === 9, () => "param"), true);
|
assert.equal(mapped.findAndUpdate(n => n === 9, () => "param"), true);
|
||||||
assert.equal(fired, true);
|
assert.equal(fired, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,7 @@ export class ObservableArray<T> extends BaseObservableList<T> {
|
|||||||
return this._items.length;
|
return this._items.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): IterableIterator<T> {
|
||||||
return this._items.values();
|
return this._items.values();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export class SortedArray<T> extends BaseObservableList<T> {
|
|||||||
const idx = sortedIndex(this._items, item, this._comparator);
|
const idx = sortedIndex(this._items, item, this._comparator);
|
||||||
if (idx >= this._items.length || this._comparator(this._items[idx], item) !== 0) {
|
if (idx >= this._items.length || this._comparator(this._items[idx], item) !== 0) {
|
||||||
this._items.splice(idx, 0, item);
|
this._items.splice(idx, 0, item);
|
||||||
this.emitAdd(idx, item)
|
this.emitAdd(idx, item);
|
||||||
} else {
|
} else {
|
||||||
this._items[idx] = item;
|
this._items[idx] = item;
|
||||||
this.emitUpdate(idx, item, updateParams);
|
this.emitUpdate(idx, item, updateParams);
|
||||||
@ -112,52 +112,46 @@ export class SortedArray<T> extends BaseObservableList<T> {
|
|||||||
return this._items.length;
|
return this._items.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): Iterator<T> {
|
||||||
return new Iterator(this);
|
return new Iterator(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// iterator that works even if the current value is removed while iterating
|
// iterator that works even if the current value is removed while iterating
|
||||||
class Iterator<T> {
|
class Iterator<T> {
|
||||||
private _sortedArray: SortedArray<T> | null
|
private _sortedArray: SortedArray<T>;
|
||||||
private _current: T | null | undefined
|
private _current: T | null | undefined;
|
||||||
|
private _consumed: boolean = false;
|
||||||
|
|
||||||
constructor(sortedArray: SortedArray<T>) {
|
constructor(sortedArray: SortedArray<T>) {
|
||||||
this._sortedArray = sortedArray;
|
this._sortedArray = sortedArray;
|
||||||
this._current = null;
|
this._current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next(): IteratorResult<T> {
|
||||||
if (this._sortedArray) {
|
if (this._consumed) {
|
||||||
if (this._current) {
|
return {value: undefined, done: true};
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!this._sortedArray) {
|
this._current = this._current? this._sortedArray._getNext(this._current): this._sortedArray.get(0);
|
||||||
return {done: true};
|
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() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
"setManyUnsorted": assert => {
|
"setManyUnsorted": (assert): void => {
|
||||||
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
|
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
|
||||||
sa.setManyUnsorted(["b", "a", "c"]);
|
sa.setManyUnsorted(["b", "a", "c"]);
|
||||||
assert.equal(sa.length, 3);
|
assert.equal(sa.length, 3);
|
||||||
assert.equal(sa.get(0), "a");
|
assert.equal(sa.get(0), "a");
|
||||||
assert.equal(sa.get(1), "b");
|
assert.equal(sa.get(1), "b");
|
||||||
assert.equal(sa.get(2), "c");
|
assert.equal(sa.get(2), "c");
|
||||||
},
|
},
|
||||||
"_getNext": assert => {
|
"_getNext": (assert): void => {
|
||||||
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
|
const sa = new SortedArray<string>((a, b) => a.localeCompare(b));
|
||||||
sa.setManyUnsorted(["b", "a", "f"]);
|
sa.setManyUnsorted(["b", "a", "f"]);
|
||||||
assert.equal(sa._getNext("a"), "b");
|
assert.equal(sa._getNext("a"), "b");
|
||||||
@ -166,7 +160,7 @@ export function tests() {
|
|||||||
assert.equal(sa._getNext("c"), "f");
|
assert.equal(sa._getNext("c"), "f");
|
||||||
assert.equal(sa._getNext("f"), undefined);
|
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);
|
const queue = new SortedArray<{idx: number}>((a, b) => a.idx - b.idx);
|
||||||
queue.setManyUnsorted([{idx: 5}, {idx: 3}, {idx: 1}, {idx: 4}, {idx: 2}]);
|
queue.setManyUnsorted([{idx: 5}, {idx: 3}, {idx: 1}, {idx: 4}, {idx: 2}]);
|
||||||
const it = queue[Symbol.iterator]();
|
const it = queue[Symbol.iterator]();
|
||||||
@ -183,5 +177,5 @@ export function tests() {
|
|||||||
// check done persists
|
// check done persists
|
||||||
assert.equal(it.next().done, true);
|
assert.equal(it.next().done, true);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export class SortedMapList extends BaseObservableList {
|
|||||||
this._sortedPairs = null;
|
this._sortedPairs = null;
|
||||||
this._mapSubscription = null;
|
this._mapSubscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key, value) {
|
||||||
const pair = {key, value};
|
const pair = {key, value};
|
||||||
const idx = sortedIndex(this._sortedPairs, pair, this._comparator);
|
const idx = sortedIndex(this._sortedPairs, pair, this._comparator);
|
||||||
@ -129,11 +129,11 @@ export class SortedMapList extends BaseObservableList {
|
|||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {ObservableMap} from "../map/ObservableMap";
|
import {ObservableMap} from "../";
|
||||||
|
|
||||||
export function tests() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
@ -267,5 +267,5 @@ export function tests() {
|
|||||||
assert.equal(updateFired, 1);
|
assert.equal(updateFired, 1);
|
||||||
assert.deepEqual(Array.from(list).map(v => v.number), [1, 3, 11]);
|
assert.deepEqual(Array.from(list).map(v => v.number), [1, 3, 11]);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,12 @@ limitations under the License.
|
|||||||
import {BaseObservableList} from "./BaseObservableList";
|
import {BaseObservableList} from "./BaseObservableList";
|
||||||
|
|
||||||
/* inline update of item in collection backed by array, without replacing the preexising item */
|
/* 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);
|
const index = array.findIndex(predicate);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const value = array[index];
|
const value = array[index];
|
||||||
|
@ -14,52 +14,62 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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();
|
super();
|
||||||
this._source = source;
|
this._source = source;
|
||||||
this._apply = apply;
|
this._apply = apply;
|
||||||
this._subscription = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasApply() {
|
hasApply(): boolean {
|
||||||
return !!this._apply;
|
return !!this._apply;
|
||||||
}
|
}
|
||||||
|
|
||||||
setApply(apply) {
|
setApply(apply?: Apply<K, V>): void {
|
||||||
this._apply = apply;
|
this._apply = apply;
|
||||||
if (apply) {
|
if (this._apply) {
|
||||||
this.applyOnce(this._apply);
|
this.applyOnce(this._apply);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyOnce(apply) {
|
applyOnce(apply: Apply<K, V>): void {
|
||||||
for (const [key, value] of this._source) {
|
for (const [key, value] of this._source) {
|
||||||
apply(key, value);
|
apply(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key: K, value: V): void {
|
||||||
if (this._apply) {
|
if (this._apply) {
|
||||||
this._apply(key, value);
|
this._apply(key, value);
|
||||||
}
|
}
|
||||||
this.emitAdd(key, value);
|
this.emitAdd(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key, value) {
|
onRemove(key: K, value: V): void {
|
||||||
this.emitRemove(key, value);
|
this.emitRemove(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onUpdate(key, value, params) {
|
onUpdate(key: K, value: V, params: any): void {
|
||||||
if (this._apply) {
|
if (this._apply) {
|
||||||
this._apply(key, value, params);
|
this._apply(key, value, params);
|
||||||
}
|
}
|
||||||
this.emitUpdate(key, value, params);
|
this.emitUpdate(key, value, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
this._subscription = this._source.subscribe(this);
|
this._subscription = this._source.subscribe(this);
|
||||||
if (this._apply) {
|
if (this._apply) {
|
||||||
this.applyOnce(this._apply);
|
this.applyOnce(this._apply);
|
||||||
@ -67,27 +77,31 @@ export class ApplyMap extends BaseObservableMap {
|
|||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this._subscription = this._subscription();
|
if (this._subscription) {
|
||||||
|
this._subscription = this._subscription();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset(): void {
|
||||||
if (this._apply) {
|
if (this._apply) {
|
||||||
this.applyOnce(this._apply);
|
this.applyOnce(this._apply);
|
||||||
}
|
}
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): Iterator<[K, V]> {
|
||||||
return this._source[Symbol.iterator]();
|
return this._source[Symbol.iterator]();
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size(): number {
|
||||||
return this._source.size;
|
return this._source.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key: K): V | undefined {
|
||||||
return this._source.get(key);
|
return this._source.get(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Apply<K, V> = (key: K, value: V, params?: any) => void;
|
@ -15,6 +15,11 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {BaseObservable} from "../BaseObservable";
|
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> {
|
export interface IMapObserver<K, V> {
|
||||||
onReset(): void;
|
onReset(): void;
|
||||||
@ -23,33 +28,70 @@ export interface IMapObserver<K, V> {
|
|||||||
onRemove(key: K, value: V): void
|
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>> {
|
export abstract class BaseObservableMap<K, V> extends BaseObservable<IMapObserver<K, V>> {
|
||||||
emitReset() {
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitReset(): void {
|
||||||
for(let h of this._handlers) {
|
for(let h of this._handlers) {
|
||||||
h.onReset();
|
h.onReset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we need batch events, mostly on index based collection though?
|
// we need batch events, mostly on index based collection though?
|
||||||
// maybe we should get started without?
|
// maybe we should get started without?
|
||||||
emitAdd(key: K, value: V) {
|
emitAdd(key: K, value: V): void {
|
||||||
for(let h of this._handlers) {
|
for(let h of this._handlers) {
|
||||||
h.onAdd(key, value);
|
h.onAdd(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitUpdate(key, value, params) {
|
emitUpdate(key: K, value: V, params: any): void {
|
||||||
for(let h of this._handlers) {
|
for(let h of this._handlers) {
|
||||||
h.onUpdate(key, value, params);
|
h.onUpdate(key, value, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitRemove(key, value) {
|
emitRemove(key: K, value: V): void {
|
||||||
for(let h of this._handlers) {
|
for(let h of this._handlers) {
|
||||||
h.onRemove(key, value);
|
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 [Symbol.iterator](): Iterator<[K, V]>;
|
||||||
abstract get size(): number;
|
abstract get size(): number;
|
||||||
abstract get(key: K): V | undefined;
|
abstract get(key: K): V | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
@ -14,19 +14,28 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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();
|
super();
|
||||||
this._source = source;
|
this._source = source;
|
||||||
this._filter = filter;
|
this._filter = filter;
|
||||||
/** @type {Map<string, bool>} */
|
|
||||||
this._included = null;
|
|
||||||
this._subscription = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFilter(filter) {
|
setFilter(filter: Filter<K, V>): void {
|
||||||
this._filter = filter;
|
this._filter = filter;
|
||||||
if (this._subscription) {
|
if (this._subscription) {
|
||||||
this._reapplyFilter();
|
this._reapplyFilter();
|
||||||
@ -36,7 +45,7 @@ export class FilteredMap extends BaseObservableMap {
|
|||||||
/**
|
/**
|
||||||
* reapply the filter
|
* reapply the filter
|
||||||
*/
|
*/
|
||||||
_reapplyFilter(silent = false) {
|
_reapplyFilter(silent = false): void {
|
||||||
if (this._filter) {
|
if (this._filter) {
|
||||||
const oldIncluded = this._included;
|
const oldIncluded = this._included;
|
||||||
this._included = this._included || new Map();
|
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) {
|
if (this._filter) {
|
||||||
const included = this._filter(value, key);
|
if (this._included) {
|
||||||
this._included.set(key, included);
|
const included = this._filter(value, key);
|
||||||
if (!included) {
|
this._included.set(key, included);
|
||||||
return;
|
if (!included) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("Internal logic error: FilteredMap._included used before initialized");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.emitAdd(key, value);
|
this.emitAdd(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key, value) {
|
onRemove(key: K, value: V): void {
|
||||||
const wasIncluded = !this._filter || this._included.get(key);
|
const wasIncluded = !this._filter || this._included?.get(key);
|
||||||
this._included.delete(key);
|
if (this._included) {
|
||||||
if (wasIncluded) {
|
this._included.delete(key);
|
||||||
this.emitRemove(key, value);
|
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 an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
|
||||||
if (!this._included) {
|
if (!this._included) {
|
||||||
return;
|
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) {
|
if (wasIncluded && !isIncluded) {
|
||||||
this.emitRemove(key, value);
|
this.emitRemove(key, value);
|
||||||
} else if (!wasIncluded && isIncluded) {
|
} else if (!wasIncluded && isIncluded) {
|
||||||
@ -106,30 +123,32 @@ export class FilteredMap extends BaseObservableMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
this._subscription = this._source.subscribe(this);
|
this._subscription = this._source.subscribe(this);
|
||||||
this._reapplyFilter(true);
|
this._reapplyFilter(true);
|
||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this._included = null;
|
this._included = undefined;
|
||||||
this._subscription = this._subscription();
|
if (this._subscription) {
|
||||||
|
this._subscription = this._subscription();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset(): void {
|
||||||
this._reapplyFilter();
|
this._reapplyFilter();
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): FilterIterator<K, V> {
|
||||||
return new FilterIterator(this._source, this._included);
|
return new FilterIterator<K, V>(this._source, this._included);
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size(): number {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
this._included.forEach(included => {
|
this._included?.forEach(included => {
|
||||||
if (included) {
|
if (included) {
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
@ -137,7 +156,7 @@ export class FilteredMap extends BaseObservableMap {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key: K): V | undefined {
|
||||||
const value = this._source.get(key);
|
const value = this._source.get(key);
|
||||||
if (value && this._filter(value, key)) {
|
if (value && this._filter(value, key)) {
|
||||||
return value;
|
return value;
|
||||||
@ -145,13 +164,15 @@ export class FilteredMap extends BaseObservableMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FilterIterator {
|
class FilterIterator<K, V> {
|
||||||
constructor(map, _included) {
|
private _included?: Map<K, boolean>
|
||||||
this._included = _included;
|
private _sourceIterator: Iterator<[K, V], any, undefined>
|
||||||
|
constructor(map: BaseObservableMap<K, V>, included?: Map<K, boolean>) {
|
||||||
|
this._included = included;
|
||||||
this._sourceIterator = map[Symbol.iterator]();
|
this._sourceIterator = map[Symbol.iterator]();
|
||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next(): IteratorResult<[K, V]> {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
const sourceResult = this._sourceIterator.next();
|
const sourceResult = this._sourceIterator.next();
|
||||||
@ -159,47 +180,62 @@ class FilterIterator {
|
|||||||
return sourceResult;
|
return sourceResult;
|
||||||
}
|
}
|
||||||
const key = sourceResult.value[0];
|
const key = sourceResult.value[0];
|
||||||
if (this._included.get(key)) {
|
if (this._included?.get(key)) {
|
||||||
return sourceResult;
|
return sourceResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import {ObservableMap} from "./ObservableMap";
|
import {ObservableMap} from "..";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function tests() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
"filter preloaded list": assert => {
|
"filter preloaded list": (assert): void => {
|
||||||
const source = new ObservableMap();
|
const source = new ObservableMap();
|
||||||
source.add("one", 1);
|
source.add("one", 1);
|
||||||
source.add("two", 2);
|
source.add("two", 2);
|
||||||
source.add("three", 3);
|
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
|
// can only iterate after subscribing
|
||||||
oddNumbers.subscribe({});
|
oddNumbers.subscribe({
|
||||||
|
onAdd() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
onRemove() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
onUpdate() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
onReset() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
assert.equal(oddNumbers.size, 2);
|
assert.equal(oddNumbers.size, 2);
|
||||||
const it = oddNumbers[Symbol.iterator]();
|
const it = oddNumbers[Symbol.iterator]();
|
||||||
assert.deepEqual(it.next().value, ["one", 1]);
|
assert.deepEqual(it.next().value, ["one", 1]);
|
||||||
assert.deepEqual(it.next().value, ["three", 3]);
|
assert.deepEqual(it.next().value, ["three", 3]);
|
||||||
assert.equal(it.next().done, true);
|
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();
|
const source = new ObservableMap();
|
||||||
let count_add = 0, count_update = 0, count_remove = 0;
|
let count_add = 0, count_update = 0, count_remove = 0;
|
||||||
source.add("num1", 1);
|
source.add("num1", 1);
|
||||||
source.add("num2", 2);
|
source.add("num2", 2);
|
||||||
source.add("num3", 3);
|
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({
|
oddMap.subscribe({
|
||||||
onAdd() {
|
onAdd() {
|
||||||
count_add += 1;
|
count_add += 1;
|
||||||
@ -209,6 +245,9 @@ export function tests() {
|
|||||||
},
|
},
|
||||||
onUpdate() {
|
onUpdate() {
|
||||||
count_update += 1;
|
count_update += 1;
|
||||||
|
},
|
||||||
|
onReset() {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
source.set("num3", 4);
|
source.set("num3", 4);
|
||||||
@ -218,5 +257,5 @@ export function tests() {
|
|||||||
assert.strictEqual(count_update, 1);
|
assert.strictEqual(count_update, 1);
|
||||||
assert.strictEqual(count_remove, 1);
|
assert.strictEqual(count_remove, 1);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
@ -14,16 +14,25 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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();
|
super();
|
||||||
this._sources = sources;
|
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)) {
|
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||||
const occludingValue = this._getValueFromOccludedSources(source, key);
|
const occludingValue = this._getValueFromOccludedSources(source, key);
|
||||||
if (occludingValue !== undefined) {
|
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)) {
|
if (!this._isKeyAtSourceOccluded(source, key)) {
|
||||||
this.emitRemove(key, value);
|
this.emitRemove(key, value);
|
||||||
const occludedValue = this._getValueFromOccludedSources(source, key);
|
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 an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
|
||||||
if (!this._subscriptions) {
|
if (!this._subscriptions) {
|
||||||
return;
|
return;
|
||||||
@ -57,16 +66,16 @@ export class JoinedMap extends BaseObservableMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset(): void {
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
this._subscriptions = this._sources.map(source => new SourceSubscriptionHandler(source, this).subscribe());
|
this._subscriptions = this._sources.map(source => new SourceSubscriptionHandler(source, this).subscribe());
|
||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
_isKeyAtSourceOccluded(source, key) {
|
_isKeyAtSourceOccluded(source: BaseObservableMap<K, V>, key: K): boolean {
|
||||||
// sources that come first in the sources array can
|
// sources that come first in the sources array can
|
||||||
// hide the keys in later sources, to prevent events
|
// hide the keys in later sources, to prevent events
|
||||||
// being emitted for the same key and different values,
|
// 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
|
// 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
|
// sources that come first in the sources array can
|
||||||
// hide the keys in later sources, to prevent events
|
// hide the keys in later sources, to prevent events
|
||||||
// being emitted for the same key and different values,
|
// being emitted for the same key and different values,
|
||||||
@ -97,53 +106,58 @@ export class JoinedMap extends BaseObservableMap {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
for (const s of this._subscriptions) {
|
if (this._subscriptions) {
|
||||||
s.dispose();
|
for (const s of this._subscriptions) {
|
||||||
|
s.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
[Symbol.iterator]() {
|
[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);
|
return this._sources.reduce((sum, s) => sum + s.size, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key: K): V | undefined {
|
||||||
for (const s of this._sources) {
|
for (const s of this._sources) {
|
||||||
const value = s.get(key);
|
const value = s.get(key);
|
||||||
if (value) {
|
if (value) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JoinedIterator {
|
class JoinedIterator<K, V> implements Iterator<[K, V]> {
|
||||||
constructor(sources) {
|
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._sources = sources;
|
||||||
this._sourceIndex = -1;
|
|
||||||
this._currentIterator = null;
|
|
||||||
this._encounteredKeys = new Set();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next() {
|
next(): IteratorYieldResult<[K, V]> | IteratorReturnResult<null> {
|
||||||
let result;
|
let result: IteratorYieldResult<[K, V]> | undefined = undefined;
|
||||||
while (!result) {
|
while (!result) {
|
||||||
if (!this._currentIterator) {
|
if (!this._currentIterator) {
|
||||||
this._sourceIndex += 1;
|
this._sourceIndex += 1;
|
||||||
if (this._sources.length <= this._sourceIndex) {
|
if (this._sources.length <= this._sourceIndex) {
|
||||||
return {done: true};
|
return {done: true, value: null};
|
||||||
}
|
}
|
||||||
this._currentIterator = this._sources[this._sourceIndex][Symbol.iterator]();
|
this._currentIterator = this._sources[this._sourceIndex][Symbol.iterator]();
|
||||||
}
|
}
|
||||||
const sourceResult = this._currentIterator.next();
|
const sourceResult = this._currentIterator?.next();
|
||||||
if (sourceResult.done) {
|
if (!sourceResult || sourceResult.done) {
|
||||||
this._currentIterator = null;
|
this._currentIterator = undefined;
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
const key = sourceResult.value[0];
|
const key = sourceResult.value[0];
|
||||||
@ -157,66 +171,73 @@ class JoinedIterator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceSubscriptionHandler {
|
class SourceSubscriptionHandler<K, V> {
|
||||||
constructor(source, joinedMap) {
|
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._source = source;
|
||||||
this._joinedMap = joinedMap;
|
this._joinedMap = joinedMap;
|
||||||
this._subscription = null;
|
this._subscription = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe() {
|
subscribe(): this {
|
||||||
this._subscription = this._source.subscribe(this);
|
this._subscription = this._source.subscribe(this);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose(): void {
|
||||||
this._subscription = this._subscription();
|
if (this._subscription) this._subscription = this._subscription();
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key: K, value: V): void {
|
||||||
this._joinedMap.onAdd(this._source, key, value);
|
this._joinedMap.onAdd(this._source, key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key, value) {
|
onRemove(key: K, value: V): void {
|
||||||
this._joinedMap.onRemove(this._source, key, value);
|
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);
|
this._joinedMap.onUpdate(this._source, key, value, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset(): void {
|
||||||
this._joinedMap.onReset(this._source);
|
this._joinedMap.onReset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
import { ObservableMap } from "./ObservableMap";
|
import {ObservableMap} from "..";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
export function tests() {
|
export function tests() {
|
||||||
|
function observeMap(map: JoinedMap<any, any>): { type: string; key: any; value: any; params?: any; }[] {
|
||||||
function observeMap(map) {
|
const events: { type: string, key: any, value: any, params?: any }[] = [];
|
||||||
const events = [];
|
|
||||||
map.subscribe({
|
map.subscribe({
|
||||||
onAdd(key, value) { events.push({type: "add", key, value}); },
|
onAdd(key, value) { events.push({ type: "add", key, value }); },
|
||||||
onRemove(key, value) { events.push({type: "remove", key, value}); },
|
onRemove(key, value) { events.push({ type: "remove", key, value }); },
|
||||||
onUpdate(key, value, params) { events.push({type: "update", key, value, params}); }
|
onUpdate(key, value, params) { events.push({ type: "update", key, value, params }); },
|
||||||
|
onReset: function (): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"joined iterator": assert => {
|
"joined iterator": (assert): void => {
|
||||||
const firstKV = ["a", 1];
|
const firstKV: [string, number] = ["a", 1];
|
||||||
const secondKV = ["b", 2];
|
const secondKV: [string, number] = ["b", 2];
|
||||||
const thirdKV = ["c", 3];
|
const thirdKV: [string, number] = ["c", 3];
|
||||||
const it = new JoinedIterator([[firstKV, secondKV], [thirdKV]]);
|
const it = new JoinedIterator<string, number>([[firstKV, secondKV], [thirdKV]]);
|
||||||
assert.equal(it.next().value, firstKV);
|
assert.equal(it.next().value, firstKV);
|
||||||
assert.equal(it.next().value, secondKV);
|
assert.equal(it.next().value, secondKV);
|
||||||
assert.equal(it.next().value, thirdKV);
|
assert.equal(it.next().value, thirdKV);
|
||||||
assert.equal(it.next().done, true);
|
assert.equal(it.next().done, true);
|
||||||
},
|
},
|
||||||
"prevent key collision during iteration": assert => {
|
"prevent key collision during iteration": (assert): void => {
|
||||||
const first = new ObservableMap();
|
const first = new ObservableMap();
|
||||||
const second = new ObservableMap();
|
const second = new ObservableMap();
|
||||||
const join = new JoinedMap([first, second]);
|
const join = new JoinedMap([first, second]);
|
||||||
@ -228,7 +249,7 @@ export function tests() {
|
|||||||
assert.deepEqual(it.next().value, ["b", 3]);
|
assert.deepEqual(it.next().value, ["b", 3]);
|
||||||
assert.equal(it.next().done, true);
|
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 first = new ObservableMap();
|
||||||
const second = new ObservableMap();
|
const second = new ObservableMap();
|
||||||
const join = new JoinedMap([first, second]);
|
const join = new JoinedMap([first, second]);
|
||||||
@ -240,7 +261,7 @@ export function tests() {
|
|||||||
assert.equal(events[0].key, "a");
|
assert.equal(events[0].key, "a");
|
||||||
assert.equal(events[0].value, 1);
|
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 first = new ObservableMap();
|
||||||
const second = new ObservableMap();
|
const second = new ObservableMap();
|
||||||
const join = new JoinedMap([first, second]);
|
const join = new JoinedMap([first, second]);
|
||||||
@ -250,7 +271,7 @@ export function tests() {
|
|||||||
second.update("a", 3);
|
second.update("a", 3);
|
||||||
assert.equal(events.length, 0);
|
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 first = new ObservableMap();
|
||||||
const second = new ObservableMap();
|
const second = new ObservableMap();
|
||||||
const join = new JoinedMap([first, second]);
|
const join = new JoinedMap([first, second]);
|
||||||
@ -266,7 +287,7 @@ export function tests() {
|
|||||||
assert.equal(events[1].key, "a");
|
assert.equal(events[1].key, "a");
|
||||||
assert.equal(events[1].value, 2);
|
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 first = new ObservableMap();
|
||||||
const second = new ObservableMap();
|
const second = new ObservableMap();
|
||||||
const join = new JoinedMap([first, second]);
|
const join = new JoinedMap([first, second]);
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
86
src/observable/map/LogMap.ts
Normal file
86
src/observable/map/LogMap.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -14,55 +14,76 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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
|
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?
|
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();
|
super();
|
||||||
this._source = source;
|
this._source = source;
|
||||||
this._mapper = mapper;
|
this._mapper = mapper;
|
||||||
this._updater = updater;
|
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);
|
const value = this._mappedValues.get(key);
|
||||||
if (value) {
|
if (value) {
|
||||||
this.emitUpdate(key, value, params);
|
this.emitUpdate(key, value, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(key, value) {
|
onAdd(key: K, value: V): void {
|
||||||
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
|
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
|
||||||
const mappedValue = this._mapper(value, emitSpontaneousUpdate);
|
const mappedValue = this._mapper(value, emitSpontaneousUpdate);
|
||||||
this._mappedValues.set(key, mappedValue);
|
this._mappedValues.set(key, mappedValue);
|
||||||
this.emitAdd(key, mappedValue);
|
this.emitAdd(key, mappedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemove(key/*, _value*/) {
|
onRemove(key: K/*, _value*/): void {
|
||||||
const mappedValue = this._mappedValues.get(key);
|
const mappedValue = this._mappedValues.get(key);
|
||||||
if (this._mappedValues.delete(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 an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
|
||||||
if (!this._mappedValues) {
|
if (!this._mappedValues) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mappedValue = this._mappedValues.get(key);
|
const mappedValue = this._mappedValues.get(key);
|
||||||
if (mappedValue !== undefined) {
|
if (mappedValue !== undefined) {
|
||||||
this._updater?.(mappedValue, params, value);
|
this._updater?.(params, mappedValue, value);
|
||||||
// TODO: map params somehow if needed?
|
// TODO: map params somehow if needed?
|
||||||
this.emitUpdate(key, mappedValue, params);
|
this.emitUpdate(key, mappedValue, params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubscribeFirst() {
|
onSubscribeFirst(): void {
|
||||||
this._subscription = this._source.subscribe(this);
|
this._subscription = this._source.subscribe(this);
|
||||||
for (let [key, value] of this._source) {
|
for (let [key, value] of this._source) {
|
||||||
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
|
const emitSpontaneousUpdate = this._emitSpontaneousUpdate.bind(this, key);
|
||||||
@ -72,26 +93,28 @@ export class MappedMap extends BaseObservableMap {
|
|||||||
super.onSubscribeFirst();
|
super.onSubscribeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast(): void {
|
||||||
super.onUnsubscribeLast();
|
super.onUnsubscribeLast();
|
||||||
this._subscription = this._subscription();
|
if (this._subscription) {
|
||||||
|
this._subscription = this._subscription();
|
||||||
|
}
|
||||||
this._mappedValues.clear();
|
this._mappedValues.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
onReset() {
|
onReset(): void {
|
||||||
this._mappedValues.clear();
|
this._mappedValues.clear();
|
||||||
this.emitReset();
|
this.emitReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Symbol.iterator]() {
|
[Symbol.iterator](): IterableIterator<[K, MappedV]> {
|
||||||
return this._mappedValues.entries();
|
return this._mappedValues.entries();
|
||||||
}
|
}
|
||||||
|
|
||||||
get size() {
|
get size(): number {
|
||||||
return this._mappedValues.size;
|
return this._mappedValues.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key) {
|
get(key: K): MappedV | undefined {
|
||||||
return this._mappedValues.get(key);
|
return this._mappedValues.get(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -14,8 +14,14 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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> {
|
export class ObservableMap<K, V> extends BaseObservableMap<K, V> {
|
||||||
private readonly _values: Map<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
|
// We set the value here because update only supports inline updates
|
||||||
this._values.set(key, value);
|
this._values.set(key, value);
|
||||||
return this.update(key, undefined);
|
return this.update(key, undefined);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return this.add(key, value);
|
return this.add(key, value);
|
||||||
}
|
}
|
||||||
@ -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() {
|
export function tests() {
|
||||||
return {
|
return {
|
||||||
test_initial_values(assert) {
|
test_initial_values(assert): void {
|
||||||
const map = new ObservableMap([
|
const map = new ObservableMap([
|
||||||
["a", 5],
|
["a", 5],
|
||||||
["b", 10]
|
["b", 10]
|
||||||
@ -105,14 +112,14 @@ export function tests() {
|
|||||||
assert.equal(map.get("b"), 10);
|
assert.equal(map.get("b"), 10);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_add(assert) {
|
test_add(assert): void {
|
||||||
let fired = 0;
|
let fired = 0;
|
||||||
const map = new ObservableMap<number, {value: number}>();
|
const map = new ObservableMap<number, {value: number}>();
|
||||||
map.subscribe({
|
map.subscribe({
|
||||||
onAdd(key, value) {
|
onAdd(key, value) {
|
||||||
fired += 1;
|
fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 5});
|
assert.deepEqual(value, {value: 5});
|
||||||
},
|
},
|
||||||
onUpdate() {},
|
onUpdate() {},
|
||||||
onRemove() {},
|
onRemove() {},
|
||||||
@ -123,7 +130,7 @@ export function tests() {
|
|||||||
assert.equal(fired, 1);
|
assert.equal(fired, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_update(assert) {
|
test_update(assert): void {
|
||||||
let fired = 0;
|
let fired = 0;
|
||||||
const map = new ObservableMap<number, {number: number}>();
|
const map = new ObservableMap<number, {number: number}>();
|
||||||
const value = {number: 5};
|
const value = {number: 5};
|
||||||
@ -132,7 +139,7 @@ export function tests() {
|
|||||||
onUpdate(key, value, params) {
|
onUpdate(key, value, params) {
|
||||||
fired += 1;
|
fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {number: 6});
|
assert.deepEqual(value, {number: 6});
|
||||||
assert.equal(params, "test");
|
assert.equal(params, "test");
|
||||||
},
|
},
|
||||||
onAdd() {},
|
onAdd() {},
|
||||||
@ -144,7 +151,7 @@ export function tests() {
|
|||||||
assert.equal(fired, 1);
|
assert.equal(fired, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_update_unknown(assert) {
|
test_update_unknown(assert): void {
|
||||||
let fired = 0;
|
let fired = 0;
|
||||||
const map = new ObservableMap<number, {number: number}>();
|
const map = new ObservableMap<number, {number: number}>();
|
||||||
map.subscribe({
|
map.subscribe({
|
||||||
@ -158,19 +165,19 @@ export function tests() {
|
|||||||
assert.equal(result, false);
|
assert.equal(result, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_set(assert) {
|
test_set(assert): void {
|
||||||
let add_fired = 0, update_fired = 0;
|
let add_fired = 0, update_fired = 0;
|
||||||
const map = new ObservableMap<number, {value: number}>();
|
const map = new ObservableMap<number, {value: number}>();
|
||||||
map.subscribe({
|
map.subscribe({
|
||||||
onAdd(key, value) {
|
onAdd(key, value) {
|
||||||
add_fired += 1;
|
add_fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 5});
|
assert.deepEqual(value, {value: 5});
|
||||||
},
|
},
|
||||||
onUpdate(key, value/*, params*/) {
|
onUpdate(key, value/*, params*/) {
|
||||||
update_fired += 1;
|
update_fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 7});
|
assert.deepEqual(value, {value: 7});
|
||||||
},
|
},
|
||||||
onRemove() {},
|
onRemove() {},
|
||||||
onReset() {}
|
onReset() {}
|
||||||
@ -185,7 +192,7 @@ export function tests() {
|
|||||||
assert.equal(update_fired, 1);
|
assert.equal(update_fired, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_remove(assert) {
|
test_remove(assert): void {
|
||||||
let fired = 0;
|
let fired = 0;
|
||||||
const map = new ObservableMap<number, {value: number}>();
|
const map = new ObservableMap<number, {value: number}>();
|
||||||
const value = {value: 5};
|
const value = {value: 5};
|
||||||
@ -194,7 +201,7 @@ export function tests() {
|
|||||||
onRemove(key, value) {
|
onRemove(key, value) {
|
||||||
fired += 1;
|
fired += 1;
|
||||||
assert.equal(key, 1);
|
assert.equal(key, 1);
|
||||||
assert.deepEqual(value, {value: 5});
|
assert.deepEqual(value, {value: 5});
|
||||||
},
|
},
|
||||||
onAdd() {},
|
onAdd() {},
|
||||||
onUpdate() {},
|
onUpdate() {},
|
||||||
@ -205,7 +212,7 @@ export function tests() {
|
|||||||
assert.equal(fired, 1);
|
assert.equal(fired, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
test_iterate(assert) {
|
test_iterate(assert): void {
|
||||||
const results: any[] = [];
|
const results: any[] = [];
|
||||||
const map = new ObservableMap<number, {number: number}>();
|
const map = new ObservableMap<number, {number: number}>();
|
||||||
map.add(1, {number: 5});
|
map.add(1, {number: 5});
|
||||||
@ -219,11 +226,11 @@ export function tests() {
|
|||||||
assert.equal(results.find(([key]) => key === 2)[1].number, 6);
|
assert.equal(results.find(([key]) => key === 2)[1].number, 6);
|
||||||
assert.equal(results.find(([key]) => key === 3)[1].number, 7);
|
assert.equal(results.find(([key]) => key === 3)[1].number, 7);
|
||||||
},
|
},
|
||||||
test_size(assert) {
|
test_size(assert): void {
|
||||||
const map = new ObservableMap<number, {number: number}>();
|
const map = new ObservableMap<number, {number: number}>();
|
||||||
map.add(1, {number: 5});
|
map.add(1, {number: 5});
|
||||||
map.add(2, {number: 6});
|
map.add(2, {number: 6});
|
||||||
assert.equal(map.size, 2);
|
assert.equal(map.size, 2);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
17
src/observable/map/index.ts
Normal file
17
src/observable/map/index.ts
Normal 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';
|
Loading…
Reference in New Issue
Block a user