@guanghechen/viewmodel
Version:
343 lines (342 loc) • 10.3 kB
JavaScript
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 };