Skip to content

Commit

Permalink
Merge pull request #180750 from microsoft/hediet/b/arrogant-hedgehog
Browse files Browse the repository at this point in the history
Improves observable docs
  • Loading branch information
hediet authored Apr 25, 2023
2 parents 4d9332c + 27c883b commit 6fc6189
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 131 deletions.
108 changes: 49 additions & 59 deletions src/vs/base/common/observableImpl/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,42 +67,56 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
private state = AutorunState.stale;
private updateCount = 0;
private disposed = false;

/**
* The actual dependencies.
*/
private _dependencies = new Set<IObservable<any>>();
public get dependencies() {
return this._dependencies;
}

/**
* Dependencies that have to be removed when {@link runFn} ran through.
*/
private staleDependencies = new Set<IObservable<any>>();
private dependencies = new Set<IObservable<any>>();
private dependenciesToBeRemoved = new Set<IObservable<any>>();

constructor(
public readonly debugName: string,
private readonly runFn: (reader: IReader) => void,
private readonly _handleChange: ((context: IChangeContext) => boolean) | undefined
) {
getLogger()?.handleAutorunCreated(this);
this.runIfNeeded();
this._runIfNeeded();
}

public readObservable<T>(observable: IObservable<T>): T {
// In case the run action disposes the autorun
if (this.disposed) {
return observable.get();
public dispose(): void {
this.disposed = true;
for (const o of this.dependencies) {
o.removeObserver(this);
}
this.dependencies.clear();
}

observable.addObserver(this);
const value = observable.get();
this._dependencies.add(observable);
this.staleDependencies.delete(observable);
return value;
private _runIfNeeded() {
if (this.state === AutorunState.upToDate) {
return;
}

const emptySet = this.dependenciesToBeRemoved;
this.dependenciesToBeRemoved = this.dependencies;
this.dependencies = emptySet;

this.state = AutorunState.upToDate;

getLogger()?.handleAutorunTriggered(this);

try {
this.runFn(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.dependenciesToBeRemoved) {
o.removeObserver(this);
}
this.dependenciesToBeRemoved.clear();
}
}

public toString(): string {
return `Autorun<${this.debugName}>`;
}

// IObserver implementation
public beginUpdate(): void {
if (this.state === AutorunState.upToDate) {
this.state = AutorunState.dependenciesMightHaveChanged;
Expand All @@ -115,7 +129,7 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
do {
if (this.state === AutorunState.dependenciesMightHaveChanged) {
this.state = AutorunState.upToDate;
for (const d of this._dependencies) {
for (const d of this.dependencies) {
d.reportChanges();
if (this.state as AutorunState === AutorunState.stale) {
// The other dependencies will refresh on demand
Expand All @@ -124,7 +138,7 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
}
}

this.runIfNeeded();
this._runIfNeeded();
} while (this.state !== AutorunState.upToDate);
}
this.updateCount--;
Expand All @@ -149,42 +163,18 @@ export class AutorunObserver implements IObserver, IReader, IDisposable {
}
}

private runIfNeeded() {
if (this.state === AutorunState.upToDate) {
return;
}

// Assert: this.staleDependencies is an empty set.
const emptySet = this.staleDependencies;
this.staleDependencies = this._dependencies;
this._dependencies = emptySet;

this.state = AutorunState.upToDate;

getLogger()?.handleAutorunTriggered(this);

try {
this.runFn(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();
}
}

public dispose(): void {
this.disposed = true;
for (const o of this._dependencies) {
o.removeObserver(this);
// IReader implementation
public readObservable<T>(observable: IObservable<T>): T {
// In case the run action disposes the autorun
if (this.disposed) {
return observable.get();
}
this._dependencies.clear();
}

public toString(): string {
return `Autorun<${this.debugName}>`;
observable.addObserver(this);
const value = observable.get();
this.dependencies.add(observable);
this.dependenciesToBeRemoved.delete(observable);
return value;
}
}

Expand Down
94 changes: 63 additions & 31 deletions src/vs/base/common/observableImpl/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,71 +8,106 @@ import type { derived } from 'vs/base/common/observableImpl/derived';
import { getLogger } from 'vs/base/common/observableImpl/logging';

export interface IObservable<T, TChange = unknown> {
readonly TChange: TChange;

/**
* Reads the current value.
* Returns the current value.
*
* Must not be called from {@link IObserver.handleChange}.
* Calls {@link IObserver.handleChange} if the observable notices that the value changed.
* Must not be called from {@link IObserver.handleChange}!
*/
get(): T;

/**
* Forces the observable to check for and report changes.
*
* Has the same effect as calling {@link IObservable.get}, but does not force the observable
* to actually construct the value, e.g. if change deltas are used.
* Calls {@link IObserver.handleChange} if the observable notices that the value changed.
* Must not be called from {@link IObserver.handleChange}!
*/
reportChanges(): void;

/**
* Adds the observer to the set of subscribed observers.
* This method is idempotent.
*/
addObserver(observer: IObserver): void;

/**
* Removes the observer from the set of subscribed observers.
* This method is idempotent.
*/
removeObserver(observer: IObserver): void;

/**
* Subscribes the reader to this observable and returns the current value of this observable.
* Reads the current value and subscribes to this observable.
*
* Just calls {@link IReader.readObservable} if a reader is given, otherwise {@link IObservable.get}
* (see {@link ConvenientObservable.read}).
*/
read(reader: IReader | undefined): T;

/**
* Creates a derived observable that depends on this observable.
* Use the reader to read other observables
* (see {@link ConvenientObservable.map}).
*/
map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;

/**
* A human-readable name for debugging purposes.
*/
readonly debugName: string;

/*get isPossiblyStale(): boolean;
get isUpdating(): boolean;*/
/**
* This property captures the type of the change object. Do not use it at runtime!
*/
readonly TChange: TChange;
}

export interface IReader {
/**
* Reads the value of an observable and subscribes to it.
*
* Is called by {@link IObservable.read}.
*/
readObservable<T>(observable: IObservable<T, any>): T;
}

/**
* Represents an observer that can be subscribed to an observable.
*
* If an observer is subscribed to an observable and that observable didn't signal
* a change through one of the observer methods, the observer can assume that the
* observable didn't change.
* If an observable reported a possible change, {@link IObservable.reportChanges} forces
* the observable to report an actual change if there was one.
*/
export interface IObserver {
/**
* Indicates that calling {@link IObservable.get} might return a different value and the observable is in updating mode.
* Must not be called when the given observable has already been reported to be in updating mode.
* Signals that the given observable might have changed and a transaction potentially modifying that observable started.
* Before the given observable can call this method again, is must call {@link IObserver.endUpdate}.
*
* The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
*/
beginUpdate<T>(observable: IObservable<T>): void;

/**
* Is called by a subscribed observable when it leaves updating mode and it doesn't expect changes anymore,
* i.e. when a transaction for that observable is over.
*
* Call {@link IObservable.reportChanges} to learn about possible changes (if they weren't reported yet).
* Signals that the transaction that potentially modified the given observable ended.
*/
endUpdate<T>(observable: IObservable<T>): void;

/**
* Signals that the given observable might have changed.
* The method {@link IObservable.reportChanges} can be used to force the observable to report the changes.
*
* Implementations must not call into other observables, as they might not have received this event yet!
* The change should be processed lazily or in {@link IObserver.endUpdate}.
*/
handlePossibleChange<T>(observable: IObservable<T>): void;

/**
* Is called by a subscribed observable immediately after it notices a change.
*
* When {@link IObservable.get} is called two times and no change was reported before the second call returns,
* there has been no change in between the two calls for that observable.
*
* If the update counter is zero for a subscribed observable and calling {@link IObservable.get} didn't trigger a change,
* subsequent calls to {@link IObservable.get} don't trigger a change either until the update counter is increased again.
* Signals that the given observable changed.
*
* Implementations must not call into other observables!
* The change should be processed when all observed observables settled.
* Implementations must not call into other observables, as they might not have received this event yet!
* The change should be processed lazily or in {@link IObserver.endUpdate}.
*/
handleChange<T, TChange>(observable: IObservable<T, TChange>, change: TChange): void;
}
Expand All @@ -83,13 +118,10 @@ export interface ISettable<T, TChange = void> {

export interface ITransaction {
/**
* Calls `Observer.beginUpdate` immediately
* and `Observer.endUpdate` when the transaction is complete.
* Calls {@link Observer.beginUpdate} immediately
* and {@link Observer.endUpdate} when the transaction ends.
*/
updateObserver(
observer: IObserver,
observable: IObservable<any, any>
): void;
updateObserver(observer: IObserver, observable: IObservable<any, any>): void;
}

let _derived: typeof derived;
Expand Down
Loading

0 comments on commit 6fc6189

Please sign in to comment.