UNPKG

@guanghechen/viewmodel

Version:
343 lines (342 loc) 10.3 kB
import { BatchDisposable, Disposable, SafeBatchHandler, isDisposable } from "@guanghechen/disposable"; import { Observable, Ticker, isObservable, noopUnsubscribable } from "@guanghechen/observable"; import { Subscriber } from "@guanghechen/subscriber"; var Computed = 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 = () => { return transform(observables.map((source) => source.getSnapshot())); }; 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); var DisposedObservable = class { 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) { if (options?.strict ?? true) throw new RangeError(`Don't update a disposed observable. value: ${String(value)}.`); } subscribe(subscriber) { subscriber.dispose(); return noopUnsubscribable; } }; const noop = (..._args) => {}; const noopUnsubscribable$1 = { unsubscribe: noop }; var ObservableCollection = class extends BatchDisposable { equals; valueEquals; _subscribers; _keySubscribersMap; _value; _lastNotifiedValue; constructor(defaultValue, options = {}) { super(); this._value = defaultValue; this._subscribers = []; this._keySubscribersMap = /* @__PURE__ */ 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) { if (options?.strict ?? true) throw new RangeError(`Don't update a disposed observable. value: ${String(value)}.`); return; } 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$1; if (this.disposed) { subscriber.dispose(); return noopUnsubscribable$1; } 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$1; if (this.disposed) { subscriber.dispose(); return noopUnsubscribable$1; } const prevValue = this._lastNotifiedValue === void 0 ? void 0 : this._lastNotifiedValue.get(key); const value = this._value.get(key); const item = { subscriber, inactive: false }; const keySubscribers = this._keySubscribersMap.get(key); if (keySubscribers === void 0) 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 === void 0 ? void 0 : 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(); } }; var ObservableMap = class 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); } }; var ObservableSet = class 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); } }; var State = class extends Observable { getSnapshot = () => { return super.getSnapshot(); }; getServerSnapshot = () => { return super.getSnapshot(); }; setState = (patch) => { const nextValue = patch(this.getSnapshot()); 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(); }; }; var ViewModel = class extends BatchDisposable { _tickerMap; constructor() { super(); this._tickerMap = /* @__PURE__ */ 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 === void 0) { const ticker = new Ticker(); item = { 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 };