UNPKG

@sussudio/base

Version:

Internal APIs for VS Code's utilities and user interface building blocks.

143 lines (142 loc) 4.01 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { BaseObservable, _setDerived } from './base.mjs'; import { getLogger } from './logging.mjs'; export function derived(debugName, computeFn) { return new Derived(debugName, computeFn); } _setDerived(derived); export class Derived extends BaseObservable { _debugName; computeFn; hadValue = false; hasValue = false; value = undefined; updateCount = 0; _dependencies = new Set(); get dependencies() { return this._dependencies; } /** * Dependencies that have to be removed when {@link runFn} ran through. */ staleDependencies = new Set(); get debugName() { return typeof this._debugName === 'function' ? this._debugName() : this._debugName; } constructor(_debugName, computeFn) { super(); this._debugName = _debugName; this.computeFn = computeFn; getLogger()?.handleDerivedCreated(this); } onLastObserverRemoved() { /** * We are not tracking changes anymore, thus we have to assume * that our cache is invalid. */ this.hasValue = false; this.hadValue = false; this.value = undefined; for (const d of this._dependencies) { d.removeObserver(this); } this._dependencies.clear(); } get() { if (this.observers.size === 0) { // Cache is not valid and don't refresh the cache. // Observables should not be read in non-reactive contexts. const result = this.computeFn(this); // Clear new dependencies this.onLastObserverRemoved(); return result; } if (this.updateCount > 0 && this.hasValue) { // Refresh dependencies for (const d of this._dependencies) { // Maybe `.get()` triggers `handleChange`? d.get(); if (!this.hasValue) { // The other dependencies will refresh on demand break; } } } if (!this.hasValue) { const emptySet = this.staleDependencies; this.staleDependencies = this._dependencies; this._dependencies = emptySet; const oldValue = this.value; try { this.value = this.computeFn(this); } finally { // We don't want our observed observables to think that they are (not even temporarily) not being observed. // Thus, we only unsubscribe from observables that are definitely not read anymore. for (const o of this.staleDependencies) { o.removeObserver(this); } this.staleDependencies.clear(); } this.hasValue = true; const didChange = this.hadValue && oldValue !== this.value; getLogger()?.handleDerivedRecomputed(this, { oldValue, newValue: this.value, change: undefined, didChange, }); if (didChange) { for (const r of this.observers) { r.handleChange(this, undefined); } } } return this.value; } // IObserver Implementation beginUpdate() { if (this.updateCount === 0) { for (const r of this.observers) { r.beginUpdate(this); } } this.updateCount++; } handleChange(_observable, _change) { if (this.hasValue) { this.hadValue = true; this.hasValue = false; } // Not in transaction: Recompute & inform observers immediately if (this.updateCount === 0 && this.observers.size > 0) { this.get(); } // Otherwise, recompute in `endUpdate` or on demand. } endUpdate() { this.updateCount--; if (this.updateCount === 0) { if (this.observers.size > 0) { // Propagate invalidation this.get(); } for (const r of this.observers) { r.endUpdate(this); } } } // IReader Implementation subscribeTo(observable) { this._dependencies.add(observable); // We are already added as observer for stale dependencies. if (!this.staleDependencies.delete(observable)) { observable.addObserver(this); } } toString() { return `LazyDerived<${this.debugName}>`; } }