UNPKG

@guanghechen/viewmodel

Version:
387 lines (378 loc) 12.9 kB
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 };