548 lines
17 KiB
JavaScript
548 lines
17 KiB
JavaScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js';
|
|
import { BaseObservable, ConvenientObservable, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js';
|
|
import { DebugNameData, getDebugName, } from './debugName.js';
|
|
import { BugIndicatingError, DisposableStore, Event, strictEquals, toDisposable } from './commonFacade/deps.js';
|
|
import { derived, derivedOpts } from './derived.js';
|
|
import { getLogger } from './logging.js';
|
|
/**
|
|
* Represents an efficient observable whose value never changes.
|
|
*/
|
|
export function constObservable(value) {
|
|
return new ConstObservable(value);
|
|
}
|
|
class ConstObservable extends ConvenientObservable {
|
|
value;
|
|
constructor(value) {
|
|
super();
|
|
this.value = value;
|
|
}
|
|
get debugName() {
|
|
return this.toString();
|
|
}
|
|
get() {
|
|
return this.value;
|
|
}
|
|
addObserver(observer) {
|
|
// NO OP
|
|
}
|
|
removeObserver(observer) {
|
|
// NO OP
|
|
}
|
|
toString() {
|
|
return `Const: ${this.value}`;
|
|
}
|
|
}
|
|
export function observableFromPromise(promise) {
|
|
const observable = observableValue('promiseValue', {});
|
|
promise.then((value) => {
|
|
observable.set({ value }, undefined);
|
|
});
|
|
return observable;
|
|
}
|
|
export function observableFromEvent(...args) {
|
|
let owner;
|
|
let event;
|
|
let getValue;
|
|
if (args.length === 3) {
|
|
[owner, event, getValue] = args;
|
|
}
|
|
else {
|
|
[event, getValue] = args;
|
|
}
|
|
return new FromEventObservable(new DebugNameData(owner, undefined, getValue), event, getValue, () => FromEventObservable.globalTransaction, strictEquals);
|
|
}
|
|
export function observableFromEventOpts(options, event, getValue) {
|
|
return new FromEventObservable(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue), event, getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals);
|
|
}
|
|
export class FromEventObservable extends BaseObservable {
|
|
_debugNameData;
|
|
event;
|
|
_getValue;
|
|
_getTransaction;
|
|
_equalityComparator;
|
|
static globalTransaction;
|
|
value;
|
|
hasValue = false;
|
|
subscription;
|
|
constructor(_debugNameData, event, _getValue, _getTransaction, _equalityComparator) {
|
|
super();
|
|
this._debugNameData = _debugNameData;
|
|
this.event = event;
|
|
this._getValue = _getValue;
|
|
this._getTransaction = _getTransaction;
|
|
this._equalityComparator = _equalityComparator;
|
|
}
|
|
getDebugName() {
|
|
return this._debugNameData.getDebugName(this);
|
|
}
|
|
get debugName() {
|
|
const name = this.getDebugName();
|
|
return 'From Event' + (name ? `: ${name}` : '');
|
|
}
|
|
onFirstObserverAdded() {
|
|
this.subscription = this.event(this.handleEvent);
|
|
}
|
|
handleEvent = (args) => {
|
|
const newValue = this._getValue(args);
|
|
const oldValue = this.value;
|
|
const didChange = !this.hasValue || !(this._equalityComparator(oldValue, newValue));
|
|
let didRunTransaction = false;
|
|
if (didChange) {
|
|
this.value = newValue;
|
|
if (this.hasValue) {
|
|
didRunTransaction = true;
|
|
subtransaction(this._getTransaction(), (tx) => {
|
|
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
|
|
for (const o of this.observers) {
|
|
tx.updateObserver(o, this);
|
|
o.handleChange(this, undefined);
|
|
}
|
|
}, () => {
|
|
const name = this.getDebugName();
|
|
return 'Event fired' + (name ? `: ${name}` : '');
|
|
});
|
|
}
|
|
this.hasValue = true;
|
|
}
|
|
if (!didRunTransaction) {
|
|
getLogger()?.handleFromEventObservableTriggered(this, { oldValue, newValue, change: undefined, didChange, hadValue: this.hasValue });
|
|
}
|
|
};
|
|
onLastObserverRemoved() {
|
|
this.subscription.dispose();
|
|
this.subscription = undefined;
|
|
this.hasValue = false;
|
|
this.value = undefined;
|
|
}
|
|
get() {
|
|
if (this.subscription) {
|
|
if (!this.hasValue) {
|
|
this.handleEvent(undefined);
|
|
}
|
|
return this.value;
|
|
}
|
|
else {
|
|
// no cache, as there are no subscribers to keep it updated
|
|
const value = this._getValue(undefined);
|
|
return value;
|
|
}
|
|
}
|
|
}
|
|
(function (observableFromEvent) {
|
|
observableFromEvent.Observer = FromEventObservable;
|
|
function batchEventsGlobally(tx, fn) {
|
|
let didSet = false;
|
|
if (FromEventObservable.globalTransaction === undefined) {
|
|
FromEventObservable.globalTransaction = tx;
|
|
didSet = true;
|
|
}
|
|
try {
|
|
fn();
|
|
}
|
|
finally {
|
|
if (didSet) {
|
|
FromEventObservable.globalTransaction = undefined;
|
|
}
|
|
}
|
|
}
|
|
observableFromEvent.batchEventsGlobally = batchEventsGlobally;
|
|
})(observableFromEvent || (observableFromEvent = {}));
|
|
export function observableSignalFromEvent(debugName, event) {
|
|
return new FromEventObservableSignal(debugName, event);
|
|
}
|
|
class FromEventObservableSignal extends BaseObservable {
|
|
debugName;
|
|
event;
|
|
subscription;
|
|
constructor(debugName, event) {
|
|
super();
|
|
this.debugName = debugName;
|
|
this.event = event;
|
|
}
|
|
onFirstObserverAdded() {
|
|
this.subscription = this.event(this.handleEvent);
|
|
}
|
|
handleEvent = () => {
|
|
transaction((tx) => {
|
|
for (const o of this.observers) {
|
|
tx.updateObserver(o, this);
|
|
o.handleChange(this, undefined);
|
|
}
|
|
}, () => this.debugName);
|
|
};
|
|
onLastObserverRemoved() {
|
|
this.subscription.dispose();
|
|
this.subscription = undefined;
|
|
}
|
|
get() {
|
|
// NO OP
|
|
}
|
|
}
|
|
export function observableSignal(debugNameOrOwner) {
|
|
if (typeof debugNameOrOwner === 'string') {
|
|
return new ObservableSignal(debugNameOrOwner);
|
|
}
|
|
else {
|
|
return new ObservableSignal(undefined, debugNameOrOwner);
|
|
}
|
|
}
|
|
class ObservableSignal extends BaseObservable {
|
|
_debugName;
|
|
_owner;
|
|
get debugName() {
|
|
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
|
|
}
|
|
toString() {
|
|
return this.debugName;
|
|
}
|
|
constructor(_debugName, _owner) {
|
|
super();
|
|
this._debugName = _debugName;
|
|
this._owner = _owner;
|
|
}
|
|
trigger(tx, change) {
|
|
if (!tx) {
|
|
transaction(tx => {
|
|
this.trigger(tx, change);
|
|
}, () => `Trigger signal ${this.debugName}`);
|
|
return;
|
|
}
|
|
for (const o of this.observers) {
|
|
tx.updateObserver(o, this);
|
|
o.handleChange(this, change);
|
|
}
|
|
}
|
|
get() {
|
|
// NO OP
|
|
}
|
|
}
|
|
export function signalFromObservable(owner, observable) {
|
|
return derivedOpts({
|
|
owner,
|
|
equalsFn: () => false,
|
|
}, reader => {
|
|
observable.read(reader);
|
|
});
|
|
}
|
|
/**
|
|
* @deprecated Use `debouncedObservable2` instead.
|
|
*/
|
|
export function debouncedObservable(observable, debounceMs, disposableStore) {
|
|
const debouncedObservable = observableValue('debounced', undefined);
|
|
let timeout = undefined;
|
|
disposableStore.add(autorun(reader => {
|
|
/** @description debounce */
|
|
const value = observable.read(reader);
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
timeout = setTimeout(() => {
|
|
transaction(tx => {
|
|
debouncedObservable.set(value, tx);
|
|
});
|
|
}, debounceMs);
|
|
}));
|
|
return debouncedObservable;
|
|
}
|
|
/**
|
|
* Creates an observable that debounces the input observable.
|
|
*/
|
|
export function debouncedObservable2(observable, debounceMs) {
|
|
let hasValue = false;
|
|
let lastValue;
|
|
let timeout = undefined;
|
|
return observableFromEvent(cb => {
|
|
const d = autorun(reader => {
|
|
const value = observable.read(reader);
|
|
if (!hasValue) {
|
|
hasValue = true;
|
|
lastValue = value;
|
|
}
|
|
else {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
timeout = setTimeout(() => {
|
|
lastValue = value;
|
|
cb();
|
|
}, debounceMs);
|
|
}
|
|
});
|
|
return {
|
|
dispose() {
|
|
d.dispose();
|
|
hasValue = false;
|
|
lastValue = undefined;
|
|
},
|
|
};
|
|
}, () => {
|
|
if (hasValue) {
|
|
return lastValue;
|
|
}
|
|
else {
|
|
return observable.get();
|
|
}
|
|
});
|
|
}
|
|
export function wasEventTriggeredRecently(event, timeoutMs, disposableStore) {
|
|
const observable = observableValue('triggeredRecently', false);
|
|
let timeout = undefined;
|
|
disposableStore.add(event(() => {
|
|
observable.set(true, undefined);
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
timeout = setTimeout(() => {
|
|
observable.set(false, undefined);
|
|
}, timeoutMs);
|
|
}));
|
|
return observable;
|
|
}
|
|
/**
|
|
* This makes sure the observable is being observed and keeps its cache alive.
|
|
*/
|
|
export function keepObserved(observable) {
|
|
const o = new KeepAliveObserver(false, undefined);
|
|
observable.addObserver(o);
|
|
return toDisposable(() => {
|
|
observable.removeObserver(o);
|
|
});
|
|
}
|
|
_setKeepObserved(keepObserved);
|
|
/**
|
|
* This converts the given observable into an autorun.
|
|
*/
|
|
export function recomputeInitiallyAndOnChange(observable, handleValue) {
|
|
const o = new KeepAliveObserver(true, handleValue);
|
|
observable.addObserver(o);
|
|
if (handleValue) {
|
|
handleValue(observable.get());
|
|
}
|
|
else {
|
|
observable.reportChanges();
|
|
}
|
|
return toDisposable(() => {
|
|
observable.removeObserver(o);
|
|
});
|
|
}
|
|
_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
|
|
export class KeepAliveObserver {
|
|
_forceRecompute;
|
|
_handleValue;
|
|
_counter = 0;
|
|
constructor(_forceRecompute, _handleValue) {
|
|
this._forceRecompute = _forceRecompute;
|
|
this._handleValue = _handleValue;
|
|
}
|
|
beginUpdate(observable) {
|
|
this._counter++;
|
|
}
|
|
endUpdate(observable) {
|
|
this._counter--;
|
|
if (this._counter === 0 && this._forceRecompute) {
|
|
if (this._handleValue) {
|
|
this._handleValue(observable.get());
|
|
}
|
|
else {
|
|
observable.reportChanges();
|
|
}
|
|
}
|
|
}
|
|
handlePossibleChange(observable) {
|
|
// NO OP
|
|
}
|
|
handleChange(observable, change) {
|
|
// NO OP
|
|
}
|
|
}
|
|
export function derivedObservableWithCache(owner, computeFn) {
|
|
let lastValue = undefined;
|
|
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
|
|
lastValue = computeFn(reader, lastValue);
|
|
return lastValue;
|
|
});
|
|
return observable;
|
|
}
|
|
export function derivedObservableWithWritableCache(owner, computeFn) {
|
|
let lastValue = undefined;
|
|
const onChange = observableSignal('derivedObservableWithWritableCache');
|
|
const observable = derived(owner, reader => {
|
|
onChange.read(reader);
|
|
lastValue = computeFn(reader, lastValue);
|
|
return lastValue;
|
|
});
|
|
return Object.assign(observable, {
|
|
clearCache: (tx) => {
|
|
lastValue = undefined;
|
|
onChange.trigger(tx);
|
|
},
|
|
setCache: (newValue, tx) => {
|
|
lastValue = newValue;
|
|
onChange.trigger(tx);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* When the items array changes, referential equal items are not mapped again.
|
|
*/
|
|
export function mapObservableArrayCached(owner, items, map, keySelector) {
|
|
let m = new ArrayMap(map, keySelector);
|
|
const self = derivedOpts({
|
|
debugReferenceFn: map,
|
|
owner,
|
|
onLastObserverRemoved: () => {
|
|
m.dispose();
|
|
m = new ArrayMap(map);
|
|
}
|
|
}, (reader) => {
|
|
m.setItems(items.read(reader));
|
|
return m.getItems();
|
|
});
|
|
return self;
|
|
}
|
|
class ArrayMap {
|
|
_map;
|
|
_keySelector;
|
|
_cache = new Map();
|
|
_items = [];
|
|
constructor(_map, _keySelector) {
|
|
this._map = _map;
|
|
this._keySelector = _keySelector;
|
|
}
|
|
dispose() {
|
|
this._cache.forEach(entry => entry.store.dispose());
|
|
this._cache.clear();
|
|
}
|
|
setItems(items) {
|
|
const newItems = [];
|
|
const itemsToRemove = new Set(this._cache.keys());
|
|
for (const item of items) {
|
|
const key = this._keySelector ? this._keySelector(item) : item;
|
|
let entry = this._cache.get(key);
|
|
if (!entry) {
|
|
const store = new DisposableStore();
|
|
const out = this._map(item, store);
|
|
entry = { out, store };
|
|
this._cache.set(key, entry);
|
|
}
|
|
else {
|
|
itemsToRemove.delete(key);
|
|
}
|
|
newItems.push(entry.out);
|
|
}
|
|
for (const item of itemsToRemove) {
|
|
const entry = this._cache.get(item);
|
|
entry.store.dispose();
|
|
this._cache.delete(item);
|
|
}
|
|
this._items = newItems;
|
|
}
|
|
getItems() {
|
|
return this._items;
|
|
}
|
|
}
|
|
export class ValueWithChangeEventFromObservable {
|
|
observable;
|
|
constructor(observable) {
|
|
this.observable = observable;
|
|
}
|
|
get onDidChange() {
|
|
return Event.fromObservableLight(this.observable);
|
|
}
|
|
get value() {
|
|
return this.observable.get();
|
|
}
|
|
}
|
|
export function observableFromValueWithChangeEvent(owner, value) {
|
|
if (value instanceof ValueWithChangeEventFromObservable) {
|
|
return value.observable;
|
|
}
|
|
return observableFromEvent(owner, value.onDidChange, () => value.value);
|
|
}
|
|
/**
|
|
* Creates an observable that has the latest changed value of the given observables.
|
|
* Initially (and when not observed), it has the value of the last observable.
|
|
* When observed and any of the observables change, it has the value of the last changed observable.
|
|
* If multiple observables change in the same transaction, the last observable wins.
|
|
*/
|
|
export function latestChangedValue(owner, observables) {
|
|
if (observables.length === 0) {
|
|
throw new BugIndicatingError();
|
|
}
|
|
let hasLastChangedValue = false;
|
|
let lastChangedValue = undefined;
|
|
const result = observableFromEvent(owner, cb => {
|
|
const store = new DisposableStore();
|
|
for (const o of observables) {
|
|
store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
|
|
hasLastChangedValue = true;
|
|
lastChangedValue = o.read(reader);
|
|
cb();
|
|
}));
|
|
}
|
|
store.add({
|
|
dispose() {
|
|
hasLastChangedValue = false;
|
|
lastChangedValue = undefined;
|
|
},
|
|
});
|
|
return store;
|
|
}, () => {
|
|
if (hasLastChangedValue) {
|
|
return lastChangedValue;
|
|
}
|
|
else {
|
|
return observables[observables.length - 1].get();
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
/**
|
|
* Works like a derived.
|
|
* However, if the value is not undefined, it is cached and will not be recomputed anymore.
|
|
* In that case, the derived will unsubscribe from its dependencies.
|
|
*/
|
|
export function derivedConstOnceDefined(owner, fn) {
|
|
return derivedObservableWithCache(owner, (reader, lastValue) => lastValue ?? fn(reader));
|
|
}
|
|
export function runOnChange(observable, cb) {
|
|
let _previousValue;
|
|
return autorunWithStoreHandleChanges({
|
|
createEmptyChangeSummary: () => ({ deltas: [], didChange: false }),
|
|
handleChange: (context, changeSummary) => {
|
|
if (context.didChange(observable)) {
|
|
const e = context.change;
|
|
if (e !== undefined) {
|
|
changeSummary.deltas.push(e);
|
|
}
|
|
changeSummary.didChange = true;
|
|
}
|
|
return true;
|
|
},
|
|
}, (reader, changeSummary) => {
|
|
const value = observable.read(reader);
|
|
const previousValue = _previousValue;
|
|
if (changeSummary.didChange) {
|
|
_previousValue = value;
|
|
cb(value, previousValue, changeSummary.deltas);
|
|
}
|
|
});
|
|
}
|
|
export function runOnChangeWithStore(observable, cb) {
|
|
const store = new DisposableStore();
|
|
const disposable = runOnChange(observable, (value, previousValue, deltas) => {
|
|
store.clear();
|
|
cb(value, previousValue, deltas, store);
|
|
});
|
|
return {
|
|
dispose() {
|
|
disposable.dispose();
|
|
store.dispose();
|
|
}
|
|
};
|
|
}
|
|
//# sourceMappingURL=utils.js.map
|