@guanghechen/viewmodel
Version:
387 lines (378 loc) • 12.9 kB
JavaScript
import { Disposable, BatchDisposable, SafeBatchHandler, isDisposable } from '@guanghechen/disposable';
export * from '@guanghechen/disposable';
import { Ticker, Observable, noopUnsubscribable as noopUnsubscribable$1, isObservable } from '@guanghechen/observable';
export * from '@guanghechen/observable';
import { Subscriber } from '@guanghechen/subscriber';
export * from '@guanghechen/subscriber';
class Computed {
_observable;
constructor(observable) {
this._observable = observable;
}
static fromObservables(observables, transform, options) {
const ticker = new Ticker();
for (const source of observables)
ticker.observe(source);
const getSnapshot = () => {
const values = observables.map(source => source.getSnapshot());
return transform(values);
};
const observable = new Observable(getSnapshot(), options);
observable.registerDisposable(ticker);
const subscriber = new Subscriber({
onNext: () => observable.next(getSnapshot()),
});
ticker.subscribe(subscriber);
return new Computed(observable);
}
get disposed() {
return this._observable.disposed;
}
dispose() {
if (!this._observable.disposed) {
this._observable.dispose();
}
}
registerDisposable(disposable) {
this._observable.registerDisposable(disposable);
}
subscribe(subscriber) {
return this._observable.subscribe(subscriber);
}
getSnapshot = () => {
return this._observable.getSnapshot();
};
getServerSnapshot = () => {
return this._observable.getSnapshot();
};
subscribeStateChange = (onStateChange) => {
const subscriber = new Subscriber({ onNext: () => onStateChange() });
const unsubscribable = this._observable.subscribe(subscriber);
const disposable = new Disposable(() => {
subscriber.dispose();
unsubscribable.unsubscribe();
});
this._observable.registerDisposable(disposable);
return () => disposable.dispose();
};
}
const defaultEquals = (x, y) => Object.is(x, y);
class DisposedObservable {
equals;
_value;
constructor(defaultValue, options) {
this._value = defaultValue;
this.equals = options?.equals ?? defaultEquals;
}
get disposed() {
return true;
}
dispose() { }
registerDisposable(disposable) {
disposable.dispose();
}
getSnapshot() {
return this._value;
}
next(value, options) {
const strict = options?.strict ?? true;
if (strict) {
throw new RangeError(`Don't update a disposed observable. value: ${String(value)}.`);
}
}
subscribe(subscriber) {
subscriber.dispose();
return noopUnsubscribable$1;
}
}
const noop = (..._args) => { };
const noopUnsubscribable = { unsubscribe: noop };
class ObservableCollection extends BatchDisposable {
equals;
valueEquals;
_subscribers;
_keySubscribersMap;
_value;
_lastNotifiedValue;
constructor(defaultValue, options = {}) {
super();
this._value = defaultValue;
this._subscribers = [];
this._keySubscribersMap = new Map();
this.valueEquals =
options.valueEquals ?? ((x, y) => Object.is(x, y));
this.equals = (x, y) => Object.is(x, y);
}
dispose() {
if (this.disposed)
return;
super.dispose();
const batcher = new SafeBatchHandler();
{
const subscribers = this._subscribers;
const size = subscribers.length;
for (let i = 0; i < size; ++i) {
const item = subscribers[i];
if (item.inactive || item.subscriber.disposed)
continue;
batcher.run(() => item.subscriber.dispose());
}
this._subscribers.length = 0;
}
{
for (const subscribers of this._keySubscribersMap.values()) {
const size = subscribers.length;
for (let i = 0; i < size; ++i) {
const item = subscribers[i];
if (item.inactive || item.subscriber.disposed)
continue;
batcher.run(() => item.subscriber.dispose());
}
}
this._keySubscribersMap.clear();
}
batcher.summary('[observable-collection] Encountered errors while disposing.');
batcher.cleanup();
}
has(key) {
return this._value.has(key);
}
get(key) {
return this._value.get(key);
}
keys() {
return this._value.keys();
}
values() {
return this._value.values();
}
entries() {
return this._value.entries();
}
getSnapshot() {
return this._value;
}
next(value, options) {
if (this.disposed) {
const strict = options?.strict ?? true;
if (strict) {
throw new RangeError(`Don't update a disposed observable. value: ${String(value)}.`);
}
}
const force = options?.force ?? false;
const prevValue = this._value;
if (!force && this.equals(value, prevValue))
return;
this._value = value;
this._notify();
}
subscribe(subscriber) {
if (subscriber.disposed)
return noopUnsubscribable;
if (this.disposed) {
subscriber.dispose();
return noopUnsubscribable;
}
const prevValue = this._lastNotifiedValue;
const value = this._value;
const item = { subscriber, inactive: false };
this._subscribers.push(item);
subscriber.next(value, prevValue);
return {
unsubscribe: () => {
item.inactive = true;
},
};
}
observeKey(key) {
const value = this._value.get(key);
if (this.disposed) {
return new DisposedObservable(value, { equals: this.valueEquals });
}
const observable = new Observable(value, { equals: this.valueEquals });
const subscriber = new Subscriber({
onNext: v => observable.next(v),
});
const unsubscribable = this.subscribeKey(key, subscriber);
const disposable = new Disposable(() => {
observable.dispose();
subscriber.dispose();
unsubscribable.unsubscribe();
});
this.registerDisposable(disposable);
observable.registerDisposable(disposable);
return observable;
}
subscribeKey(key, subscriber) {
if (subscriber.disposed)
return noopUnsubscribable;
if (this.disposed) {
subscriber.dispose();
return noopUnsubscribable;
}
const prevValue = this._lastNotifiedValue === undefined ? undefined : this._lastNotifiedValue.get(key);
const value = this._value.get(key);
const item = { subscriber, inactive: false };
const keySubscribers = this._keySubscribersMap.get(key);
if (keySubscribers === undefined)
this._keySubscribersMap.set(key, [item]);
else
keySubscribers.push(item);
subscriber.next(value, prevValue);
return {
unsubscribe: () => {
item.inactive = true;
},
};
}
_notify() {
const value = this._value;
const prevValue = this._lastNotifiedValue;
this._lastNotifiedValue = value;
const batcher = new SafeBatchHandler();
for (const [key, subscribers] of this._keySubscribersMap) {
const val = value.get(key);
const prevVal = prevValue === undefined ? undefined : prevValue.get(key);
const size = subscribers.length;
for (let i = 0; i < size; ++i) {
const subscriber = subscribers[i];
if (subscriber.inactive || subscriber.subscriber.disposed)
continue;
if (this.valueEquals(val, prevVal))
continue;
batcher.run(() => subscriber.subscriber.next(val, prevVal));
}
}
{
const subscribers = this._subscribers;
const size = subscribers.length;
for (let i = 0; i < size; ++i) {
const subscriber = subscribers[i];
if (subscriber.inactive || subscriber.subscriber.disposed)
continue;
batcher.run(() => subscriber.subscriber.next(value, prevValue));
}
}
batcher.summary('[observable-collection] Encountered errors while notifying subscribers.');
batcher.cleanup();
}
}
class ObservableMap extends ObservableCollection {
set(key, value, options) {
const nextValue = this._value.set(key, value);
this.next(nextValue, options);
}
delete(key, options) {
const nextValue = this._value.delete(key);
this.next(nextValue, options);
}
deleteAll(keys, options) {
const nextValue = this._value.withMutations(mutable => {
for (const key of keys)
mutable.delete(key);
});
this.next(nextValue, options);
}
merge(entries, options) {
const nextValue = this._value.withMutations(mutable => {
for (const [key, val] of entries)
mutable.set(key, val);
});
this.next(nextValue, options);
}
}
class ObservableSet extends ObservableCollection {
add(value, options) {
const nextValue = this._value.add(value);
this.next(nextValue, options);
}
addAll(values, options) {
const nextValue = this._value.withMutations(mutable => {
for (const value of values)
mutable.add(value);
});
this.next(nextValue, options);
}
delete(value, options) {
const nextValue = this._value.delete(value);
this.next(nextValue, options);
}
deleteAll(values, options) {
const nextValue = this._value.withMutations(mutable => {
for (const value of values)
mutable.delete(value);
});
this.next(nextValue, options);
}
}
class State extends Observable {
getSnapshot = () => {
return super.getSnapshot();
};
getServerSnapshot = () => {
return super.getSnapshot();
};
setState = (patch) => {
const prevValue = this.getSnapshot();
const nextValue = patch(prevValue);
super.next(nextValue);
};
updateState = (patch) => {
const prevValue = this.getSnapshot();
const nextValue = typeof patch === 'function' ? patch(prevValue) : patch;
super.next(nextValue);
};
subscribeStateChange = (onStateChange) => {
const subscriber = new Subscriber({ onNext: () => onStateChange() });
const unsubscribable = super.subscribe(subscriber);
const disposable = new Disposable(() => {
subscriber.dispose();
unsubscribable.unsubscribe();
});
this.registerDisposable(disposable);
return () => disposable.dispose();
};
}
class ViewModel extends BatchDisposable {
_tickerMap;
constructor() {
super();
this._tickerMap = new Map();
}
dispose() {
if (!this.disposed) {
super.dispose();
for (const key of Reflect.ownKeys(this)) {
if (typeof key === 'string' && key.endsWith('$')) {
const disposable = this[key];
if (isDisposable(disposable))
disposable.dispose();
}
}
for (const ticker of this._tickerMap.values())
ticker.ticker.dispose();
this._tickerMap.clear();
}
}
ticker(observableKeys) {
const keys = Array.from(new Set(observableKeys)).sort();
const key = keys.join('|');
let item = this._tickerMap.get(key);
if (item === undefined) {
const ticker = new Ticker();
item = { keys: keys, ticker };
this.registerDisposable(ticker);
this._tickerMap.set(key, item);
for (const obKey of keys) {
const observable = this[obKey];
if (!isObservable(observable)) {
console.warn('[ViewModel.ticker] not an observable, key:', obKey, 'val:', observable);
continue;
}
ticker.observe(observable);
}
}
return item;
}
}
export { Computed, DisposedObservable, ObservableCollection, ObservableMap, ObservableSet, State, ViewModel };