264 lines
9.9 KiB
JavaScript
264 lines
9.9 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 { DebugNameData } from './debugName.js';
|
|
import { assertFn, BugIndicatingError, DisposableStore, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js';
|
|
import { getLogger } from './logging.js';
|
|
/**
|
|
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
|
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
|
*/
|
|
export function autorun(fn) {
|
|
return new AutorunObserver(new DebugNameData(undefined, undefined, fn), fn, undefined, undefined);
|
|
}
|
|
/**
|
|
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
|
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
|
*/
|
|
export function autorunOpts(options, fn) {
|
|
return new AutorunObserver(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, undefined, undefined);
|
|
}
|
|
/**
|
|
* Runs immediately and whenever a transaction ends and an observed observable changed.
|
|
* {@link fn} should start with a JS Doc using `@description` to name the autorun.
|
|
*
|
|
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
|
|
* Use `handleChange` to add a reported change to the change summary.
|
|
* The run function is given the last change summary.
|
|
* The change summary is discarded after the run function was called.
|
|
*
|
|
* @see autorun
|
|
*/
|
|
export function autorunHandleChanges(options, fn) {
|
|
return new AutorunObserver(new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn), fn, options.createEmptyChangeSummary, options.handleChange);
|
|
}
|
|
/**
|
|
* @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
|
|
*/
|
|
export function autorunWithStoreHandleChanges(options, fn) {
|
|
const store = new DisposableStore();
|
|
const disposable = autorunHandleChanges({
|
|
owner: options.owner,
|
|
debugName: options.debugName,
|
|
debugReferenceFn: options.debugReferenceFn ?? fn,
|
|
createEmptyChangeSummary: options.createEmptyChangeSummary,
|
|
handleChange: options.handleChange,
|
|
}, (reader, changeSummary) => {
|
|
store.clear();
|
|
fn(reader, changeSummary, store);
|
|
});
|
|
return toDisposable(() => {
|
|
disposable.dispose();
|
|
store.dispose();
|
|
});
|
|
}
|
|
/**
|
|
* @see autorun (but with a disposable store that is cleared before the next run or on dispose)
|
|
*/
|
|
export function autorunWithStore(fn) {
|
|
const store = new DisposableStore();
|
|
const disposable = autorunOpts({
|
|
owner: undefined,
|
|
debugName: undefined,
|
|
debugReferenceFn: fn,
|
|
}, reader => {
|
|
store.clear();
|
|
fn(reader, store);
|
|
});
|
|
return toDisposable(() => {
|
|
disposable.dispose();
|
|
store.dispose();
|
|
});
|
|
}
|
|
export function autorunDelta(observable, handler) {
|
|
let _lastValue;
|
|
return autorunOpts({ debugReferenceFn: handler }, (reader) => {
|
|
const newValue = observable.read(reader);
|
|
const lastValue = _lastValue;
|
|
_lastValue = newValue;
|
|
handler({ lastValue, newValue });
|
|
});
|
|
}
|
|
export function autorunIterableDelta(getValue, handler, getUniqueIdentifier = v => v) {
|
|
const lastValues = new Map();
|
|
return autorunOpts({ debugReferenceFn: getValue }, (reader) => {
|
|
const newValues = new Map();
|
|
const removedValues = new Map(lastValues);
|
|
for (const value of getValue(reader)) {
|
|
const id = getUniqueIdentifier(value);
|
|
if (lastValues.has(id)) {
|
|
removedValues.delete(id);
|
|
}
|
|
else {
|
|
newValues.set(id, value);
|
|
lastValues.set(id, value);
|
|
}
|
|
}
|
|
for (const id of removedValues.keys()) {
|
|
lastValues.delete(id);
|
|
}
|
|
if (newValues.size || removedValues.size) {
|
|
handler({ addedValues: [...newValues.values()], removedValues: [...removedValues.values()] });
|
|
}
|
|
});
|
|
}
|
|
var AutorunState;
|
|
(function (AutorunState) {
|
|
/**
|
|
* A dependency could have changed.
|
|
* We need to explicitly ask them if at least one dependency changed.
|
|
*/
|
|
AutorunState[AutorunState["dependenciesMightHaveChanged"] = 1] = "dependenciesMightHaveChanged";
|
|
/**
|
|
* A dependency changed and we need to recompute.
|
|
*/
|
|
AutorunState[AutorunState["stale"] = 2] = "stale";
|
|
AutorunState[AutorunState["upToDate"] = 3] = "upToDate";
|
|
})(AutorunState || (AutorunState = {}));
|
|
export class AutorunObserver {
|
|
_debugNameData;
|
|
_runFn;
|
|
createChangeSummary;
|
|
_handleChange;
|
|
state = 2 /* AutorunState.stale */;
|
|
updateCount = 0;
|
|
disposed = false;
|
|
dependencies = new Set();
|
|
dependenciesToBeRemoved = new Set();
|
|
changeSummary;
|
|
get debugName() {
|
|
return this._debugNameData.getDebugName(this) ?? '(anonymous)';
|
|
}
|
|
constructor(_debugNameData, _runFn, createChangeSummary, _handleChange) {
|
|
this._debugNameData = _debugNameData;
|
|
this._runFn = _runFn;
|
|
this.createChangeSummary = createChangeSummary;
|
|
this._handleChange = _handleChange;
|
|
this.changeSummary = this.createChangeSummary?.();
|
|
getLogger()?.handleAutorunCreated(this);
|
|
this._runIfNeeded();
|
|
trackDisposable(this);
|
|
}
|
|
dispose() {
|
|
this.disposed = true;
|
|
for (const o of this.dependencies) {
|
|
o.removeObserver(this);
|
|
}
|
|
this.dependencies.clear();
|
|
markAsDisposed(this);
|
|
}
|
|
_runIfNeeded() {
|
|
if (this.state === 3 /* AutorunState.upToDate */) {
|
|
return;
|
|
}
|
|
const emptySet = this.dependenciesToBeRemoved;
|
|
this.dependenciesToBeRemoved = this.dependencies;
|
|
this.dependencies = emptySet;
|
|
this.state = 3 /* AutorunState.upToDate */;
|
|
const isDisposed = this.disposed;
|
|
try {
|
|
if (!isDisposed) {
|
|
getLogger()?.handleAutorunTriggered(this);
|
|
const changeSummary = this.changeSummary;
|
|
try {
|
|
this.changeSummary = this.createChangeSummary?.();
|
|
this._isReaderValid = true;
|
|
this._runFn(this, changeSummary);
|
|
}
|
|
catch (e) {
|
|
onBugIndicatingError(e);
|
|
}
|
|
finally {
|
|
this._isReaderValid = false;
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
if (!isDisposed) {
|
|
getLogger()?.handleAutorunFinished(this);
|
|
}
|
|
// 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();
|
|
}
|
|
}
|
|
toString() {
|
|
return `Autorun<${this.debugName}>`;
|
|
}
|
|
// IObserver implementation
|
|
beginUpdate() {
|
|
if (this.state === 3 /* AutorunState.upToDate */) {
|
|
this.state = 1 /* AutorunState.dependenciesMightHaveChanged */;
|
|
}
|
|
this.updateCount++;
|
|
}
|
|
endUpdate() {
|
|
try {
|
|
if (this.updateCount === 1) {
|
|
do {
|
|
if (this.state === 1 /* AutorunState.dependenciesMightHaveChanged */) {
|
|
this.state = 3 /* AutorunState.upToDate */;
|
|
for (const d of this.dependencies) {
|
|
d.reportChanges();
|
|
if (this.state === 2 /* AutorunState.stale */) {
|
|
// The other dependencies will refresh on demand
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this._runIfNeeded();
|
|
} while (this.state !== 3 /* AutorunState.upToDate */);
|
|
}
|
|
}
|
|
finally {
|
|
this.updateCount--;
|
|
}
|
|
assertFn(() => this.updateCount >= 0);
|
|
}
|
|
handlePossibleChange(observable) {
|
|
if (this.state === 3 /* AutorunState.upToDate */ && this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
|
this.state = 1 /* AutorunState.dependenciesMightHaveChanged */;
|
|
}
|
|
}
|
|
handleChange(observable, change) {
|
|
if (this.dependencies.has(observable) && !this.dependenciesToBeRemoved.has(observable)) {
|
|
try {
|
|
const shouldReact = this._handleChange ? this._handleChange({
|
|
changedObservable: observable,
|
|
change,
|
|
didChange: (o) => o === observable,
|
|
}, this.changeSummary) : true;
|
|
if (shouldReact) {
|
|
this.state = 2 /* AutorunState.stale */;
|
|
}
|
|
}
|
|
catch (e) {
|
|
onBugIndicatingError(e);
|
|
}
|
|
}
|
|
}
|
|
// IReader implementation
|
|
_isReaderValid = false;
|
|
readObservable(observable) {
|
|
if (!this._isReaderValid) {
|
|
throw new BugIndicatingError('The reader object cannot be used outside its compute function!');
|
|
}
|
|
// In case the run action disposes the autorun
|
|
if (this.disposed) {
|
|
return observable.get();
|
|
}
|
|
observable.addObserver(this);
|
|
const value = observable.get();
|
|
this.dependencies.add(observable);
|
|
this.dependenciesToBeRemoved.delete(observable);
|
|
return value;
|
|
}
|
|
}
|
|
(function (autorun) {
|
|
autorun.Observer = AutorunObserver;
|
|
})(autorun || (autorun = {}));
|
|
//# sourceMappingURL=autorun.js.map
|