mono/packages/core/dist/observableInternal/autorun.js
2025-01-28 13:42:22 +01:00

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