2020-08-05 22:08:55 +05:30
|
|
|
/*
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-11-16 14:21:47 +05:30
|
|
|
import {AbortError} from "../utils/error";
|
2021-09-30 05:48:22 +05:30
|
|
|
import {BaseObservable} from "./BaseObservable";
|
2022-01-28 21:05:49 +05:30
|
|
|
import type {SubscriptionHandle} from "./BaseObservable";
|
2020-04-19 22:32:10 +05:30
|
|
|
|
|
|
|
// like an EventEmitter, but doesn't have an event type
|
2021-09-30 06:13:17 +05:30
|
|
|
export abstract class BaseObservableValue<T> extends BaseObservable<(value: T) => void> {
|
|
|
|
emit(argument: T) {
|
2020-04-19 22:32:10 +05:30
|
|
|
for (const h of this._handlers) {
|
|
|
|
h(argument);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
abstract get(): T;
|
2020-10-06 21:34:34 +05:30
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
waitFor(predicate: (value: T) => boolean): IWaitHandle<T> {
|
2020-10-06 21:34:34 +05:30
|
|
|
if (predicate(this.get())) {
|
|
|
|
return new ResolvedWaitForHandle(Promise.resolve(this.get()));
|
|
|
|
} else {
|
|
|
|
return new WaitForHandle(this, predicate);
|
|
|
|
}
|
|
|
|
}
|
2022-01-28 21:05:49 +05:30
|
|
|
|
|
|
|
flatMap<C>(mapper: (value: T) => (BaseObservableValue<C> | undefined)): BaseObservableValue<C | undefined> {
|
|
|
|
return new FlatMapObservableValue<T, C>(this, mapper);
|
|
|
|
}
|
2020-04-19 22:32:10 +05:30
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
interface IWaitHandle<T> {
|
|
|
|
promise: Promise<T>;
|
|
|
|
dispose(): void;
|
|
|
|
}
|
|
|
|
|
|
|
|
class WaitForHandle<T> implements IWaitHandle<T> {
|
|
|
|
private _promise: Promise<T>
|
|
|
|
private _reject: ((reason?: any) => void) | null;
|
|
|
|
private _subscription: (() => void) | null;
|
|
|
|
|
|
|
|
constructor(observable: BaseObservableValue<T>, predicate: (value: T) => boolean) {
|
2020-04-19 22:32:10 +05:30
|
|
|
this._promise = new Promise((resolve, reject) => {
|
|
|
|
this._reject = reject;
|
|
|
|
this._subscription = observable.subscribe(v => {
|
|
|
|
if (predicate(v)) {
|
|
|
|
this._reject = null;
|
|
|
|
resolve(v);
|
|
|
|
this.dispose();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
get promise(): Promise<T> {
|
2020-04-19 22:32:10 +05:30
|
|
|
return this._promise;
|
|
|
|
}
|
|
|
|
|
|
|
|
dispose() {
|
|
|
|
if (this._subscription) {
|
|
|
|
this._subscription();
|
|
|
|
this._subscription = null;
|
|
|
|
}
|
|
|
|
if (this._reject) {
|
|
|
|
this._reject(new AbortError());
|
|
|
|
this._reject = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
class ResolvedWaitForHandle<T> implements IWaitHandle<T> {
|
|
|
|
constructor(public promise: Promise<T>) {}
|
2020-04-19 22:32:10 +05:30
|
|
|
dispose() {}
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
export class ObservableValue<T> extends BaseObservableValue<T> {
|
|
|
|
private _value: T;
|
|
|
|
|
|
|
|
constructor(initialValue: T) {
|
2020-04-19 22:32:10 +05:30
|
|
|
super();
|
|
|
|
this._value = initialValue;
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
get(): T {
|
2020-04-19 22:32:10 +05:30
|
|
|
return this._value;
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
set(value: T): void {
|
2020-04-19 22:32:10 +05:30
|
|
|
if (value !== this._value) {
|
|
|
|
this._value = value;
|
|
|
|
this.emit(this._value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-30 06:13:17 +05:30
|
|
|
export class RetainedObservableValue<T> extends ObservableValue<T> {
|
|
|
|
private _freeCallback: () => void;
|
|
|
|
|
|
|
|
constructor(initialValue: T, freeCallback: () => void) {
|
2021-05-07 16:36:20 +05:30
|
|
|
super(initialValue);
|
|
|
|
this._freeCallback = freeCallback;
|
|
|
|
}
|
|
|
|
|
|
|
|
onUnsubscribeLast() {
|
|
|
|
super.onUnsubscribeLast();
|
|
|
|
this._freeCallback();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-28 21:05:49 +05:30
|
|
|
export class FlatMapObservableValue<P, C> extends BaseObservableValue<C | undefined> {
|
|
|
|
private sourceSubscription?: SubscriptionHandle;
|
|
|
|
private targetSubscription?: SubscriptionHandle;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
private readonly source: BaseObservableValue<P>,
|
|
|
|
private readonly mapper: (value: P) => (BaseObservableValue<C> | undefined)
|
|
|
|
) {
|
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
|
|
|
onUnsubscribeLast() {
|
|
|
|
super.onUnsubscribeLast();
|
|
|
|
this.sourceSubscription = this.sourceSubscription!();
|
|
|
|
if (this.targetSubscription) {
|
|
|
|
this.targetSubscription = this.targetSubscription();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onSubscribeFirst() {
|
|
|
|
super.onSubscribeFirst();
|
|
|
|
this.sourceSubscription = this.source.subscribe(() => {
|
|
|
|
this.updateTargetSubscription();
|
|
|
|
this.emit(this.get());
|
|
|
|
});
|
|
|
|
this.updateTargetSubscription();
|
|
|
|
}
|
|
|
|
|
|
|
|
private updateTargetSubscription() {
|
|
|
|
const sourceValue = this.source.get();
|
|
|
|
if (sourceValue) {
|
|
|
|
const target = this.mapper(sourceValue);
|
|
|
|
if (target) {
|
|
|
|
if (!this.targetSubscription) {
|
|
|
|
this.targetSubscription = target.subscribe(() => this.emit(this.get()));
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// if no sourceValue or target
|
|
|
|
if (this.targetSubscription) {
|
|
|
|
this.targetSubscription = this.targetSubscription();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get(): C | undefined {
|
|
|
|
const sourceValue = this.source.get();
|
|
|
|
if (!sourceValue) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const mapped = this.mapper(sourceValue);
|
|
|
|
return mapped?.get();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-19 22:32:10 +05:30
|
|
|
export function tests() {
|
|
|
|
return {
|
|
|
|
"set emits an update": assert => {
|
2021-09-30 06:13:17 +05:30
|
|
|
const a = new ObservableValue<number>(0);
|
2020-04-19 22:32:10 +05:30
|
|
|
let fired = false;
|
|
|
|
const subscription = a.subscribe(v => {
|
|
|
|
fired = true;
|
|
|
|
assert.strictEqual(v, 5);
|
|
|
|
});
|
|
|
|
a.set(5);
|
|
|
|
assert(fired);
|
|
|
|
subscription();
|
|
|
|
},
|
|
|
|
"set doesn't emit if value hasn't changed": assert => {
|
|
|
|
const a = new ObservableValue(5);
|
|
|
|
let fired = false;
|
|
|
|
const subscription = a.subscribe(() => {
|
|
|
|
fired = true;
|
|
|
|
});
|
|
|
|
a.set(5);
|
|
|
|
a.set(5);
|
|
|
|
assert(!fired);
|
|
|
|
subscription();
|
|
|
|
},
|
|
|
|
"waitFor promise resolves on matching update": async assert => {
|
|
|
|
const a = new ObservableValue(5);
|
|
|
|
const handle = a.waitFor(v => v === 6);
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
a.set(6);
|
|
|
|
});
|
|
|
|
await handle.promise;
|
|
|
|
assert.strictEqual(a.get(), 6);
|
|
|
|
},
|
|
|
|
"waitFor promise rejects when disposed": async assert => {
|
2021-09-30 06:13:17 +05:30
|
|
|
const a = new ObservableValue<number>(0);
|
2020-04-19 22:32:10 +05:30
|
|
|
const handle = a.waitFor(() => false);
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
handle.dispose();
|
|
|
|
});
|
|
|
|
await assert.rejects(handle.promise, AbortError);
|
|
|
|
},
|
2022-01-28 21:05:49 +05:30
|
|
|
"flatMap.get": assert => {
|
|
|
|
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
2022-01-28 21:10:32 +05:30
|
|
|
const countProxy = a.flatMap(a => a!.count);
|
2022-01-28 21:05:49 +05:30
|
|
|
assert.strictEqual(countProxy.get(), undefined);
|
|
|
|
const count = new ObservableValue<number>(0);
|
|
|
|
a.set({count});
|
|
|
|
assert.strictEqual(countProxy.get(), 0);
|
|
|
|
},
|
|
|
|
"flatMap update from source": assert => {
|
|
|
|
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
|
|
|
const updates: (number | undefined)[] = [];
|
2022-01-28 21:10:32 +05:30
|
|
|
a.flatMap(a => a!.count).subscribe(count => {
|
2022-01-28 21:05:49 +05:30
|
|
|
updates.push(count);
|
|
|
|
});
|
|
|
|
const count = new ObservableValue<number>(0);
|
|
|
|
a.set({count});
|
|
|
|
assert.deepEqual(updates, [0]);
|
|
|
|
},
|
|
|
|
"flatMap update from target": assert => {
|
|
|
|
const a = new ObservableValue<undefined | {count: ObservableValue<number>}>(undefined);
|
|
|
|
const updates: (number | undefined)[] = [];
|
2022-01-28 21:10:32 +05:30
|
|
|
a.flatMap(a => a!.count).subscribe(count => {
|
2022-01-28 21:05:49 +05:30
|
|
|
updates.push(count);
|
|
|
|
});
|
|
|
|
const count = new ObservableValue<number>(0);
|
|
|
|
a.set({count});
|
|
|
|
count.set(5);
|
|
|
|
assert.deepEqual(updates, [0, 5]);
|
|
|
|
}
|
2020-04-19 22:32:10 +05:30
|
|
|
}
|
|
|
|
}
|