src/vs/base/common/observableInternal/observables/derivedImpl.ts

459 LOC · 409 covered · 50 uncovered · 83 ranges · 6771 concepts · 24 introducers · 3467 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- devToolsLogger.ts ×24
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { IObservable, IObservableWithChange, IObserver, IReaderWithStore, ISettableObservable, ITransaction, } from '../base.js';
7 > import { BaseObservable } from './baseObservable.js';
8 > import { DebugNameData } from '../debugName.js';
9 > import { BugIndicatingError, DisposableStore, EqualityComparer, assertFn, onBugIndicatingError } from '../commonFacade/deps.js';
10 > import { getLogger } from '../logging/logging.js';
11 > import { IChangeTracker } from '../changeTracker.js';
12 > import { DebugLocation } from '../debugLocation.js';
13 >
14 > export interface IDerivedReader<TChange = void> extends IReaderWithStore {
15 > /**
16 > * Call this to report a change delta or to force report a change, even if the new value is the same as the old value.
17 > */
18 > reportChange(change: TChange): void;
19 > }
20 >
21 > export const enum DerivedState {
22 > /** Initial state, no previous value, recomputation needed */
23 > initial = 0,
24 >
25 > /**
26 > * A dependency could have changed.
27 > * We need to explicitly ask them if at least one dependency changed.
28 > */
29 > dependenciesMightHaveChanged = 1,
30 >
31 > /**
32 > * A dependency changed and we need to recompute.
33 > * After recomputation, we need to check the previous value to see if we changed as well.
34 > */
35 > stale = 2,
36 >
37 > /**
38 > * No change reported, our cached value is up to date.
39 > */
40 > upToDate = 3,
41 > }
42 >
43 > function derivedStateToString(state: DerivedState): string { consoleObservableLogger.ts ×8
44 > switch (state) {
45 > case DerivedState.initial: return 'initial';
46 > case DerivedState.dependenciesMightHaveChanged: return 'dependenciesMightHaveChanged';
47 > case DerivedState.stale: return 'stale';
48 > case DerivedState.upToDate: return 'upToDate';
49 > default: return '<unknown>';
50 > }
51 > }
53 > export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObservable<T, TChange> implements IDerivedReader<TChange>, IObserver {
54 > private _state = DerivedState.initial;
55 > private _value: T | undefined = undefined;
56 > private _updateCount = 0;
57 > private _dependencies = new Set<IObservable<any>>();
58 > private _dependenciesToBeRemoved = new Set<IObservable<any>>();
59 > private _changeSummary: TChangeSummary | undefined = undefined;
60 > private _isUpdating = false;
61 > private _isComputing = false;
62 > private _didReportChange = false;
63 > private _isInBeforeUpdate = false;
64 > private _isReaderValid = false;
65 > private _store: DisposableStore | undefined = undefined;
66 > private _delayedStore: DisposableStore | undefined = undefined;
67 > private _removedObserverToCallEndUpdateOn: Set<IObserver> | null = null;
68 >
69 > public override get debugName(): string {
70 > return this._debugNameData.getDebugName(this) ?? '(anonymous)';
71 > }
72 >
73 > constructor(
74 > public readonly _debugNameData: DebugNameData, derivedImpl.ts ×1
75 > public readonly _computeFn: (reader: IDerivedReader<TChange>, changeSummary: TChangeSummary) => T,
76 > private readonly _changeTracker: IChangeTracker<TChangeSummary> | undefined,
77 > private readonly _handleLastObserverRemoved: (() => void) | undefined = undefined,
78 > private readonly _equalityComparator: EqualityComparer<T>,
79 > debugLocation: DebugLocation,
80 > ) {
81 > super(debugLocation);
82 > this._changeSummary = this._changeTracker?.createChangeSummary(undefined);
83 > }
85 > protected override onLastObserverRemoved(): void {
87 > * We are not tracking changes anymore, thus we have to assume
88 > * that our cache is invalid.
89 > */
90 > this._state = DerivedState.initial;
91 > this._value = undefined;
92 > getLogger()?.handleDerivedCleared(this);
93 > for (const d of this._dependencies) {
94 > d.removeObserver(this); derivedImpl.ts ×1
95 > }
96 > this._dependencies.clear(); derivedImpl.ts ×4
97 >
98 > if (this._store !== undefined) {
99 > this._store.dispose(); derivedImpl.ts ×2
100 > this._store = undefined;
101 > }
102 > if (this._delayedStore !== undefined) { derivedImpl.ts ×4
103 > this._delayedStore.dispose(); derivedImpl.ts ×4
104 > this._delayedStore = undefined;
105 > }
107 > this._handleLastObserverRemoved?.();
108 > }
110 > public override get(): T {
111 > const checkEnabled = false; // TODO set to true derivedImpl.ts ×4
112 > if (this._isComputing && checkEnabled) {
113 // investigate why this fails in the diff editor!
114 throw new BugIndicatingError('Cyclic deriveds are not supported yet!');
115 }
117 > if (this._observers.size === 0) {
118 > let result; derivedImpl.ts ×2
119 > // Without observers, we don't know when to clean up stuff.
120 > // Thus, we don't cache anything to prevent memory leaks.
121 > try {
122 > this._isReaderValid = true;
123 > let changeSummary = undefined;
124 > if (this._changeTracker) {
125 changeSummary = this._changeTracker.createChangeSummary(undefined);
126 this._changeTracker.beforeUpdate?.(this, changeSummary);
127 }
128 > result = this._computeFn(this, changeSummary!); derivedImpl.ts ×2
129 > } finally {
130 > this._isReaderValid = false;
131 > }
132 > // Clear new dependencies
133 > this.onLastObserverRemoved();
134 > return result;
135 >
136 > } else { derivedImpl.ts ×4
137 > do { derivedImpl.ts ×13
138 > // We might not get a notification for a dependency that changed while it is updating,
139 > // thus we also have to ask all our depedencies if they changed in this case.
140 > if (this._state === DerivedState.dependenciesMightHaveChanged) {
141 > for (const d of this._dependencies) { derivedImpl.ts ×2
142 > /** might call {@link handleChange} indirectly, which could make us stale */
143 > d.reportChanges();
144 >
145 > if (this._state as DerivedState === DerivedState.stale) {
146 > // The other dependencies will refresh on demand, so early break derivedImpl.ts ×1
147 > break;
148 > }
150 > }
152 > // We called report changes of all dependencies.
153 > // If we are still not stale, we can assume to be up to date again.
154 > if (this._state === DerivedState.dependenciesMightHaveChanged) {
155 > this._state = DerivedState.upToDate; derivedImpl.ts ×1
156 > }
158 > if (this._state !== DerivedState.upToDate) {
159 > this._recompute();
160 > }
161 > // In case recomputation changed one of our dependencies, we need to recompute again.
162 > } while (this._state !== DerivedState.upToDate);
163 > return this._value!;
164 > }
167 > private _recompute() {
168 > let didChange = false; derivedImpl.ts ×13
169 > this._isComputing = true;
170 > this._didReportChange = false;
171 >
172 > const emptySet = this._dependenciesToBeRemoved;
173 > this._dependenciesToBeRemoved = this._dependencies;
174 > this._dependencies = emptySet;
175 >
176 > try {
177 > const changeSummary = this._changeSummary!;
178 >
179 > this._isReaderValid = true;
180 > if (this._changeTracker) {
181 > this._isInBeforeUpdate = true; derivedImpl.ts ×3
182 > this._changeTracker.beforeUpdate?.(this, changeSummary);
183 > this._isInBeforeUpdate = false;
184 > this._changeSummary = this._changeTracker?.createChangeSummary(changeSummary);
185 > }
187 > const hadValue = this._state !== DerivedState.initial;
188 > const oldValue = this._value;
189 > this._state = DerivedState.upToDate;
190 >
191 > const delayedStore = this._delayedStore;
192 > if (delayedStore !== undefined) {
193 > this._delayedStore = undefined; derivedImpl.ts ×4
194 > }
195 > try { derivedImpl.ts ×13
196 > if (this._store !== undefined) {
197 > this._store.dispose(); derivedImpl.ts ×1
198 > this._store = undefined;
199 > }
200 > /** might call {@link handleChange} indirectly, which could invalidate us */ derivedImpl.ts ×13
201 > this._value = this._computeFn(this, changeSummary);
202 >
203 > } finally {
204 > this._isReaderValid = false;
205 > // We don't want our observed observables to think that they are (not even temporarily) not being observed.
206 > // Thus, we only unsubscribe from observables that are definitely not read anymore.
207 > for (const o of this._dependenciesToBeRemoved) {
208 > o.removeObserver(this); derivedImpl.ts ×1
209 > }
210 > this._dependenciesToBeRemoved.clear(); derivedImpl.ts ×13
211 >
212 > if (delayedStore !== undefined) {
213 > delayedStore.dispose(); derivedImpl.ts ×4
214 > }
216 >
217 > didChange = this._didReportChange || (hadValue && !(this._equalityComparator(oldValue!, this._value)));
218 >
219 > getLogger()?.handleObservableUpdated(this, {
220 > oldValue,
221 > newValue: this._value,
222 > change: undefined,
223 > didChange,
224 > hadValue,
225 > });
226 > } catch (e) {
227 onBugIndicatingError(e);
228 }
230 > this._isComputing = false;
231 >
232 > if (!this._didReportChange && didChange) {
233 > for (const r of this._observers) { derivedImpl.ts ×1
234 > r.handleChange(this, undefined);
235 > }
236 > } else { derivedImpl.ts ×13
237 > this._didReportChange = false;
238 > }
239 > }
241 > public override toString(): string {
242 return `LazyDerived<${this.debugName}>`;
243 }
245 > // IObserver Implementation
246 >
247 > public beginUpdate<T>(_observable: IObservable<T>): void {
248 > if (this._isUpdating) { derivedImpl.ts ×9
249 throw new BugIndicatingError('Cyclic deriveds are not supported yet!');
250 }
252 > this._updateCount++;
253 > this._isUpdating = true;
254 > try {
255 > const propagateBeginUpdate = this._updateCount === 1;
256 > if (this._state === DerivedState.upToDate) {
257 > this._state = DerivedState.dependenciesMightHaveChanged;
258 > // If we propagate begin update, that will already signal a possible change.
259 > if (!propagateBeginUpdate) {
260 > for (const r of this._observers) { derivedImpl.ts ×1
261 > r.handlePossibleChange(this);
262 > }
263 > }
265 > if (propagateBeginUpdate) {
266 > for (const r of this._observers) {
267 > r.beginUpdate(this); // This signals a possible change
268 > }
269 > }
270 > } finally {
271 > this._isUpdating = false;
272 > }
273 > }
275 > public endUpdate<T>(_observable: IObservable<T>): void {
276 > this._updateCount--; derivedImpl.ts ×9
277 > if (this._updateCount === 0) {
278 > // End update could change the observer list.
279 > const observers = [...this._observers];
280 > for (const r of observers) {
281 > r.endUpdate(this);
282 > }
283 > if (this._removedObserverToCallEndUpdateOn) {
284 const observers = [...this._removedObserverToCallEndUpdateOn];
285 this._removedObserverToCallEndUpdateOn = null;
286 for (const r of observers) {
287 r.endUpdate(this);
288 }
289 }
291 > assertFn(() => this._updateCount >= 0);
292 > }
294 > public handlePossibleChange<T>(observable: IObservable<T>): void {
295 > // In all other states, observers already know that we might have changed. derivedImpl.ts ×1
296 > if (this._state === DerivedState.upToDate && this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable)) {
297 > this._state = DerivedState.dependenciesMightHaveChanged;
298 > for (const r of this._observers) {
299 > r.handlePossibleChange(this);
300 > }
301 > }
302 > }
304 > public handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
305 > if (this._dependencies.has(observable) && !this._dependenciesToBeRemoved.has(observable) || this._isInBeforeUpdate) { derivedImpl.ts ×9
306 > getLogger()?.handleDerivedDependencyChanged(this, observable, change);
307 >
308 > let shouldReact = false;
309 > try {
310 > shouldReact = this._changeTracker ? this._changeTracker.handleChange({
311 > changedObservable: observable, derivedImpl.ts ×3
312 > change,
313 > // eslint-disable-next-line local/code-no-any-casts
314 > didChange: (o): this is any => o === observable as any,
315 > }, this._changeSummary!) : true; derivedImpl.ts ×9
316 > } catch (e) {
317 onBugIndicatingError(e);
318 }
320 > const wasUpToDate = this._state === DerivedState.upToDate;
321 > if (shouldReact && (this._state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) {
322 > this._state = DerivedState.stale;
323 > if (wasUpToDate) {
324 for (const r of this._observers) {
325 r.handlePossibleChange(this);
326 }
327 }
329 > }
330 > }
332 > // IReader Implementation
333 >
334 > private _ensureReaderValid(): void {
335 > if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); } derivedImpl.ts ×2
336 > }
338 > public readObservable<T>(observable: IObservable<T>): T {
339 > this._ensureReaderValid(); derivedImpl.ts ×2
340 >
341 > // Subscribe before getting the value to enable caching
342 > observable.addObserver(this);
343 > /** This might call {@link handleChange} indirectly, which could invalidate us */
344 > const value = observable.get();
345 > // Which is why we only add the observable to the dependencies now.
346 > this._dependencies.add(observable);
347 > this._dependenciesToBeRemoved.delete(observable);
348 > return value;
349 > }
351 > public reportChange(change: TChange): void {
352 > this._ensureReaderValid(); derivedImpl.ts ×3
353 >
354 > this._didReportChange = true;
355 > // TODO add logging
356 > for (const r of this._observers) {
357 > r.handleChange(this, change);
358 > }
359 > }
361 > get store(): DisposableStore {
362 > this._ensureReaderValid(); derivedImpl.ts ×2
363 >
364 > if (this._store === undefined) {
365 > this._store = new DisposableStore();
366 > }
367 > return this._store;
368 > }
370 > get delayedStore(): DisposableStore {
371 > this._ensureReaderValid(); derivedImpl.ts ×4
372 >
373 > if (this._delayedStore === undefined) {
374 > this._delayedStore = new DisposableStore();
375 > }
376 > return this._delayedStore;
377 > }
379 > public override addObserver(observer: IObserver): void {
380 > const shouldCallBeginUpdate = !this._observers.has(observer) && this._updateCount > 0; derivedImpl.ts ×13
381 > super.addObserver(observer);
382 >
383 > if (shouldCallBeginUpdate) {
384 > if (!this._removedObserverToCallEndUpdateOn?.delete(observer)) { derivedImpl.ts ×1
385 > observer.beginUpdate(this);
386 > }
387 > }
390 > public override removeObserver(observer: IObserver): void {
391 > if (this._observers.has(observer) && this._updateCount > 0) { derivedImpl.ts ×2
392 if (!this._removedObserverToCallEndUpdateOn) {
393 this._removedObserverToCallEndUpdateOn = new Set();
394 }
395 this._removedObserverToCallEndUpdateOn.add(observer);
396 }
397 > super.removeObserver(observer); derivedImpl.ts ×2
398 > }
400 > public debugGetState() {
402 > state: this._state,
403 > stateStr: derivedStateToString(this._state),
404 > updateCount: this._updateCount,
405 > isComputing: this._isComputing,
406 > dependencies: this._dependencies,
407 > value: this._value,
408 > };
409 > }
411 > public debugSetValue(newValue: unknown) {
412 // eslint-disable-next-line local/code-no-any-casts
413 this._value = newValue as any;
414 }
416 > public debugRecompute(): void {
417 this.beginUpdate(this);
418 try {
419 if (!this._isComputing) {
420 this._recompute();
421 } else {
422 this._state = DerivedState.stale;
423 }
424 } finally {
425 this.endUpdate(this);
426 }
427 }
429 > public setValue(newValue: T, tx: ITransaction, change: TChange): void {
430 this._value = newValue;
431 const observers = this._observers;
432 tx.updateObserver(this, this);
433 for (const d of observers) {
434 d.handleChange(this, change);
435 }
436 }
438 >
439 >
440 > export class DerivedWithSetter<T, TChangeSummary = any, TOutChanges = any> extends Derived<T, TChangeSummary, TOutChanges> implements ISettableObservable<T, TOutChanges> {
441 > constructor(
442 > debugNameData: DebugNameData, reducer.ts ×3
443 > computeFn: (reader: IDerivedReader<TOutChanges>, changeSummary: TChangeSummary) => T,
444 > changeTracker: IChangeTracker<TChangeSummary> | undefined,
445 > handleLastObserverRemoved: (() => void) | undefined = undefined,
446 > equalityComparator: EqualityComparer<T>,
447 > public readonly set: (value: T, tx: ITransaction | undefined, change: TOutChanges) => void,
448 > debugLocation: DebugLocation,
449 > ) {
450 > super(
451 > debugNameData,
452 > computeFn,
453 > changeTracker,
454 > handleLastObserverRemoved,
455 > equalityComparator,
456 > debugLocation,
457 > );
458 > }