/*--------------------------------------------------------------------------------------------- * 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