@guanghechen/viewmodel
Version:
410 lines (400 loc) • 13.9 kB
JavaScript
;
var disposable = require('@guanghechen/disposable');
var observable = require('@guanghechen/observable');
var subscriber = require('@guanghechen/subscriber');
class Computed {
_observable;
constructor(observable) {
this._observable = observable;
}
static fromObservables(observables, transform, options) {
const ticker = new observable.Ticker();
for (const source of observables)
ticker.observe(source);
const getSnapshot = () => {
const values = observables.map(source => source.getSnapshot());
return transform(values);
};
const observable$1 = new observable.Observable(getSnapshot(), options);
observable$1.registerDisposable(ticker);
const subscriber$1 = new subscriber.Subscriber({
onNext: () => observable$1.next(getSnapshot()),
});
ticker.subscribe(subscriber$1);
return new Computed(observable$1);
}
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$1 = new subscriber.Subscriber({ onNext: () => onStateChange() });
const unsubscribable = this._observable.subscribe(subscriber$1);
const disposable$1 = new disposable.Disposable(() => {
subscriber$1.dispose();
unsubscribable.unsubscribe();
});
this._observable.registerDisposable(disposable$1);
return () => disposable$1.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 observable.noopUnsubscribable;
}
}
const noop = (..._args) => { };
const noopUnsubscribable = { unsubscribe: noop };
class ObservableCollection extends disposable.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 disposable.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$1 = new observable.Observable(value, { equals: this.valueEquals });
const subscriber$1 = new subscriber.Subscriber({
onNext: v => observable$1.next(v),
});
const unsubscribable = this.subscribeKey(key, subscriber$1);
const disposable$1 = new disposable.Disposable(() => {
observable$1.dispose();
subscriber$1.dispose();
unsubscribable.unsubscribe();
});
this.registerDisposable(disposable$1);
observable$1.registerDisposable(disposable$1);
return observable$1;
}
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 disposable.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.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$1 = new subscriber.Subscriber({ onNext: () => onStateChange() });
const unsubscribable = super.subscribe(subscriber$1);
const disposable$1 = new disposable.Disposable(() => {
subscriber$1.dispose();
unsubscribable.unsubscribe();
});
this.registerDisposable(disposable$1);
return () => disposable$1.dispose();
};
}
class ViewModel extends disposable.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$1 = this[key];
if (disposable.isDisposable(disposable$1))
disposable$1.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 observable.Ticker();
item = { keys: keys, ticker };
this.registerDisposable(ticker);
this._tickerMap.set(key, item);
for (const obKey of keys) {
const observable$1 = this[obKey];
if (!observable.isObservable(observable$1)) {
console.warn('[ViewModel.ticker] not an observable, key:', obKey, 'val:', observable$1);
continue;
}
ticker.observe(observable$1);
}
}
return item;
}
}
exports.Computed = Computed;
exports.DisposedObservable = DisposedObservable;
exports.ObservableCollection = ObservableCollection;
exports.ObservableMap = ObservableMap;
exports.ObservableSet = ObservableSet;
exports.State = State;
exports.ViewModel = ViewModel;
Object.keys(disposable).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return disposable[k]; }
});
});
Object.keys(observable).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return observable[k]; }
});
});
Object.keys(subscriber).forEach(function (k) {
if (k !== 'default' && !Object.prototype.hasOwnProperty.call(exports, k)) Object.defineProperty(exports, k, {
enumerable: true,
get: function () { return subscriber[k]; }
});
});