278 lines
8.7 KiB
JavaScript
278 lines
8.7 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, getFunctionName } from './debugName.js';
|
|
import { strictEquals } from './commonFacade/deps.js';
|
|
import { getLogger, logObservable } from './logging.js';
|
|
let _recomputeInitiallyAndOnChange;
|
|
export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange) {
|
|
_recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
|
|
}
|
|
let _keepObserved;
|
|
export function _setKeepObserved(keepObserved) {
|
|
_keepObserved = keepObserved;
|
|
}
|
|
let _derived;
|
|
/**
|
|
* @internal
|
|
* This is to allow splitting files.
|
|
*/
|
|
export function _setDerivedOpts(derived) {
|
|
_derived = derived;
|
|
}
|
|
export class ConvenientObservable {
|
|
get TChange() { return null; }
|
|
reportChanges() {
|
|
this.get();
|
|
}
|
|
/** @sealed */
|
|
read(reader) {
|
|
if (reader) {
|
|
return reader.readObservable(this);
|
|
}
|
|
else {
|
|
return this.get();
|
|
}
|
|
}
|
|
map(fnOrOwner, fnOrUndefined) {
|
|
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner;
|
|
const fn = fnOrUndefined === undefined ? fnOrOwner : fnOrUndefined;
|
|
return _derived({
|
|
owner,
|
|
debugName: () => {
|
|
const name = getFunctionName(fn);
|
|
if (name !== undefined) {
|
|
return name;
|
|
}
|
|
// regexp to match `x => x.y` or `x => x?.y` where x and y can be arbitrary identifiers (uses backref):
|
|
const regexp = /^\s*\(?\s*([a-zA-Z_$][a-zA-Z_$0-9]*)\s*\)?\s*=>\s*\1(?:\??)\.([a-zA-Z_$][a-zA-Z_$0-9]*)\s*$/;
|
|
const match = regexp.exec(fn.toString());
|
|
if (match) {
|
|
return `${this.debugName}.${match[2]}`;
|
|
}
|
|
if (!owner) {
|
|
return `${this.debugName} (mapped)`;
|
|
}
|
|
return undefined;
|
|
},
|
|
debugReferenceFn: fn,
|
|
}, (reader) => fn(this.read(reader), reader));
|
|
}
|
|
log() {
|
|
logObservable(this);
|
|
return this;
|
|
}
|
|
/**
|
|
* @sealed
|
|
* Converts an observable of an observable value into a direct observable of the value.
|
|
*/
|
|
flatten() {
|
|
return _derived({
|
|
owner: undefined,
|
|
debugName: () => `${this.debugName} (flattened)`,
|
|
}, (reader) => this.read(reader).read(reader));
|
|
}
|
|
recomputeInitiallyAndOnChange(store, handleValue) {
|
|
store.add(_recomputeInitiallyAndOnChange(this, handleValue));
|
|
return this;
|
|
}
|
|
/**
|
|
* Ensures that this observable is observed. This keeps the cache alive.
|
|
* However, in case of deriveds, it does not force eager evaluation (only when the value is read/get).
|
|
* Use `recomputeInitiallyAndOnChange` for eager evaluation.
|
|
*/
|
|
keepObserved(store) {
|
|
store.add(_keepObserved(this));
|
|
return this;
|
|
}
|
|
get debugValue() {
|
|
return this.get();
|
|
}
|
|
}
|
|
export class BaseObservable extends ConvenientObservable {
|
|
observers = new Set();
|
|
addObserver(observer) {
|
|
const len = this.observers.size;
|
|
this.observers.add(observer);
|
|
if (len === 0) {
|
|
this.onFirstObserverAdded();
|
|
}
|
|
}
|
|
removeObserver(observer) {
|
|
const deleted = this.observers.delete(observer);
|
|
if (deleted && this.observers.size === 0) {
|
|
this.onLastObserverRemoved();
|
|
}
|
|
}
|
|
onFirstObserverAdded() { }
|
|
onLastObserverRemoved() { }
|
|
}
|
|
/**
|
|
* Starts a transaction in which many observables can be changed at once.
|
|
* {@link fn} should start with a JS Doc using `@description` to give the transaction a debug name.
|
|
* Reaction run on demand or when the transaction ends.
|
|
*/
|
|
export function transaction(fn, getDebugName) {
|
|
const tx = new TransactionImpl(fn, getDebugName);
|
|
try {
|
|
fn(tx);
|
|
}
|
|
finally {
|
|
tx.finish();
|
|
}
|
|
}
|
|
let _globalTransaction = undefined;
|
|
export function globalTransaction(fn) {
|
|
if (_globalTransaction) {
|
|
fn(_globalTransaction);
|
|
}
|
|
else {
|
|
const tx = new TransactionImpl(fn, undefined);
|
|
_globalTransaction = tx;
|
|
try {
|
|
fn(tx);
|
|
}
|
|
finally {
|
|
tx.finish(); // During finish, more actions might be added to the transaction.
|
|
// Which is why we only clear the global transaction after finish.
|
|
_globalTransaction = undefined;
|
|
}
|
|
}
|
|
}
|
|
export async function asyncTransaction(fn, getDebugName) {
|
|
const tx = new TransactionImpl(fn, getDebugName);
|
|
try {
|
|
await fn(tx);
|
|
}
|
|
finally {
|
|
tx.finish();
|
|
}
|
|
}
|
|
/**
|
|
* Allows to chain transactions.
|
|
*/
|
|
export function subtransaction(tx, fn, getDebugName) {
|
|
if (!tx) {
|
|
transaction(fn, getDebugName);
|
|
}
|
|
else {
|
|
fn(tx);
|
|
}
|
|
}
|
|
export class TransactionImpl {
|
|
_fn;
|
|
_getDebugName;
|
|
updatingObservers = [];
|
|
constructor(_fn, _getDebugName) {
|
|
this._fn = _fn;
|
|
this._getDebugName = _getDebugName;
|
|
getLogger()?.handleBeginTransaction(this);
|
|
}
|
|
getDebugName() {
|
|
if (this._getDebugName) {
|
|
return this._getDebugName();
|
|
}
|
|
return getFunctionName(this._fn);
|
|
}
|
|
updateObserver(observer, observable) {
|
|
// When this gets called while finish is active, they will still get considered
|
|
this.updatingObservers.push({ observer, observable });
|
|
observer.beginUpdate(observable);
|
|
}
|
|
finish() {
|
|
const updatingObservers = this.updatingObservers;
|
|
for (let i = 0; i < updatingObservers.length; i++) {
|
|
const { observer, observable } = updatingObservers[i];
|
|
observer.endUpdate(observable);
|
|
}
|
|
// Prevent anyone from updating observers from now on.
|
|
this.updatingObservers = null;
|
|
getLogger()?.handleEndTransaction();
|
|
}
|
|
}
|
|
export function observableValue(nameOrOwner, initialValue) {
|
|
let debugNameData;
|
|
if (typeof nameOrOwner === 'string') {
|
|
debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
|
|
}
|
|
else {
|
|
debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
|
|
}
|
|
return new ObservableValue(debugNameData, initialValue, strictEquals);
|
|
}
|
|
export class ObservableValue extends BaseObservable {
|
|
_debugNameData;
|
|
_equalityComparator;
|
|
_value;
|
|
get debugName() {
|
|
return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
|
|
}
|
|
constructor(_debugNameData, initialValue, _equalityComparator) {
|
|
super();
|
|
this._debugNameData = _debugNameData;
|
|
this._equalityComparator = _equalityComparator;
|
|
this._value = initialValue;
|
|
}
|
|
get() {
|
|
return this._value;
|
|
}
|
|
set(value, tx, change) {
|
|
if (change === undefined && this._equalityComparator(this._value, value)) {
|
|
return;
|
|
}
|
|
let _tx;
|
|
if (!tx) {
|
|
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
|
|
}
|
|
try {
|
|
const oldValue = this._value;
|
|
this._setValue(value);
|
|
getLogger()?.handleObservableChanged(this, { oldValue, newValue: value, change, didChange: true, hadValue: true });
|
|
for (const observer of this.observers) {
|
|
tx.updateObserver(observer, this);
|
|
observer.handleChange(this, change);
|
|
}
|
|
}
|
|
finally {
|
|
if (_tx) {
|
|
_tx.finish();
|
|
}
|
|
}
|
|
}
|
|
toString() {
|
|
return `${this.debugName}: ${this._value}`;
|
|
}
|
|
_setValue(newValue) {
|
|
this._value = newValue;
|
|
}
|
|
}
|
|
/**
|
|
* A disposable observable. When disposed, its value is also disposed.
|
|
* When a new value is set, the previous value is disposed.
|
|
*/
|
|
export function disposableObservableValue(nameOrOwner, initialValue) {
|
|
let debugNameData;
|
|
if (typeof nameOrOwner === 'string') {
|
|
debugNameData = new DebugNameData(undefined, nameOrOwner, undefined);
|
|
}
|
|
else {
|
|
debugNameData = new DebugNameData(nameOrOwner, undefined, undefined);
|
|
}
|
|
return new DisposableObservableValue(debugNameData, initialValue, strictEquals);
|
|
}
|
|
export class DisposableObservableValue extends ObservableValue {
|
|
_setValue(newValue) {
|
|
if (this._value === newValue) {
|
|
return;
|
|
}
|
|
if (this._value) {
|
|
this._value.dispose();
|
|
}
|
|
this._value = newValue;
|
|
}
|
|
dispose() {
|
|
this._value?.dispose();
|
|
}
|
|
}
|
|
//# sourceMappingURL=base.js.map
|