mirror of https://github.com/microsoft/vscode.git
Splits up observable files
This commit is contained in:
parent
5c34839c25
commit
bf56196802
|
@ -3,12 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js';
|
||||
import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js';
|
||||
import type { derivedOpts } from './derived.js';
|
||||
import { getLogger, logObservable } from './logging/logging.js';
|
||||
import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js';
|
||||
import { onUnexpectedError } from '../errors.js';
|
||||
import { DisposableStore, onUnexpectedError } from './commonFacade/deps.js';
|
||||
|
||||
/**
|
||||
* Represents an observable value.
|
||||
|
@ -179,272 +174,10 @@ export interface ITransaction {
|
|||
updateObserver(observer: IObserver, observable: IObservableWithChange<any, any>): void;
|
||||
}
|
||||
|
||||
let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange;
|
||||
export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange: typeof _recomputeInitiallyAndOnChange) {
|
||||
_recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
|
||||
}
|
||||
|
||||
let _keepObserved: typeof keepObserved;
|
||||
export function _setKeepObserved(keepObserved: typeof _keepObserved) {
|
||||
_keepObserved = keepObserved;
|
||||
}
|
||||
|
||||
let _derived: typeof derivedOpts;
|
||||
/**
|
||||
* @internal
|
||||
* This is to allow splitting files.
|
||||
*/
|
||||
export function _setDerivedOpts(derived: typeof _derived) {
|
||||
_derived = derived;
|
||||
}
|
||||
|
||||
export abstract class ConvenientObservable<T, TChange> implements IObservableWithChange<T, TChange> {
|
||||
get TChange(): TChange { return null!; }
|
||||
|
||||
public abstract get(): T;
|
||||
|
||||
public reportChanges(): void {
|
||||
this.get();
|
||||
}
|
||||
|
||||
public abstract addObserver(observer: IObserver): void;
|
||||
public abstract removeObserver(observer: IObserver): void;
|
||||
|
||||
/** @sealed */
|
||||
public read(reader: IReader | undefined): T {
|
||||
if (reader) {
|
||||
return reader.readObservable(this);
|
||||
} else {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
/** @sealed */
|
||||
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
|
||||
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
|
||||
const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : 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),
|
||||
);
|
||||
}
|
||||
|
||||
public abstract log(): IObservableWithChange<T, TChange>;
|
||||
|
||||
/**
|
||||
* @sealed
|
||||
* Converts an observable of an observable value into a direct observable of the value.
|
||||
*/
|
||||
public flatten<TNew>(this: IObservable<IObservableWithChange<TNew, any>>): IObservable<TNew> {
|
||||
return _derived(
|
||||
{
|
||||
owner: undefined,
|
||||
debugName: () => `${this.debugName} (flattened)`,
|
||||
},
|
||||
(reader) => this.read(reader).read(reader),
|
||||
);
|
||||
}
|
||||
|
||||
public recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T> {
|
||||
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.
|
||||
*/
|
||||
public keepObserved(store: DisposableStore): IObservable<T> {
|
||||
store.add(_keepObserved!(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract get debugName(): string;
|
||||
|
||||
protected get debugValue() {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
|
||||
protected readonly _observers = new Set<IObserver>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
getLogger()?.handleObservableCreated(this);
|
||||
}
|
||||
|
||||
public addObserver(observer: IObserver): void {
|
||||
const len = this._observers.size;
|
||||
this._observers.add(observer);
|
||||
if (len === 0) {
|
||||
this.onFirstObserverAdded();
|
||||
}
|
||||
if (len !== this._observers.size) {
|
||||
getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
|
||||
}
|
||||
}
|
||||
|
||||
public removeObserver(observer: IObserver): void {
|
||||
const deleted = this._observers.delete(observer);
|
||||
if (deleted && this._observers.size === 0) {
|
||||
this.onLastObserverRemoved();
|
||||
}
|
||||
if (deleted) {
|
||||
getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
|
||||
}
|
||||
}
|
||||
|
||||
protected onFirstObserverAdded(): void { }
|
||||
protected onLastObserverRemoved(): void { }
|
||||
|
||||
public override log(): IObservableWithChange<T, TChange> {
|
||||
const hadLogger = !!getLogger();
|
||||
logObservable(this);
|
||||
if (!hadLogger) {
|
||||
getLogger()?.handleObservableCreated(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public debugGetObservers() {
|
||||
return this._observers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
|
||||
let _globalTransaction: ITransaction | undefined = undefined;
|
||||
|
||||
export function globalTransaction(fn: (tx: ITransaction) => void) {
|
||||
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: (tx: ITransaction) => Promise<void>, getDebugName?: () => string): Promise<void> {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
await fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to chain transactions.
|
||||
*/
|
||||
export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
if (!tx) {
|
||||
transaction(fn, getDebugName);
|
||||
} else {
|
||||
fn(tx);
|
||||
}
|
||||
}
|
||||
|
||||
export class TransactionImpl implements ITransaction {
|
||||
private _updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
|
||||
|
||||
constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) {
|
||||
getLogger()?.handleBeginTransaction(this);
|
||||
}
|
||||
|
||||
public getDebugName(): string | undefined {
|
||||
if (this._getDebugName) {
|
||||
return this._getDebugName();
|
||||
}
|
||||
return getFunctionName(this._fn);
|
||||
}
|
||||
|
||||
public updateObserver(observer: IObserver, observable: IObservable<any>): void {
|
||||
if (!this._updatingObservers) {
|
||||
// This happens when a transaction is used in a callback or async function.
|
||||
// If an async transaction is used, make sure the promise awaits all users of the transaction (e.g. no race).
|
||||
handleBugIndicatingErrorRecovery('Transaction already finished!');
|
||||
// Error recovery
|
||||
transaction(tx => {
|
||||
tx.updateObserver(observer, observable);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// When this gets called while finish is active, they will still get considered
|
||||
this._updatingObservers.push({ observer, observable });
|
||||
observer.beginUpdate(observable);
|
||||
}
|
||||
|
||||
public finish(): void {
|
||||
const updatingObservers = this._updatingObservers;
|
||||
if (!updatingObservers) {
|
||||
handleBugIndicatingErrorRecovery('transaction.finish() has already been called!');
|
||||
return;
|
||||
}
|
||||
|
||||
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(this);
|
||||
}
|
||||
|
||||
public debugGetUpdatingObservers() {
|
||||
return this._updatingObservers;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to indicate that the caller recovered from an error that indicates a bug.
|
||||
*/
|
||||
function handleBugIndicatingErrorRecovery(message: string) {
|
||||
export function handleBugIndicatingErrorRecovery(message: string) {
|
||||
const err = new Error('BugIndicatingErrorRecovery: ' + message);
|
||||
onUnexpectedError(err);
|
||||
console.error('recovered from an error that indicates a bug', err);
|
||||
|
@ -456,121 +189,6 @@ function handleBugIndicatingErrorRecovery(message: string) {
|
|||
export interface ISettableObservable<T, TChange = void> extends IObservableWithChange<T, TChange>, ISettable<T, TChange> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable value.
|
||||
* Observers get informed when the value changes.
|
||||
* @template TChange An arbitrary type to describe how or why the value changed. Defaults to `void`.
|
||||
* Observers will receive every single change value.
|
||||
*/
|
||||
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> {
|
||||
let debugNameData: 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<T, TChange = void>
|
||||
extends BaseObservable<T, TChange>
|
||||
implements ISettableObservable<T, TChange> {
|
||||
protected _value: T;
|
||||
|
||||
get debugName() {
|
||||
return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
initialValue: T,
|
||||
private readonly _equalityComparator: EqualityComparer<T>,
|
||||
) {
|
||||
super();
|
||||
this._value = initialValue;
|
||||
|
||||
getLogger()?.handleObservableUpdated(this, { hadValue: false, newValue: initialValue, change: undefined, didChange: true, oldValue: undefined });
|
||||
}
|
||||
public override get(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
|
||||
if (change === undefined && this._equalityComparator(this._value, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _tx: TransactionImpl | undefined;
|
||||
if (!tx) {
|
||||
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
|
||||
}
|
||||
try {
|
||||
const oldValue = this._value;
|
||||
this._setValue(value);
|
||||
getLogger()?.handleObservableUpdated(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `${this.debugName}: ${this._value}`;
|
||||
}
|
||||
|
||||
protected _setValue(newValue: T): void {
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
public debugGetState() {
|
||||
return {
|
||||
value: this._value,
|
||||
};
|
||||
}
|
||||
|
||||
public debugSetValue(value: unknown) {
|
||||
this._value = value as T;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A disposable observable. When disposed, its value is also disposed.
|
||||
* When a new value is set, the previous value is disposed.
|
||||
*/
|
||||
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
|
||||
let debugNameData: 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<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
|
||||
protected override _setValue(newValue: T): void {
|
||||
if (this._value === newValue) {
|
||||
return;
|
||||
}
|
||||
if (this._value) {
|
||||
this._value.dispose();
|
||||
}
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._value?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface IReaderWithStore extends IReader {
|
||||
/**
|
||||
* Items in this store get disposed just before the observable recomputes/reruns or when it becomes unobserved.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BugIndicatingError } from '../errors.js';
|
||||
import { BugIndicatingError } from './commonFacade/deps.js';
|
||||
import { IObservableWithChange, IReader } from './base.js';
|
||||
|
||||
export interface IChangeTracker<TChangeSummary> {
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export { CancellationError } from '../../errors.js';
|
||||
export { CancellationToken, CancellationTokenSource } from '../../cancellation.js';
|
||||
export { CancellationToken, CancellationTokenSource, cancelOnDispose } from '../../cancellation.js';
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
|
||||
export { assertFn } from '../../assert.js';
|
||||
export { type EqualityComparer, strictEquals } from '../../equals.js';
|
||||
export { BugIndicatingError, onBugIndicatingError } from '../../errors.js';
|
||||
export { BugIndicatingError, onBugIndicatingError, onUnexpectedError } from '../../errors.js';
|
||||
export { Event, type IValueWithChangeEvent } from '../../event.js';
|
||||
export { DisposableStore, type IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../../lifecycle.js';
|
||||
|
|
|
@ -3,24 +3,26 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EqualityComparer, strictEquals } from '../equals.js';
|
||||
import { BugIndicatingError } from '../errors.js';
|
||||
import { IObservable, IObservableWithChange, ISettableObservable, subtransaction } from './base.js';
|
||||
import { IChangeTracker } from './changeTracker.js';
|
||||
import { DebugNameData, DebugOwner } from './debugName.js';
|
||||
import { DerivedWithSetter, IDerivedReader } from './derived.js';
|
||||
import { EqualityComparer, strictEquals, BugIndicatingError } from '../commonFacade/deps.js';
|
||||
import { IObservable, IObservableWithChange, ISettableObservable } from '../base.js';
|
||||
import { subtransaction } from '../transaction.js';
|
||||
import { IChangeTracker } from '../changeTracker.js';
|
||||
import { DebugNameData, DebugOwner } from '../debugName.js';
|
||||
import { DerivedWithSetter, IDerivedReader } from '../observables/derivedImpl.js';
|
||||
|
||||
export interface IReducerOptions<T, TChangeSummary = void, TOutChange = void> {
|
||||
/**
|
||||
* Is called to create the initial value of the observable when it becomes observed.
|
||||
*/
|
||||
initial: T | (() => T);
|
||||
|
||||
/**
|
||||
* Is called to dispose the observable value when it is no longer observed.
|
||||
*/
|
||||
disposeFinal?(value: T): void;
|
||||
changeTracker?: IChangeTracker<TChangeSummary>;
|
||||
equalityComparer?: EqualityComparer<T>;
|
||||
|
||||
/**
|
||||
* Applies the changes to the value.
|
||||
* Use `reader.reportChange` to report change details or to report a change if the same value is returned.
|
|
@ -0,0 +1,60 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable, IReader } from '../base.js';
|
||||
import { BugIndicatingError, DisposableStore } from '../commonFacade/deps.js';
|
||||
import { DebugOwner, getDebugName, DebugNameData } from '../debugName.js';
|
||||
import { observableFromEvent } from '../observables/observableFromEvent.js';
|
||||
import { autorunOpts } from '../reactions/autorun.js';
|
||||
import { derivedObservableWithCache } from '../utils/utils.js';
|
||||
|
||||
/**
|
||||
* Creates an observable that has the latest changed value of the given observables.
|
||||
* Initially (and when not observed), it has the value of the last observable.
|
||||
* When observed and any of the observables change, it has the value of the last changed observable.
|
||||
* If multiple observables change in the same transaction, the last observable wins.
|
||||
*/
|
||||
export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
|
||||
if (observables.length === 0) {
|
||||
throw new BugIndicatingError();
|
||||
}
|
||||
|
||||
let hasLastChangedValue = false;
|
||||
let lastChangedValue: any = undefined;
|
||||
|
||||
const result = observableFromEvent<any, void>(owner, cb => {
|
||||
const store = new DisposableStore();
|
||||
for (const o of observables) {
|
||||
store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
|
||||
hasLastChangedValue = true;
|
||||
lastChangedValue = o.read(reader);
|
||||
cb();
|
||||
}));
|
||||
}
|
||||
store.add({
|
||||
dispose() {
|
||||
hasLastChangedValue = false;
|
||||
lastChangedValue = undefined;
|
||||
},
|
||||
});
|
||||
return store;
|
||||
}, () => {
|
||||
if (hasLastChangedValue) {
|
||||
return lastChangedValue;
|
||||
} else {
|
||||
return observables[observables.length - 1].get();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like a derived.
|
||||
* However, if the value is not undefined, it is cached and will not be recomputed anymore.
|
||||
* In that case, the derived will unsubscribe from its dependencies.
|
||||
*/
|
||||
export function derivedConstOnceDefined<T>(owner: DebugOwner, fn: (reader: IReader) => T): IObservable<T | undefined> {
|
||||
return derivedObservableWithCache<T | undefined>(owner, (reader, lastValue) => lastValue ?? fn(reader));
|
||||
}
|
|
@ -5,21 +5,39 @@
|
|||
|
||||
// This is a facade for the observable implementation. Only import from here!
|
||||
|
||||
export { observableValueOpts } from './api.js';
|
||||
export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta } from './autorun.js';
|
||||
export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js';
|
||||
export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore, type IDerivedReader } from './derived.js';
|
||||
export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js';
|
||||
export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js';
|
||||
export { constObservable, debouncedObservableDeprecated, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, runOnChangeWithCancellationToken, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js';
|
||||
export { observableValueOpts } from './observables/observableValueOpts.js';
|
||||
export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges, autorunIterableDelta } from './reactions/autorun.js';
|
||||
export { type IObservable, type IObservableWithChange, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction } from './base.js';
|
||||
export { disposableObservableValue } from './observables/observableValue.js';
|
||||
export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './observables/derived.js';
|
||||
export { type IDerivedReader } from './observables/derivedImpl.js';
|
||||
export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './utils/promise.js';
|
||||
export { derivedWithCancellationToken, waitForState } from './utils/utilsCancellation.js';
|
||||
export {
|
||||
debouncedObservableDeprecated, debouncedObservable, derivedObservableWithCache,
|
||||
derivedObservableWithWritableCache, keepObserved, mapObservableArrayCached, observableFromPromise,
|
||||
recomputeInitiallyAndOnChange,
|
||||
signalFromObservable, wasEventTriggeredRecently,
|
||||
} from './utils/utils.js';
|
||||
export { type DebugOwner } from './debugName.js';
|
||||
export { type IChangeContext, type IChangeTracker, recordChanges } from './changeTracker.js';
|
||||
export { constObservable } from './observables/constObservable.js';
|
||||
export { type IObservableSignal, observableSignal } from './observables/observableSignal.js';
|
||||
export { observableFromEventOpts } from './observables/observableFromEvent.js';
|
||||
export { observableSignalFromEvent } from './observables/observableSignalFromEvent.js';
|
||||
export { asyncTransaction, globalTransaction, subtransaction, transaction, TransactionImpl } from './transaction.js';
|
||||
export { observableFromValueWithChangeEvent, ValueWithChangeEventFromObservable } from './utils/valueWithChangeEvent.js';
|
||||
export { runOnChange, runOnChangeWithCancellationToken, runOnChangeWithStore } from './utils/runOnChange.js';
|
||||
export { derivedConstOnceDefined, latestChangedValue } from './experimental/utils.js';
|
||||
export { observableFromEvent } from './observables/observableFromEvent.js';
|
||||
export { observableValue } from './observables/observableValue.js';
|
||||
|
||||
import { addLogger, setLogObservableFn } from './logging/logging.js';
|
||||
import { ConsoleObservableLogger, logObservableToConsole } from './logging/consoleObservableLogger.js';
|
||||
import { DevToolsLogger } from './logging/debugger/devToolsLogger.js';
|
||||
import { env } from '../process.js';
|
||||
|
||||
|
||||
setLogObservableFn(logObservableToConsole);
|
||||
|
||||
// Remove "//" in the next line to enable logging
|
||||
|
|
|
@ -3,12 +3,13 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AutorunObserver } from '../autorun.js';
|
||||
import { IObservable, TransactionImpl } from '../base.js';
|
||||
import { Derived } from '../derived.js';
|
||||
import { IObservable } from '../base.js';
|
||||
import { TransactionImpl } from '../transaction.js';
|
||||
import { IObservableLogger, IChangeInformation, addLogger } from './logging.js';
|
||||
import { FromEventObservable } from '../utils.js';
|
||||
import { FromEventObservable } from '../observables/observableFromEvent.js';
|
||||
import { getClassName } from '../debugName.js';
|
||||
import { Derived } from '../observables/derivedImpl.js';
|
||||
import { AutorunObserver } from '../reactions/autorunImpl.js';
|
||||
|
||||
let consoleObservableLogger: ConsoleObservableLogger | undefined;
|
||||
|
||||
|
|
|
@ -3,17 +3,20 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AutorunObserver, AutorunState } from '../../autorun.js';
|
||||
import { BaseObservable, IObservable, IObserver, ObservableValue, TransactionImpl } from '../../base.js';
|
||||
import { Derived, DerivedState } from '../../derived.js';
|
||||
import { AutorunObserver, AutorunState } from '../../reactions/autorunImpl.js';
|
||||
import { TransactionImpl } from '../../transaction.js';
|
||||
import { IChangeInformation, IObservableLogger } from '../logging.js';
|
||||
import { formatValue } from '../consoleObservableLogger.js';
|
||||
import { ObsDebuggerApi, IObsDeclaration, ObsInstanceId, ObsStateUpdate, ITransactionState, ObserverInstanceState } from './debuggerApi.js';
|
||||
import { registerDebugChannel } from './debuggerRpc.js';
|
||||
import { deepAssign, deepAssignDeleteNulls, getFirstStackFrameOutsideOf, ILocation, Throttler } from './utils.js';
|
||||
import { isDefined } from '../../../types.js';
|
||||
import { FromEventObservable } from '../../utils.js';
|
||||
import { FromEventObservable } from '../../observables/observableFromEvent.js';
|
||||
import { BugIndicatingError, onUnexpectedError } from '../../../errors.js';
|
||||
import { IObservable, IObserver } from '../../base.js';
|
||||
import { BaseObservable } from '../../observables/baseObservable.js';
|
||||
import { Derived, DerivedState } from '../../observables/derivedImpl.js';
|
||||
import { ObservableValue } from '../../observables/observableValue.js';
|
||||
|
||||
interface IInstanceInfo {
|
||||
declarationId: number;
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AutorunObserver } from '../autorun.js';
|
||||
import { IObservable, TransactionImpl } from '../base.js';
|
||||
import type { Derived } from '../derived.js';
|
||||
import { AutorunObserver } from '../reactions/autorunImpl.js';
|
||||
import { IObservable } from '../base.js';
|
||||
import { TransactionImpl } from '../transaction.js';
|
||||
import type { Derived } from '../observables/derivedImpl.js';
|
||||
|
||||
let globalObservableLogger: IObservableLogger | undefined;
|
||||
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservableWithChange, IObserver, IReader, IObservable } from '../base.js';
|
||||
import { DisposableStore } from '../commonFacade/deps.js';
|
||||
import { DebugOwner, getFunctionName } from '../debugName.js';
|
||||
import { getLogger, logObservable } from '../logging/logging.js';
|
||||
import type { keepObserved, recomputeInitiallyAndOnChange } from '../utils/utils.js';
|
||||
import { derivedOpts } from './derived.js';
|
||||
|
||||
let _derived: typeof derivedOpts;
|
||||
/**
|
||||
* @internal
|
||||
* This is to allow splitting files.
|
||||
*/
|
||||
export function _setDerivedOpts(derived: typeof _derived) {
|
||||
_derived = derived;
|
||||
}
|
||||
|
||||
let _recomputeInitiallyAndOnChange: typeof recomputeInitiallyAndOnChange;
|
||||
export function _setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange: typeof _recomputeInitiallyAndOnChange) {
|
||||
_recomputeInitiallyAndOnChange = recomputeInitiallyAndOnChange;
|
||||
}
|
||||
|
||||
let _keepObserved: typeof keepObserved;
|
||||
export function _setKeepObserved(keepObserved: typeof _keepObserved) {
|
||||
_keepObserved = keepObserved;
|
||||
}
|
||||
|
||||
export abstract class ConvenientObservable<T, TChange> implements IObservableWithChange<T, TChange> {
|
||||
get TChange(): TChange { return null!; }
|
||||
|
||||
public abstract get(): T;
|
||||
|
||||
public reportChanges(): void {
|
||||
this.get();
|
||||
}
|
||||
|
||||
public abstract addObserver(observer: IObserver): void;
|
||||
public abstract removeObserver(observer: IObserver): void;
|
||||
|
||||
/** @sealed */
|
||||
public read(reader: IReader | undefined): T {
|
||||
if (reader) {
|
||||
return reader.readObservable(this);
|
||||
} else {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
/** @sealed */
|
||||
public map<TNew>(fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(owner: DebugOwner, fn: (value: T, reader: IReader) => TNew): IObservable<TNew>;
|
||||
public map<TNew>(fnOrOwner: DebugOwner | ((value: T, reader: IReader) => TNew), fnOrUndefined?: (value: T, reader: IReader) => TNew): IObservable<TNew> {
|
||||
const owner = fnOrUndefined === undefined ? undefined : fnOrOwner as DebugOwner;
|
||||
const fn = fnOrUndefined === undefined ? fnOrOwner as (value: T, reader: IReader) => TNew : 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)
|
||||
);
|
||||
}
|
||||
|
||||
public abstract log(): IObservableWithChange<T, TChange>;
|
||||
|
||||
/**
|
||||
* @sealed
|
||||
* Converts an observable of an observable value into a direct observable of the value.
|
||||
*/
|
||||
public flatten<TNew>(this: IObservable<IObservableWithChange<TNew, any>>): IObservable<TNew> {
|
||||
return _derived(
|
||||
{
|
||||
owner: undefined,
|
||||
debugName: () => `${this.debugName} (flattened)`,
|
||||
},
|
||||
(reader) => this.read(reader).read(reader)
|
||||
);
|
||||
}
|
||||
|
||||
public recomputeInitiallyAndOnChange(store: DisposableStore, handleValue?: (value: T) => void): IObservable<T> {
|
||||
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.
|
||||
*/
|
||||
public keepObserved(store: DisposableStore): IObservable<T> {
|
||||
store.add(_keepObserved!(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract get debugName(): string;
|
||||
|
||||
protected get debugValue() {
|
||||
return this.get();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseObservable<T, TChange = void> extends ConvenientObservable<T, TChange> {
|
||||
protected readonly _observers = new Set<IObserver>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
getLogger()?.handleObservableCreated(this);
|
||||
}
|
||||
|
||||
public addObserver(observer: IObserver): void {
|
||||
const len = this._observers.size;
|
||||
this._observers.add(observer);
|
||||
if (len === 0) {
|
||||
this.onFirstObserverAdded();
|
||||
}
|
||||
if (len !== this._observers.size) {
|
||||
getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
|
||||
}
|
||||
}
|
||||
|
||||
public removeObserver(observer: IObserver): void {
|
||||
const deleted = this._observers.delete(observer);
|
||||
if (deleted && this._observers.size === 0) {
|
||||
this.onLastObserverRemoved();
|
||||
}
|
||||
if (deleted) {
|
||||
getLogger()?.handleOnListenerCountChanged(this, this._observers.size);
|
||||
}
|
||||
}
|
||||
|
||||
protected onFirstObserverAdded(): void { }
|
||||
protected onLastObserverRemoved(): void { }
|
||||
|
||||
public override log(): IObservableWithChange<T, TChange> {
|
||||
const hadLogger = !!getLogger();
|
||||
logObservable(this);
|
||||
if (!hadLogger) {
|
||||
getLogger()?.handleObservableCreated(this);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public debugGetObservers() {
|
||||
return this._observers;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable, IObserver, IObservableWithChange } from '../base.js';
|
||||
import { ConvenientObservable } from './baseObservable.js';
|
||||
|
||||
/**
|
||||
* Represents an efficient observable whose value never changes.
|
||||
*/
|
||||
|
||||
export function constObservable<T>(value: T): IObservable<T> {
|
||||
return new ConstObservable(value);
|
||||
}
|
||||
class ConstObservable<T> extends ConvenientObservable<T, void> {
|
||||
constructor(private readonly value: T) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override get debugName(): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
return this.value;
|
||||
}
|
||||
public addObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
public removeObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
override log(): IObservableWithChange<T, void> {
|
||||
return this;
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `Const: ${this.value}`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable, IReader, ITransaction, ISettableObservable, IObservableWithChange } from '../base.js';
|
||||
import { IChangeTracker } from '../changeTracker.js';
|
||||
import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from '../commonFacade/deps.js';
|
||||
import { DebugOwner, DebugNameData, IDebugNameData } from '../debugName.js';
|
||||
import { _setDerivedOpts } from './baseObservable.js';
|
||||
import { IDerivedReader, Derived, DerivedWithSetter } from './derivedImpl.js';
|
||||
|
||||
/**
|
||||
* Creates an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*/
|
||||
export function derived<T, TChange = void>(computeFn: (reader: IDerivedReader<TChange>) => T): IObservable<T>;
|
||||
export function derived<T, TChange = void>(owner: DebugOwner, computeFn: (reader: IDerivedReader<TChange>) => T): IObservable<T>;
|
||||
export function derived<T, TChange = void>(computeFnOrOwner: ((reader: IDerivedReader<TChange>) => T) | DebugOwner, computeFn?: ((reader: IDerivedReader<TChange>) => T) | undefined): IObservable<T> {
|
||||
if (computeFn !== undefined) {
|
||||
return new Derived(
|
||||
new DebugNameData(computeFnOrOwner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
return new Derived(
|
||||
new DebugNameData(undefined, undefined, computeFnOrOwner as any),
|
||||
computeFnOrOwner as any,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
|
||||
return new DerivedWithSetter(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals,
|
||||
setter
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedOpts<T>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
onLastObserverRemoved?: (() => void);
|
||||
},
|
||||
computeFn: (reader: IReader) => T
|
||||
): IObservable<T> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
options.onLastObserverRemoved,
|
||||
options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
_setDerivedOpts(derivedOpts);
|
||||
|
||||
/**
|
||||
* Represents an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*
|
||||
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `handleChange` to add a reported change to the change summary.
|
||||
* The compute function is given the last change summary.
|
||||
* The change summary is discarded after the compute function was called.
|
||||
*
|
||||
* @see derived
|
||||
*/
|
||||
export function derivedHandleChanges<T, TDelta, TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
equalityComparer?: EqualityComparer<T>;
|
||||
},
|
||||
computeFn: (reader: IDerivedReader<TDelta>, changeSummary: TChangeSummary) => T
|
||||
): IObservableWithChange<T, TDelta> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, undefined),
|
||||
computeFn,
|
||||
options.changeTracker,
|
||||
undefined,
|
||||
options.equalityComparer ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
|
||||
/**
|
||||
* @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function derivedWithStore<T>(owner: DebugOwner, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: DisposableStore) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
// Intentionally re-assigned in case an inactive observable is re-used later
|
||||
// eslint-disable-next-line local/code-no-potentially-unsafe-disposables
|
||||
let store = new DisposableStore();
|
||||
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (store.isDisposed) {
|
||||
store = new DisposableStore();
|
||||
} else {
|
||||
store.clear();
|
||||
}
|
||||
return computeFn(r, store);
|
||||
},
|
||||
undefined,
|
||||
() => store.dispose(),
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
let store: DisposableStore | undefined = undefined;
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (!store) {
|
||||
store = new DisposableStore();
|
||||
} else {
|
||||
store.clear();
|
||||
}
|
||||
const result = computeFn(r);
|
||||
if (result) {
|
||||
store.add(result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
if (store) {
|
||||
store.dispose();
|
||||
store = undefined;
|
||||
}
|
||||
},
|
||||
strictEquals
|
||||
);
|
||||
}
|
|
@ -3,11 +3,12 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { BaseObservable, IObservable, IObservableWithChange, IObserver, IReader, IReaderWithStore, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js';
|
||||
import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js';
|
||||
import { BugIndicatingError, DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js';
|
||||
import { getLogger } from './logging/logging.js';
|
||||
import { IChangeTracker } from './changeTracker.js';
|
||||
import { IObservable, IObservableWithChange, IObserver, IReaderWithStore, ISettableObservable, ITransaction, } from '../base.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
import { DebugNameData } from '../debugName.js';
|
||||
import { BugIndicatingError, DisposableStore, EqualityComparer, assertFn, onBugIndicatingError } from '../commonFacade/deps.js';
|
||||
import { getLogger } from '../logging/logging.js';
|
||||
import { IChangeTracker } from '../changeTracker.js';
|
||||
|
||||
export interface IDerivedReader<TChange = void> extends IReaderWithStore {
|
||||
/**
|
||||
|
@ -16,169 +17,6 @@ export interface IDerivedReader<TChange = void> extends IReaderWithStore {
|
|||
reportChange(change: TChange): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*/
|
||||
export function derived<T, TChange = void>(computeFn: (reader: IDerivedReader<TChange>) => T): IObservable<T>;
|
||||
export function derived<T, TChange = void>(owner: DebugOwner, computeFn: (reader: IDerivedReader<TChange>) => T): IObservable<T>;
|
||||
export function derived<T, TChange = void>(computeFnOrOwner: ((reader: IDerivedReader<TChange>) => T) | DebugOwner, computeFn?: ((reader: IDerivedReader<TChange>) => T) | undefined): IObservable<T> {
|
||||
if (computeFn !== undefined) {
|
||||
return new Derived(
|
||||
new DebugNameData(computeFnOrOwner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
return new Derived(
|
||||
new DebugNameData(undefined, undefined, computeFnOrOwner as any),
|
||||
computeFnOrOwner as any,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedWithSetter<T>(owner: DebugOwner | undefined, computeFn: (reader: IReader) => T, setter: (value: T, transaction: ITransaction | undefined) => void): ISettableObservable<T> {
|
||||
return new DerivedWithSetter(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
undefined,
|
||||
strictEquals,
|
||||
setter,
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedOpts<T>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
onLastObserverRemoved?: (() => void);
|
||||
},
|
||||
computeFn: (reader: IReader) => T
|
||||
): IObservable<T> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn),
|
||||
computeFn,
|
||||
undefined,
|
||||
options.onLastObserverRemoved,
|
||||
options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
_setDerivedOpts(derivedOpts);
|
||||
|
||||
/**
|
||||
* Represents an observable that is derived from other observables.
|
||||
* The value is only recomputed when absolutely needed.
|
||||
*
|
||||
* {@link computeFn} should start with a JS Doc using `@description` to name the derived.
|
||||
*
|
||||
* Use `createEmptyChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `handleChange` to add a reported change to the change summary.
|
||||
* The compute function is given the last change summary.
|
||||
* The change summary is discarded after the compute function was called.
|
||||
*
|
||||
* @see derived
|
||||
*/
|
||||
export function derivedHandleChanges<T, TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
equalityComparer?: EqualityComparer<T>;
|
||||
},
|
||||
computeFn: (reader: IReader, changeSummary: TChangeSummary) => T
|
||||
): IObservable<T> {
|
||||
return new Derived(
|
||||
new DebugNameData(options.owner, options.debugName, undefined),
|
||||
computeFn,
|
||||
options.changeTracker,
|
||||
undefined,
|
||||
options.equalityComparer ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function derivedWithStore<T>(computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
/**
|
||||
* @deprecated Use `derived(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function derivedWithStore<T>(owner: DebugOwner, computeFn: (reader: IReader, store: DisposableStore) => T): IObservable<T>;
|
||||
export function derivedWithStore<T>(computeFnOrOwner: ((reader: IReader, store: DisposableStore) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader, store: DisposableStore) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader, store: DisposableStore) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
// Intentionally re-assigned in case an inactive observable is re-used later
|
||||
// eslint-disable-next-line local/code-no-potentially-unsafe-disposables
|
||||
let store = new DisposableStore();
|
||||
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (store.isDisposed) {
|
||||
store = new DisposableStore();
|
||||
} else {
|
||||
store.clear();
|
||||
}
|
||||
return computeFn(r, store);
|
||||
},
|
||||
undefined,
|
||||
() => store.dispose(),
|
||||
strictEquals,
|
||||
);
|
||||
}
|
||||
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(owner: DebugOwner, computeFn: (reader: IReader) => T): IObservable<T>;
|
||||
export function derivedDisposable<T extends IDisposable | undefined>(computeFnOrOwner: ((reader: IReader) => T) | DebugOwner, computeFnOrUndefined?: ((reader: IReader) => T)): IObservable<T> {
|
||||
let computeFn: (reader: IReader) => T;
|
||||
let owner: DebugOwner;
|
||||
if (computeFnOrUndefined === undefined) {
|
||||
computeFn = computeFnOrOwner as any;
|
||||
owner = undefined;
|
||||
} else {
|
||||
owner = computeFnOrOwner;
|
||||
computeFn = computeFnOrUndefined as any;
|
||||
}
|
||||
|
||||
let store: DisposableStore | undefined = undefined;
|
||||
return new Derived(
|
||||
new DebugNameData(owner, undefined, computeFn),
|
||||
r => {
|
||||
if (!store) {
|
||||
store = new DisposableStore();
|
||||
} else {
|
||||
store.clear();
|
||||
}
|
||||
const result = computeFn(r);
|
||||
if (result) {
|
||||
store.add(result);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
undefined,
|
||||
() => {
|
||||
if (store) {
|
||||
store.dispose();
|
||||
store = undefined;
|
||||
}
|
||||
},
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export const enum DerivedState {
|
||||
/** Initial state, no previous value, recomputation needed */
|
||||
initial = 0,
|
||||
|
@ -475,12 +313,12 @@ export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObserv
|
|||
// IReader Implementation
|
||||
private _isReaderValid = false;
|
||||
|
||||
private _ensureNoRunning(): void {
|
||||
private _ensureReaderValid(): void {
|
||||
if (!this._isReaderValid) { throw new BugIndicatingError('The reader object cannot be used outside its compute function!'); }
|
||||
}
|
||||
|
||||
public readObservable<T>(observable: IObservable<T>): T {
|
||||
this._ensureNoRunning();
|
||||
this._ensureReaderValid();
|
||||
|
||||
// Subscribe before getting the value to enable caching
|
||||
observable.addObserver(this);
|
||||
|
@ -493,7 +331,7 @@ export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObserv
|
|||
}
|
||||
|
||||
public reportChange(change: TChange): void {
|
||||
this._ensureNoRunning();
|
||||
this._ensureReaderValid();
|
||||
|
||||
this._didReportChange = true;
|
||||
// TODO add logging
|
||||
|
@ -504,7 +342,7 @@ export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObserv
|
|||
|
||||
private _store: DisposableStore | undefined = undefined;
|
||||
get store(): DisposableStore {
|
||||
this._ensureNoRunning();
|
||||
this._ensureReaderValid();
|
||||
|
||||
if (this._store === undefined) {
|
||||
this._store = new DisposableStore();
|
||||
|
@ -514,7 +352,7 @@ export class Derived<T, TChangeSummary = any, TChange = void> extends BaseObserv
|
|||
|
||||
private _delayedStore: DisposableStore | undefined = undefined;
|
||||
get delayedStore(): DisposableStore {
|
||||
this._ensureNoRunning();
|
||||
this._ensureReaderValid();
|
||||
|
||||
if (this._delayedStore === undefined) {
|
||||
this._delayedStore = new DisposableStore();
|
|
@ -3,10 +3,12 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EqualityComparer } from './commonFacade/deps.js';
|
||||
import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js';
|
||||
import { DebugNameData } from './debugName.js';
|
||||
import { getLogger } from './logging/logging.js';
|
||||
import { EqualityComparer } from '../commonFacade/deps.js';
|
||||
import { IObserver, ISettableObservable, ITransaction } from '../base.js';
|
||||
import { TransactionImpl } from '../transaction.js';
|
||||
import { DebugNameData } from '../debugName.js';
|
||||
import { getLogger } from '../logging/logging.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
|
||||
/**
|
||||
* Holds off updating observers until the value is actually read.
|
|
@ -0,0 +1,166 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable, ITransaction } from '../base.js';
|
||||
import { subtransaction } from '../transaction.js';
|
||||
import { EqualityComparer, Event, IDisposable, strictEquals } from '../commonFacade/deps.js';
|
||||
import { DebugOwner, DebugNameData, IDebugNameData } from '../debugName.js';
|
||||
import { getLogger } from '../logging/logging.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
|
||||
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
owner: DebugOwner,
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent(...args:
|
||||
[owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any] |
|
||||
[event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
): IObservable<any> {
|
||||
let owner;
|
||||
let event;
|
||||
let getValue;
|
||||
if (args.length === 3) {
|
||||
[owner, event, getValue] = args;
|
||||
} else {
|
||||
[event, getValue] = args;
|
||||
}
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(owner, undefined, getValue),
|
||||
event,
|
||||
getValue,
|
||||
() => FromEventObservable.globalTransaction,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function observableFromEventOpts<T, TArgs = unknown>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
},
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T
|
||||
): IObservable<T> {
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue),
|
||||
event,
|
||||
getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
||||
public static globalTransaction: ITransaction | undefined;
|
||||
|
||||
private _value: T | undefined;
|
||||
private _hasValue = false;
|
||||
private _subscription: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
private readonly event: Event<TArgs>,
|
||||
public readonly _getValue: (args: TArgs | undefined) => T,
|
||||
private readonly _getTransaction: () => ITransaction | undefined,
|
||||
private readonly _equalityComparator: EqualityComparer<T>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getDebugName(): string | undefined {
|
||||
return this._debugNameData.getDebugName(this);
|
||||
}
|
||||
|
||||
public get debugName(): string {
|
||||
const name = this.getDebugName();
|
||||
return 'From Event' + (name ? `: ${name}` : '');
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this._subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = (args: TArgs | undefined) => {
|
||||
const newValue = this._getValue(args);
|
||||
const oldValue = this._value;
|
||||
|
||||
const didChange = !this._hasValue || !(this._equalityComparator(oldValue!, newValue));
|
||||
let didRunTransaction = false;
|
||||
|
||||
if (didChange) {
|
||||
this._value = newValue;
|
||||
|
||||
if (this._hasValue) {
|
||||
didRunTransaction = true;
|
||||
subtransaction(
|
||||
this._getTransaction(),
|
||||
(tx) => {
|
||||
getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue });
|
||||
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const name = this.getDebugName();
|
||||
return 'Event fired' + (name ? `: ${name}` : '');
|
||||
}
|
||||
);
|
||||
}
|
||||
this._hasValue = true;
|
||||
}
|
||||
|
||||
if (!didRunTransaction) {
|
||||
getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue });
|
||||
}
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this._subscription!.dispose();
|
||||
this._subscription = undefined;
|
||||
this._hasValue = false;
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
if (this._subscription) {
|
||||
if (!this._hasValue) {
|
||||
this.handleEvent(undefined);
|
||||
}
|
||||
return this._value!;
|
||||
} else {
|
||||
// no cache, as there are no subscribers to keep it updated
|
||||
const value = this._getValue(undefined);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public debugSetValue(value: unknown) {
|
||||
this._value = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace observableFromEvent {
|
||||
export const Observer = FromEventObservable;
|
||||
|
||||
export function batchEventsGlobally(tx: ITransaction, fn: () => void): void {
|
||||
let didSet = false;
|
||||
if (FromEventObservable.globalTransaction === undefined) {
|
||||
FromEventObservable.globalTransaction = tx;
|
||||
didSet = true;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
if (didSet) {
|
||||
FromEventObservable.globalTransaction = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservableWithChange, ITransaction } from '../base.js';
|
||||
import { transaction } from '../transaction.js';
|
||||
import { DebugNameData } from '../debugName.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
|
||||
/**
|
||||
* Creates a signal that can be triggered to invalidate observers.
|
||||
* Signals don't have a value - when they are triggered they indicate a change.
|
||||
* However, signals can carry a delta that is passed to observers.
|
||||
*/
|
||||
export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(debugNameOrOwner: string | object): IObservableSignal<TDelta> {
|
||||
if (typeof debugNameOrOwner === 'string') {
|
||||
return new ObservableSignal<TDelta>(debugNameOrOwner);
|
||||
} else {
|
||||
return new ObservableSignal<TDelta>(undefined, debugNameOrOwner);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IObservableSignal<TChange> extends IObservableWithChange<void, TChange> {
|
||||
trigger(tx: ITransaction | undefined, change: TChange): void;
|
||||
}
|
||||
|
||||
class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
|
||||
public get debugName() {
|
||||
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return this.debugName;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugName: string | undefined,
|
||||
private readonly _owner?: object
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public trigger(tx: ITransaction | undefined, change: TChange): void {
|
||||
if (!tx) {
|
||||
transaction(tx => {
|
||||
this.trigger(tx, change);
|
||||
}, () => `Trigger signal ${this.debugName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, change);
|
||||
}
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable } from '../base.js';
|
||||
import { transaction } from '../transaction.js';
|
||||
import { Event, IDisposable } from '../commonFacade/deps.js';
|
||||
import { DebugOwner, DebugNameData } from '../debugName.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
|
||||
export function observableSignalFromEvent(
|
||||
owner: DebugOwner | string,
|
||||
event: Event<any>
|
||||
): IObservable<void> {
|
||||
return new FromEventObservableSignal(typeof owner === 'string' ? owner : new DebugNameData(owner, undefined, undefined), event);
|
||||
}
|
||||
|
||||
class FromEventObservableSignal extends BaseObservable<void> {
|
||||
private subscription: IDisposable | undefined;
|
||||
|
||||
public readonly debugName: string;
|
||||
constructor(
|
||||
debugNameDataOrName: DebugNameData | string,
|
||||
private readonly event: Event<any>
|
||||
) {
|
||||
super();
|
||||
this.debugName = typeof debugNameDataOrName === 'string'
|
||||
? debugNameDataOrName
|
||||
: debugNameDataOrName.getDebugName(this) ?? 'Observable Signal From Event';
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this.subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = () => {
|
||||
transaction(
|
||||
(tx) => {
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => this.debugName
|
||||
);
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this.subscription!.dispose();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISettableObservable, ITransaction } from '../base.js';
|
||||
import { TransactionImpl } from '../transaction.js';
|
||||
import { BaseObservable } from './baseObservable.js';
|
||||
import { EqualityComparer, IDisposable, strictEquals } from '../commonFacade/deps.js';
|
||||
import { DebugNameData } from '../debugName.js';
|
||||
import { getLogger } from '../logging/logging.js';
|
||||
|
||||
/**
|
||||
* Creates an observable value.
|
||||
* Observers get informed when the value changes.
|
||||
* @template TChange An arbitrary type to describe how or why the value changed. Defaults to `void`.
|
||||
* Observers will receive every single change value.
|
||||
*/
|
||||
|
||||
export function observableValue<T, TChange = void>(name: string, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(owner: object, initialValue: T): ISettableObservable<T, TChange>;
|
||||
export function observableValue<T, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> {
|
||||
let debugNameData: 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<T, TChange = void>
|
||||
extends BaseObservable<T, TChange>
|
||||
implements ISettableObservable<T, TChange> {
|
||||
protected _value: T;
|
||||
|
||||
get debugName() {
|
||||
return this._debugNameData.getDebugName(this) ?? 'ObservableValue';
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
initialValue: T,
|
||||
private readonly _equalityComparator: EqualityComparer<T>
|
||||
) {
|
||||
super();
|
||||
this._value = initialValue;
|
||||
|
||||
getLogger()?.handleObservableUpdated(this, { hadValue: false, newValue: initialValue, change: undefined, didChange: true, oldValue: undefined });
|
||||
}
|
||||
public override get(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public set(value: T, tx: ITransaction | undefined, change: TChange): void {
|
||||
if (change === undefined && this._equalityComparator(this._value, value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _tx: TransactionImpl | undefined;
|
||||
if (!tx) {
|
||||
tx = _tx = new TransactionImpl(() => { }, () => `Setting ${this.debugName}`);
|
||||
}
|
||||
try {
|
||||
const oldValue = this._value;
|
||||
this._setValue(value);
|
||||
getLogger()?.handleObservableUpdated(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `${this.debugName}: ${this._value}`;
|
||||
}
|
||||
|
||||
protected _setValue(newValue: T): void {
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
public debugGetState() {
|
||||
return {
|
||||
value: this._value,
|
||||
};
|
||||
}
|
||||
|
||||
public debugSetValue(value: unknown) {
|
||||
this._value = value as T;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A disposable observable. When disposed, its value is also disposed.
|
||||
* When a new value is set, the previous value is disposed.
|
||||
*/
|
||||
|
||||
export function disposableObservableValue<T extends IDisposable | undefined, TChange = void>(nameOrOwner: string | object, initialValue: T): ISettableObservable<T, TChange> & IDisposable {
|
||||
let debugNameData: 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<T extends IDisposable | undefined, TChange = void> extends ObservableValue<T, TChange> implements IDisposable {
|
||||
protected override _setValue(newValue: T): void {
|
||||
if (this._value === newValue) {
|
||||
return;
|
||||
}
|
||||
if (this._value) {
|
||||
this._value.dispose();
|
||||
}
|
||||
this._value = newValue;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._value?.dispose();
|
||||
}
|
||||
}
|
|
@ -3,9 +3,10 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ISettableObservable, ObservableValue } from './base.js';
|
||||
import { DebugNameData, IDebugNameData } from './debugName.js';
|
||||
import { EqualityComparer, strictEquals } from './commonFacade/deps.js';
|
||||
import { ISettableObservable } from '../base.js';
|
||||
import { DebugNameData, IDebugNameData } from '../debugName.js';
|
||||
import { EqualityComparer, strictEquals } from '../commonFacade/deps.js';
|
||||
import { ObservableValue } from './observableValue.js';
|
||||
import { LazyObservableValue } from './lazyObservableValue.js';
|
||||
|
||||
export function observableValueOpts<T, TChange = void>(
|
|
@ -0,0 +1,151 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IReaderWithStore, IReader, IObservable } from '../base.js';
|
||||
import { IChangeTracker } from '../changeTracker.js';
|
||||
import { DisposableStore, IDisposable, toDisposable } from '../commonFacade/deps.js';
|
||||
import { DebugNameData, IDebugNameData } from '../debugName.js';
|
||||
import { AutorunObserver } from './autorunImpl.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: (reader: IReaderWithStore) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(undefined, undefined, fn),
|
||||
fn,
|
||||
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: IDebugNameData & {}, fn: (reader: IReaderWithStore) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
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 `changeTracker.createChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `changeTracker.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<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary) => void
|
||||
): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
options.changeTracker
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
|
||||
*/
|
||||
export function autorunWithStoreHandleChanges<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void
|
||||
): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = autorunHandleChanges(
|
||||
{
|
||||
owner: options.owner,
|
||||
debugName: options.debugName,
|
||||
debugReferenceFn: options.debugReferenceFn ?? fn,
|
||||
changeTracker: options.changeTracker,
|
||||
},
|
||||
(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)
|
||||
*
|
||||
* @deprecated Use `autorun(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable {
|
||||
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<T>(
|
||||
observable: IObservable<T>,
|
||||
handler: (args: { lastValue: T | undefined; newValue: T }) => void
|
||||
): IDisposable {
|
||||
let _lastValue: T | undefined;
|
||||
return autorunOpts({ debugReferenceFn: handler }, (reader) => {
|
||||
const newValue = observable.read(reader);
|
||||
const lastValue = _lastValue;
|
||||
_lastValue = newValue;
|
||||
handler({ lastValue, newValue });
|
||||
});
|
||||
}
|
||||
|
||||
export function autorunIterableDelta<T>(
|
||||
getValue: (reader: IReader) => Iterable<T>,
|
||||
handler: (args: { addedValues: T[]; removedValues: T[] }) => void,
|
||||
getUniqueIdentifier: (value: T) => unknown = v => v
|
||||
) {
|
||||
const lastValues = new Map<unknown, T>();
|
||||
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()] });
|
||||
}
|
||||
});
|
||||
}
|
|
@ -3,152 +3,11 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable, IObservableWithChange, IObserver, IReader, IReaderWithStore } from './base.js';
|
||||
import { DebugNameData, IDebugNameData } from './debugName.js';
|
||||
import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js';
|
||||
import { getLogger } from './logging/logging.js';
|
||||
import { IChangeTracker } from './changeTracker.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: (reader: IReaderWithStore) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(undefined, undefined, fn),
|
||||
fn,
|
||||
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: IDebugNameData & {}, fn: (reader: IReaderWithStore) => void): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
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 `changeTracker.createChangeSummary` to create a "change summary" that can collect the changes.
|
||||
* Use `changeTracker.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<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary) => void
|
||||
): IDisposable {
|
||||
return new AutorunObserver(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn),
|
||||
fn,
|
||||
options.changeTracker,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see autorunHandleChanges (but with a disposable store that is cleared before the next run or on dispose)
|
||||
*/
|
||||
export function autorunWithStoreHandleChanges<TChangeSummary>(
|
||||
options: IDebugNameData & {
|
||||
changeTracker: IChangeTracker<TChangeSummary>;
|
||||
},
|
||||
fn: (reader: IReader, changeSummary: TChangeSummary, store: DisposableStore) => void
|
||||
): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = autorunHandleChanges(
|
||||
{
|
||||
owner: options.owner,
|
||||
debugName: options.debugName,
|
||||
debugReferenceFn: options.debugReferenceFn ?? fn,
|
||||
changeTracker: options.changeTracker,
|
||||
},
|
||||
(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)
|
||||
*
|
||||
* @deprecated Use `autorun(reader => { reader.store.add(...) })` instead!
|
||||
*/
|
||||
export function autorunWithStore(fn: (reader: IReader, store: DisposableStore) => void): IDisposable {
|
||||
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<T>(
|
||||
observable: IObservable<T>,
|
||||
handler: (args: { lastValue: T | undefined; newValue: T }) => void
|
||||
): IDisposable {
|
||||
let _lastValue: T | undefined;
|
||||
return autorunOpts({ debugReferenceFn: handler }, (reader) => {
|
||||
const newValue = observable.read(reader);
|
||||
const lastValue = _lastValue;
|
||||
_lastValue = newValue;
|
||||
handler({ lastValue, newValue });
|
||||
});
|
||||
}
|
||||
|
||||
export function autorunIterableDelta<T>(
|
||||
getValue: (reader: IReader) => Iterable<T>,
|
||||
handler: (args: { addedValues: T[]; removedValues: T[] }) => void,
|
||||
getUniqueIdentifier: (value: T) => unknown = v => v,
|
||||
) {
|
||||
const lastValues = new Map<unknown, T>();
|
||||
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()] });
|
||||
}
|
||||
});
|
||||
}
|
||||
import { IObservable, IObservableWithChange, IObserver, IReaderWithStore } from '../base.js';
|
||||
import { DebugNameData } from '../debugName.js';
|
||||
import { assertFn, BugIndicatingError, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, trackDisposable } from '../commonFacade/deps.js';
|
||||
import { getLogger } from '../logging/logging.js';
|
||||
import { IChangeTracker } from '../changeTracker.js';
|
||||
|
||||
export const enum AutorunState {
|
||||
/**
|
||||
|
@ -391,7 +250,3 @@ export class AutorunObserver<TChangeSummary = any> implements IObserver, IReader
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace autorun {
|
||||
export const Observer = AutorunObserver;
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { handleBugIndicatingErrorRecovery, IObservable, IObserver, ITransaction } from './base.js';
|
||||
import { getFunctionName } from './debugName.js';
|
||||
import { getLogger } from './logging/logging.js';
|
||||
|
||||
/**
|
||||
* 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: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
let _globalTransaction: ITransaction | undefined = undefined;
|
||||
|
||||
export function globalTransaction(fn: (tx: ITransaction) => void) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @deprecated */
|
||||
|
||||
export async function asyncTransaction(fn: (tx: ITransaction) => Promise<void>, getDebugName?: () => string): Promise<void> {
|
||||
const tx = new TransactionImpl(fn, getDebugName);
|
||||
try {
|
||||
await fn(tx);
|
||||
} finally {
|
||||
tx.finish();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Allows to chain transactions.
|
||||
*/
|
||||
|
||||
export function subtransaction(tx: ITransaction | undefined, fn: (tx: ITransaction) => void, getDebugName?: () => string): void {
|
||||
if (!tx) {
|
||||
transaction(fn, getDebugName);
|
||||
} else {
|
||||
fn(tx);
|
||||
}
|
||||
} export class TransactionImpl implements ITransaction {
|
||||
private _updatingObservers: { observer: IObserver; observable: IObservable<any> }[] | null = [];
|
||||
|
||||
constructor(public readonly _fn: Function, private readonly _getDebugName?: () => string) {
|
||||
getLogger()?.handleBeginTransaction(this);
|
||||
}
|
||||
|
||||
public getDebugName(): string | undefined {
|
||||
if (this._getDebugName) {
|
||||
return this._getDebugName();
|
||||
}
|
||||
return getFunctionName(this._fn);
|
||||
}
|
||||
|
||||
public updateObserver(observer: IObserver, observable: IObservable<any>): void {
|
||||
if (!this._updatingObservers) {
|
||||
// This happens when a transaction is used in a callback or async function.
|
||||
// If an async transaction is used, make sure the promise awaits all users of the transaction (e.g. no race).
|
||||
handleBugIndicatingErrorRecovery('Transaction already finished!');
|
||||
// Error recovery
|
||||
transaction(tx => {
|
||||
tx.updateObserver(observer, observable);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// When this gets called while finish is active, they will still get considered
|
||||
this._updatingObservers.push({ observer, observable });
|
||||
observer.beginUpdate(observable);
|
||||
}
|
||||
|
||||
public finish(): void {
|
||||
const updatingObservers = this._updatingObservers;
|
||||
if (!updatingObservers) {
|
||||
handleBugIndicatingErrorRecovery('transaction.finish() has already been called!');
|
||||
return;
|
||||
}
|
||||
|
||||
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(this);
|
||||
}
|
||||
|
||||
public debugGetUpdatingObservers() {
|
||||
return this._updatingObservers;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,692 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js';
|
||||
import { BaseObservable, ConvenientObservable, IObservable, IObservableWithChange, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js';
|
||||
import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js';
|
||||
import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js';
|
||||
import { derived, derivedOpts } from './derived.js';
|
||||
import { getLogger } from './logging/logging.js';
|
||||
import { CancellationToken, cancelOnDispose } from '../cancellation.js';
|
||||
|
||||
/**
|
||||
* Represents an efficient observable whose value never changes.
|
||||
*/
|
||||
export function constObservable<T>(value: T): IObservable<T> {
|
||||
return new ConstObservable(value);
|
||||
}
|
||||
|
||||
class ConstObservable<T> extends ConvenientObservable<T, void> {
|
||||
constructor(private readonly value: T) {
|
||||
super();
|
||||
}
|
||||
|
||||
public override get debugName(): string {
|
||||
return this.toString();
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
return this.value;
|
||||
}
|
||||
public addObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
public removeObserver(observer: IObserver): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
override log(): IObservableWithChange<T, void> {
|
||||
return this;
|
||||
}
|
||||
|
||||
override toString(): string {
|
||||
return `Const: ${this.value}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
|
||||
const observable = observableValue<{ value?: T }>('promiseValue', {});
|
||||
promise.then((value) => {
|
||||
observable.set({ value }, undefined);
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
owner: DebugOwner,
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent<T, TArgs = unknown>(
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T>;
|
||||
export function observableFromEvent(...args:
|
||||
[owner: DebugOwner, event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
| [event: Event<any>, getValue: (args: any | undefined) => any]
|
||||
): IObservable<any> {
|
||||
let owner;
|
||||
let event;
|
||||
let getValue;
|
||||
if (args.length === 3) {
|
||||
[owner, event, getValue] = args;
|
||||
} else {
|
||||
[event, getValue] = args;
|
||||
}
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(owner, undefined, getValue),
|
||||
event,
|
||||
getValue,
|
||||
() => FromEventObservable.globalTransaction,
|
||||
strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export function observableFromEventOpts<T, TArgs = unknown>(
|
||||
options: IDebugNameData & {
|
||||
equalsFn?: EqualityComparer<T>;
|
||||
},
|
||||
event: Event<TArgs>,
|
||||
getValue: (args: TArgs | undefined) => T,
|
||||
): IObservable<T> {
|
||||
return new FromEventObservable(
|
||||
new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? getValue),
|
||||
event,
|
||||
getValue, () => FromEventObservable.globalTransaction, options.equalsFn ?? strictEquals
|
||||
);
|
||||
}
|
||||
|
||||
export class FromEventObservable<TArgs, T> extends BaseObservable<T> {
|
||||
public static globalTransaction: ITransaction | undefined;
|
||||
|
||||
private _value: T | undefined;
|
||||
private _hasValue = false;
|
||||
private _subscription: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _debugNameData: DebugNameData,
|
||||
private readonly event: Event<TArgs>,
|
||||
public readonly _getValue: (args: TArgs | undefined) => T,
|
||||
private readonly _getTransaction: () => ITransaction | undefined,
|
||||
private readonly _equalityComparator: EqualityComparer<T>
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
private getDebugName(): string | undefined {
|
||||
return this._debugNameData.getDebugName(this);
|
||||
}
|
||||
|
||||
public get debugName(): string {
|
||||
const name = this.getDebugName();
|
||||
return 'From Event' + (name ? `: ${name}` : '');
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this._subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = (args: TArgs | undefined) => {
|
||||
const newValue = this._getValue(args);
|
||||
const oldValue = this._value;
|
||||
|
||||
const didChange = !this._hasValue || !(this._equalityComparator(oldValue!, newValue));
|
||||
let didRunTransaction = false;
|
||||
|
||||
if (didChange) {
|
||||
this._value = newValue;
|
||||
|
||||
if (this._hasValue) {
|
||||
didRunTransaction = true;
|
||||
subtransaction(
|
||||
this._getTransaction(),
|
||||
(tx) => {
|
||||
getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue });
|
||||
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
const name = this.getDebugName();
|
||||
return 'Event fired' + (name ? `: ${name}` : '');
|
||||
}
|
||||
);
|
||||
}
|
||||
this._hasValue = true;
|
||||
}
|
||||
|
||||
if (!didRunTransaction) {
|
||||
getLogger()?.handleObservableUpdated(this, { oldValue, newValue, change: undefined, didChange, hadValue: this._hasValue });
|
||||
}
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this._subscription!.dispose();
|
||||
this._subscription = undefined;
|
||||
this._hasValue = false;
|
||||
this._value = undefined;
|
||||
}
|
||||
|
||||
public get(): T {
|
||||
if (this._subscription) {
|
||||
if (!this._hasValue) {
|
||||
this.handleEvent(undefined);
|
||||
}
|
||||
return this._value!;
|
||||
} else {
|
||||
// no cache, as there are no subscribers to keep it updated
|
||||
const value = this._getValue(undefined);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public debugSetValue(value: unknown) {
|
||||
this._value = value as any;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace observableFromEvent {
|
||||
export const Observer = FromEventObservable;
|
||||
|
||||
export function batchEventsGlobally(tx: ITransaction, fn: () => void): void {
|
||||
let didSet = false;
|
||||
if (FromEventObservable.globalTransaction === undefined) {
|
||||
FromEventObservable.globalTransaction = tx;
|
||||
didSet = true;
|
||||
}
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
if (didSet) {
|
||||
FromEventObservable.globalTransaction = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function observableSignalFromEvent(
|
||||
owner: DebugOwner | string,
|
||||
event: Event<any>
|
||||
): IObservable<void> {
|
||||
return new FromEventObservableSignal(typeof owner === 'string' ? owner : new DebugNameData(owner, undefined, undefined), event);
|
||||
}
|
||||
|
||||
class FromEventObservableSignal extends BaseObservable<void> {
|
||||
private subscription: IDisposable | undefined;
|
||||
|
||||
public readonly debugName: string;
|
||||
constructor(
|
||||
debugNameDataOrName: DebugNameData | string,
|
||||
private readonly event: Event<any>,
|
||||
) {
|
||||
super();
|
||||
this.debugName = typeof debugNameDataOrName === 'string'
|
||||
? debugNameDataOrName
|
||||
: debugNameDataOrName.getDebugName(this) ?? 'Observable Signal From Event';
|
||||
}
|
||||
|
||||
protected override onFirstObserverAdded(): void {
|
||||
this.subscription = this.event(this.handleEvent);
|
||||
}
|
||||
|
||||
private readonly handleEvent = () => {
|
||||
transaction(
|
||||
(tx) => {
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, undefined);
|
||||
}
|
||||
},
|
||||
() => this.debugName
|
||||
);
|
||||
};
|
||||
|
||||
protected override onLastObserverRemoved(): void {
|
||||
this.subscription!.dispose();
|
||||
this.subscription = undefined;
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a signal that can be triggered to invalidate observers.
|
||||
* Signals don't have a value - when they are triggered they indicate a change.
|
||||
* However, signals can carry a delta that is passed to observers.
|
||||
*/
|
||||
export function observableSignal<TDelta = void>(debugName: string): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(owner: object): IObservableSignal<TDelta>;
|
||||
export function observableSignal<TDelta = void>(debugNameOrOwner: string | object): IObservableSignal<TDelta> {
|
||||
if (typeof debugNameOrOwner === 'string') {
|
||||
return new ObservableSignal<TDelta>(debugNameOrOwner);
|
||||
} else {
|
||||
return new ObservableSignal<TDelta>(undefined, debugNameOrOwner);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IObservableSignal<TChange> extends IObservableWithChange<void, TChange> {
|
||||
trigger(tx: ITransaction | undefined, change: TChange): void;
|
||||
}
|
||||
|
||||
class ObservableSignal<TChange> extends BaseObservable<void, TChange> implements IObservableSignal<TChange> {
|
||||
public get debugName() {
|
||||
return new DebugNameData(this._owner, this._debugName, undefined).getDebugName(this) ?? 'Observable Signal';
|
||||
}
|
||||
|
||||
public override toString(): string {
|
||||
return this.debugName;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _debugName: string | undefined,
|
||||
private readonly _owner?: object,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public trigger(tx: ITransaction | undefined, change: TChange): void {
|
||||
if (!tx) {
|
||||
transaction(tx => {
|
||||
this.trigger(tx, change);
|
||||
}, () => `Trigger signal ${this.debugName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const o of this._observers) {
|
||||
tx.updateObserver(o, this);
|
||||
o.handleChange(this, change);
|
||||
}
|
||||
}
|
||||
|
||||
public override get(): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
export function signalFromObservable<T>(owner: DebugOwner | undefined, observable: IObservable<T>): IObservable<void> {
|
||||
return derivedOpts({
|
||||
owner,
|
||||
equalsFn: () => false,
|
||||
}, reader => {
|
||||
observable.read(reader);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `debouncedObservable` instead.
|
||||
*/
|
||||
export function debouncedObservableDeprecated<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T | undefined> {
|
||||
const debouncedObservable = observableValue<T | undefined>('debounced', undefined);
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
disposableStore.add(autorun(reader => {
|
||||
/** @description debounce */
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
transaction(tx => {
|
||||
debouncedObservable.set(value, tx);
|
||||
});
|
||||
}, debounceMs);
|
||||
|
||||
}));
|
||||
|
||||
return debouncedObservable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that debounces the input observable.
|
||||
*/
|
||||
export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number): IObservable<T> {
|
||||
let hasValue = false;
|
||||
let lastValue: T | undefined;
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
return observableFromEvent<T, void>(cb => {
|
||||
const d = autorun(reader => {
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (!hasValue) {
|
||||
hasValue = true;
|
||||
lastValue = value;
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
lastValue = value;
|
||||
cb();
|
||||
}, debounceMs);
|
||||
}
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
d.dispose();
|
||||
hasValue = false;
|
||||
lastValue = undefined;
|
||||
},
|
||||
};
|
||||
}, () => {
|
||||
if (hasValue) {
|
||||
return lastValue!;
|
||||
} else {
|
||||
return observable.get();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
|
||||
const observable = observableValue('triggeredRecently', false);
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
disposableStore.add(event(() => {
|
||||
observable.set(true, undefined);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
observable.set(false, undefined);
|
||||
}, timeoutMs);
|
||||
}));
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes sure the observable is being observed and keeps its cache alive.
|
||||
*/
|
||||
export function keepObserved<T>(observable: IObservable<T>): IDisposable {
|
||||
const o = new KeepAliveObserver(false, undefined);
|
||||
observable.addObserver(o);
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setKeepObserved(keepObserved);
|
||||
|
||||
/**
|
||||
* This converts the given observable into an autorun.
|
||||
*/
|
||||
export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>, handleValue?: (value: T) => void): IDisposable {
|
||||
const o = new KeepAliveObserver(true, handleValue);
|
||||
observable.addObserver(o);
|
||||
try {
|
||||
o.beginUpdate(observable);
|
||||
} finally {
|
||||
o.endUpdate(observable);
|
||||
}
|
||||
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
|
||||
|
||||
export class KeepAliveObserver implements IObserver {
|
||||
private _counter = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _forceRecompute: boolean,
|
||||
private readonly _handleValue: ((value: any) => void) | undefined,
|
||||
) { }
|
||||
|
||||
beginUpdate<T>(observable: IObservable<T>): void {
|
||||
this._counter++;
|
||||
}
|
||||
|
||||
endUpdate<T>(observable: IObservable<T>): void {
|
||||
if (this._counter === 1 && this._forceRecompute) {
|
||||
if (this._handleValue) {
|
||||
this._handleValue(observable.get());
|
||||
} else {
|
||||
observable.reportChanges();
|
||||
}
|
||||
}
|
||||
this._counter--;
|
||||
}
|
||||
|
||||
handlePossibleChange<T>(observable: IObservable<T>): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T>
|
||||
& { clearCache(transaction: ITransaction): void; setCache(newValue: T | undefined, tx: ITransaction | undefined): void } {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const onChange = observableSignal('derivedObservableWithWritableCache');
|
||||
const observable = derived(owner, reader => {
|
||||
onChange.read(reader);
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return Object.assign(observable, {
|
||||
clearCache: (tx: ITransaction) => {
|
||||
lastValue = undefined;
|
||||
onChange.trigger(tx);
|
||||
},
|
||||
setCache: (newValue: T | undefined, tx: ITransaction | undefined) => {
|
||||
lastValue = newValue;
|
||||
onChange.trigger(tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the items array changes, referential equal items are not mapped again.
|
||||
*/
|
||||
export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
|
||||
let m = new ArrayMap(map, keySelector);
|
||||
const self = derivedOpts({
|
||||
debugReferenceFn: map,
|
||||
owner,
|
||||
onLastObserverRemoved: () => {
|
||||
m.dispose();
|
||||
m = new ArrayMap(map);
|
||||
}
|
||||
}, (reader) => {
|
||||
m.setItems(items.read(reader));
|
||||
return m.getItems();
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
class ArrayMap<TIn, TOut, TKey> implements IDisposable {
|
||||
private readonly _cache = new Map<TKey, { out: TOut; store: DisposableStore }>();
|
||||
private _items: TOut[] = [];
|
||||
constructor(
|
||||
private readonly _map: (input: TIn, store: DisposableStore) => TOut,
|
||||
private readonly _keySelector?: (input: TIn) => TKey,
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._cache.forEach(entry => entry.store.dispose());
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
public setItems(items: readonly TIn[]): void {
|
||||
const newItems: TOut[] = [];
|
||||
const itemsToRemove = new Set(this._cache.keys());
|
||||
|
||||
for (const item of items) {
|
||||
const key = this._keySelector ? this._keySelector(item) : item as unknown as TKey;
|
||||
|
||||
let entry = this._cache.get(key);
|
||||
if (!entry) {
|
||||
const store = new DisposableStore();
|
||||
const out = this._map(item, store);
|
||||
entry = { out, store };
|
||||
this._cache.set(key, entry);
|
||||
} else {
|
||||
itemsToRemove.delete(key);
|
||||
}
|
||||
newItems.push(entry.out);
|
||||
}
|
||||
|
||||
for (const item of itemsToRemove) {
|
||||
const entry = this._cache.get(item)!;
|
||||
entry.store.dispose();
|
||||
this._cache.delete(item);
|
||||
}
|
||||
|
||||
this._items = newItems;
|
||||
}
|
||||
|
||||
public getItems(): TOut[] {
|
||||
return this._items;
|
||||
}
|
||||
}
|
||||
|
||||
export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEvent<T> {
|
||||
constructor(public readonly observable: IObservable<T>) {
|
||||
}
|
||||
|
||||
get onDidChange(): Event<void> {
|
||||
return Event.fromObservableLight(this.observable);
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this.observable.get();
|
||||
}
|
||||
}
|
||||
|
||||
export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
|
||||
if (value instanceof ValueWithChangeEventFromObservable) {
|
||||
return value.observable;
|
||||
}
|
||||
return observableFromEvent(owner, value.onDidChange, () => value.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that has the latest changed value of the given observables.
|
||||
* Initially (and when not observed), it has the value of the last observable.
|
||||
* When observed and any of the observables change, it has the value of the last changed observable.
|
||||
* If multiple observables change in the same transaction, the last observable wins.
|
||||
*/
|
||||
export function latestChangedValue<T extends IObservable<any>[]>(owner: DebugOwner, observables: T): IObservable<ReturnType<T[number]['get']>> {
|
||||
if (observables.length === 0) {
|
||||
throw new BugIndicatingError();
|
||||
}
|
||||
|
||||
let hasLastChangedValue = false;
|
||||
let lastChangedValue: any = undefined;
|
||||
|
||||
const result = observableFromEvent<any, void>(owner, cb => {
|
||||
const store = new DisposableStore();
|
||||
for (const o of observables) {
|
||||
store.add(autorunOpts({ debugName: () => getDebugName(result, new DebugNameData(owner, undefined, undefined)) + '.updateLastChangedValue' }, reader => {
|
||||
hasLastChangedValue = true;
|
||||
lastChangedValue = o.read(reader);
|
||||
cb();
|
||||
}));
|
||||
}
|
||||
store.add({
|
||||
dispose() {
|
||||
hasLastChangedValue = false;
|
||||
lastChangedValue = undefined;
|
||||
},
|
||||
});
|
||||
return store;
|
||||
}, () => {
|
||||
if (hasLastChangedValue) {
|
||||
return lastChangedValue;
|
||||
} else {
|
||||
return observables[observables.length - 1].get();
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Works like a derived.
|
||||
* However, if the value is not undefined, it is cached and will not be recomputed anymore.
|
||||
* In that case, the derived will unsubscribe from its dependencies.
|
||||
*/
|
||||
export function derivedConstOnceDefined<T>(owner: DebugOwner, fn: (reader: IReader) => T): IObservable<T | undefined> {
|
||||
return derivedObservableWithCache<T | undefined>(owner, (reader, lastValue) => lastValue ?? fn(reader));
|
||||
}
|
||||
|
||||
|
||||
export type RemoveUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
export function runOnChange<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[]) => void): IDisposable {
|
||||
let _previousValue: T | undefined;
|
||||
let _firstRun = true;
|
||||
return autorunWithStoreHandleChanges({
|
||||
changeTracker: {
|
||||
createChangeSummary: () => ({ deltas: [] as RemoveUndefined<TChange>[], didChange: false }),
|
||||
handleChange: (context, changeSummary) => {
|
||||
if (context.didChange(observable)) {
|
||||
const e = context.change;
|
||||
if (e !== undefined) {
|
||||
changeSummary.deltas.push(e as RemoveUndefined<TChange>);
|
||||
}
|
||||
changeSummary.didChange = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}, (reader, changeSummary) => {
|
||||
const value = observable.read(reader);
|
||||
const previousValue = _previousValue;
|
||||
if (changeSummary.didChange) {
|
||||
_previousValue = value;
|
||||
// didChange can never be true on the first autorun, so we know previousValue is defined
|
||||
cb(value, previousValue!, changeSummary.deltas);
|
||||
}
|
||||
if (_firstRun) {
|
||||
_firstRun = false;
|
||||
_previousValue = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function runOnChangeWithStore<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], store: DisposableStore) => void): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = runOnChange(observable, (value, previousValue: T, deltas) => {
|
||||
store.clear();
|
||||
cb(value, previousValue, deltas, store);
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
disposable.dispose();
|
||||
store.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function runOnChangeWithCancellationToken<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], token: CancellationToken) => Promise<void>): IDisposable {
|
||||
return runOnChangeWithStore(observable, (value, previousValue, deltas, store) => {
|
||||
cb(value, previousValue, deltas, cancelOnDispose(store));
|
||||
});
|
||||
}
|
|
@ -2,8 +2,10 @@
|
|||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { IObservable, observableValue, transaction } from './base.js';
|
||||
import { derived } from './derived.js';
|
||||
import { IObservable } from '../base.js';
|
||||
import { transaction } from '../transaction.js';
|
||||
import { derived } from '../observables/derived.js';
|
||||
import { observableValue } from '../observables/observableValue.js';
|
||||
|
||||
export class ObservableLazy<T> {
|
||||
private readonly _value = observableValue<T | undefined>(this, undefined);
|
||||
|
@ -21,7 +23,7 @@ export class ObservableLazy<T> {
|
|||
* Returns the cached value.
|
||||
* Computes the value if the value has not been cached yet.
|
||||
*/
|
||||
public getValue() {
|
||||
public getValue(): T {
|
||||
let v = this._value.get();
|
||||
if (!v) {
|
||||
v = this._computeValue();
|
|
@ -0,0 +1,63 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservableWithChange } from '../base.js';
|
||||
import { CancellationToken, cancelOnDispose } from '../commonFacade/cancellation.js';
|
||||
import { DisposableStore, IDisposable } from '../commonFacade/deps.js';
|
||||
import { autorunWithStoreHandleChanges } from '../reactions/autorun.js';
|
||||
|
||||
export type RemoveUndefined<T> = T extends undefined ? never : T;
|
||||
|
||||
export function runOnChange<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[]) => void): IDisposable {
|
||||
let _previousValue: T | undefined;
|
||||
let _firstRun = true;
|
||||
return autorunWithStoreHandleChanges({
|
||||
changeTracker: {
|
||||
createChangeSummary: () => ({ deltas: [] as RemoveUndefined<TChange>[], didChange: false }),
|
||||
handleChange: (context, changeSummary) => {
|
||||
if (context.didChange(observable)) {
|
||||
const e = context.change;
|
||||
if (e !== undefined) {
|
||||
changeSummary.deltas.push(e as RemoveUndefined<TChange>);
|
||||
}
|
||||
changeSummary.didChange = true;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}, (reader, changeSummary) => {
|
||||
const value = observable.read(reader);
|
||||
const previousValue = _previousValue;
|
||||
if (changeSummary.didChange) {
|
||||
_previousValue = value;
|
||||
// didChange can never be true on the first autorun, so we know previousValue is defined
|
||||
cb(value, previousValue!, changeSummary.deltas);
|
||||
}
|
||||
if (_firstRun) {
|
||||
_firstRun = false;
|
||||
_previousValue = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function runOnChangeWithStore<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], store: DisposableStore) => void): IDisposable {
|
||||
const store = new DisposableStore();
|
||||
const disposable = runOnChange(observable, (value, previousValue: T, deltas) => {
|
||||
store.clear();
|
||||
cb(value, previousValue, deltas, store);
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
disposable.dispose();
|
||||
store.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function runOnChangeWithCancellationToken<T, TChange>(observable: IObservableWithChange<T, TChange>, cb: (value: T, previousValue: T, deltas: RemoveUndefined<TChange>[], token: CancellationToken) => Promise<void>): IDisposable {
|
||||
return runOnChangeWithStore(observable, (value, previousValue, deltas, store) => {
|
||||
cb(value, previousValue, deltas, cancelOnDispose(store));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { autorun } from '../reactions/autorun.js';
|
||||
import { IObservable, IObservableWithChange, IObserver, IReader, ITransaction } from '../base.js';
|
||||
import { transaction } from '../transaction.js';
|
||||
import { observableValue } from '../observables/observableValue.js';
|
||||
import { DebugOwner } from '../debugName.js';
|
||||
import { DisposableStore, Event, IDisposable, toDisposable } from '../commonFacade/deps.js';
|
||||
import { derived, derivedOpts } from '../observables/derived.js';
|
||||
import { observableFromEvent } from '../observables/observableFromEvent.js';
|
||||
import { observableSignal } from '../observables/observableSignal.js';
|
||||
import { _setKeepObserved, _setRecomputeInitiallyAndOnChange } from '../observables/baseObservable.js';
|
||||
|
||||
export function observableFromPromise<T>(promise: Promise<T>): IObservable<{ value?: T }> {
|
||||
const observable = observableValue<{ value?: T }>('promiseValue', {});
|
||||
promise.then((value) => {
|
||||
observable.set({ value }, undefined);
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function signalFromObservable<T>(owner: DebugOwner | undefined, observable: IObservable<T>): IObservable<void> {
|
||||
return derivedOpts({
|
||||
owner,
|
||||
equalsFn: () => false,
|
||||
}, reader => {
|
||||
observable.read(reader);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `debouncedObservable` instead.
|
||||
*/
|
||||
export function debouncedObservableDeprecated<T>(observable: IObservable<T>, debounceMs: number, disposableStore: DisposableStore): IObservable<T | undefined> {
|
||||
const debouncedObservable = observableValue<T | undefined>('debounced', undefined);
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
disposableStore.add(autorun(reader => {
|
||||
/** @description debounce */
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
transaction(tx => {
|
||||
debouncedObservable.set(value, tx);
|
||||
});
|
||||
}, debounceMs);
|
||||
|
||||
}));
|
||||
|
||||
return debouncedObservable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an observable that debounces the input observable.
|
||||
*/
|
||||
export function debouncedObservable<T>(observable: IObservable<T>, debounceMs: number): IObservable<T> {
|
||||
let hasValue = false;
|
||||
let lastValue: T | undefined;
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
return observableFromEvent<T, void>(cb => {
|
||||
const d = autorun(reader => {
|
||||
const value = observable.read(reader);
|
||||
|
||||
if (!hasValue) {
|
||||
hasValue = true;
|
||||
lastValue = value;
|
||||
} else {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
lastValue = value;
|
||||
cb();
|
||||
}, debounceMs);
|
||||
}
|
||||
});
|
||||
return {
|
||||
dispose() {
|
||||
d.dispose();
|
||||
hasValue = false;
|
||||
lastValue = undefined;
|
||||
},
|
||||
};
|
||||
}, () => {
|
||||
if (hasValue) {
|
||||
return lastValue!;
|
||||
} else {
|
||||
return observable.get();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function wasEventTriggeredRecently(event: Event<any>, timeoutMs: number, disposableStore: DisposableStore): IObservable<boolean> {
|
||||
const observable = observableValue('triggeredRecently', false);
|
||||
|
||||
let timeout: Timeout | undefined = undefined;
|
||||
|
||||
disposableStore.add(event(() => {
|
||||
observable.set(true, undefined);
|
||||
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(() => {
|
||||
observable.set(false, undefined);
|
||||
}, timeoutMs);
|
||||
}));
|
||||
|
||||
return observable;
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes sure the observable is being observed and keeps its cache alive.
|
||||
*/
|
||||
export function keepObserved<T>(observable: IObservable<T>): IDisposable {
|
||||
const o = new KeepAliveObserver(false, undefined);
|
||||
observable.addObserver(o);
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setKeepObserved(keepObserved);
|
||||
|
||||
/**
|
||||
* This converts the given observable into an autorun.
|
||||
*/
|
||||
export function recomputeInitiallyAndOnChange<T>(observable: IObservable<T>, handleValue?: (value: T) => void): IDisposable {
|
||||
const o = new KeepAliveObserver(true, handleValue);
|
||||
observable.addObserver(o);
|
||||
try {
|
||||
o.beginUpdate(observable);
|
||||
} finally {
|
||||
o.endUpdate(observable);
|
||||
}
|
||||
|
||||
return toDisposable(() => {
|
||||
observable.removeObserver(o);
|
||||
});
|
||||
}
|
||||
|
||||
_setRecomputeInitiallyAndOnChange(recomputeInitiallyAndOnChange);
|
||||
|
||||
export class KeepAliveObserver implements IObserver {
|
||||
private _counter = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _forceRecompute: boolean,
|
||||
private readonly _handleValue: ((value: any) => void) | undefined,
|
||||
) { }
|
||||
|
||||
beginUpdate<T>(observable: IObservable<T>): void {
|
||||
this._counter++;
|
||||
}
|
||||
|
||||
endUpdate<T>(observable: IObservable<T>): void {
|
||||
if (this._counter === 1 && this._forceRecompute) {
|
||||
if (this._handleValue) {
|
||||
this._handleValue(observable.get());
|
||||
} else {
|
||||
observable.reportChanges();
|
||||
}
|
||||
}
|
||||
this._counter--;
|
||||
}
|
||||
|
||||
handlePossibleChange<T>(observable: IObservable<T>): void {
|
||||
// NO OP
|
||||
}
|
||||
|
||||
handleChange<T, TChange>(observable: IObservableWithChange<T, TChange>, change: TChange): void {
|
||||
// NO OP
|
||||
}
|
||||
}
|
||||
|
||||
export function derivedObservableWithCache<T>(owner: DebugOwner, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T> {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const observable = derivedOpts({ owner, debugReferenceFn: computeFn }, reader => {
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return observable;
|
||||
}
|
||||
|
||||
export function derivedObservableWithWritableCache<T>(owner: object, computeFn: (reader: IReader, lastValue: T | undefined) => T): IObservable<T>
|
||||
& { clearCache(transaction: ITransaction): void; setCache(newValue: T | undefined, tx: ITransaction | undefined): void } {
|
||||
let lastValue: T | undefined = undefined;
|
||||
const onChange = observableSignal('derivedObservableWithWritableCache');
|
||||
const observable = derived(owner, reader => {
|
||||
onChange.read(reader);
|
||||
lastValue = computeFn(reader, lastValue);
|
||||
return lastValue;
|
||||
});
|
||||
return Object.assign(observable, {
|
||||
clearCache: (tx: ITransaction) => {
|
||||
lastValue = undefined;
|
||||
onChange.trigger(tx);
|
||||
},
|
||||
setCache: (newValue: T | undefined, tx: ITransaction | undefined) => {
|
||||
lastValue = newValue;
|
||||
onChange.trigger(tx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* When the items array changes, referential equal items are not mapped again.
|
||||
*/
|
||||
export function mapObservableArrayCached<TIn, TOut, TKey = TIn>(owner: DebugOwner, items: IObservable<readonly TIn[]>, map: (input: TIn, store: DisposableStore) => TOut, keySelector?: (input: TIn) => TKey): IObservable<readonly TOut[]> {
|
||||
let m = new ArrayMap(map, keySelector);
|
||||
const self = derivedOpts({
|
||||
debugReferenceFn: map,
|
||||
owner,
|
||||
onLastObserverRemoved: () => {
|
||||
m.dispose();
|
||||
m = new ArrayMap(map);
|
||||
}
|
||||
}, (reader) => {
|
||||
m.setItems(items.read(reader));
|
||||
return m.getItems();
|
||||
});
|
||||
return self;
|
||||
}
|
||||
|
||||
class ArrayMap<TIn, TOut, TKey> implements IDisposable {
|
||||
private readonly _cache = new Map<TKey, { out: TOut; store: DisposableStore }>();
|
||||
private _items: TOut[] = [];
|
||||
constructor(
|
||||
private readonly _map: (input: TIn, store: DisposableStore) => TOut,
|
||||
private readonly _keySelector?: (input: TIn) => TKey,
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._cache.forEach(entry => entry.store.dispose());
|
||||
this._cache.clear();
|
||||
}
|
||||
|
||||
public setItems(items: readonly TIn[]): void {
|
||||
const newItems: TOut[] = [];
|
||||
const itemsToRemove = new Set(this._cache.keys());
|
||||
|
||||
for (const item of items) {
|
||||
const key = this._keySelector ? this._keySelector(item) : item as unknown as TKey;
|
||||
|
||||
let entry = this._cache.get(key);
|
||||
if (!entry) {
|
||||
const store = new DisposableStore();
|
||||
const out = this._map(item, store);
|
||||
entry = { out, store };
|
||||
this._cache.set(key, entry);
|
||||
} else {
|
||||
itemsToRemove.delete(key);
|
||||
}
|
||||
newItems.push(entry.out);
|
||||
}
|
||||
|
||||
for (const item of itemsToRemove) {
|
||||
const entry = this._cache.get(item)!;
|
||||
entry.store.dispose();
|
||||
this._cache.delete(item);
|
||||
}
|
||||
|
||||
this._items = newItems;
|
||||
}
|
||||
|
||||
public getItems(): TOut[] {
|
||||
return this._items;
|
||||
}
|
||||
}
|
|
@ -3,12 +3,12 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IReader, IObservable } from './base.js';
|
||||
import { DebugOwner, DebugNameData } from './debugName.js';
|
||||
import { CancellationError, CancellationToken, CancellationTokenSource } from './commonFacade/cancellation.js';
|
||||
import { Derived } from './derived.js';
|
||||
import { strictEquals } from './commonFacade/deps.js';
|
||||
import { autorun } from './autorun.js';
|
||||
import { IReader, IObservable } from '../base.js';
|
||||
import { DebugOwner, DebugNameData } from '../debugName.js';
|
||||
import { CancellationError, CancellationToken, CancellationTokenSource } from '../commonFacade/cancellation.js';
|
||||
import { strictEquals } from '../commonFacade/deps.js';
|
||||
import { autorun } from '../reactions/autorun.js';
|
||||
import { Derived } from '../observables/derivedImpl.js';
|
||||
|
||||
/**
|
||||
* Resolves the promise when the observables state matches the predicate.
|
|
@ -0,0 +1,29 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IObservable } from '../base.js';
|
||||
import { Event, IValueWithChangeEvent } from '../commonFacade/deps.js';
|
||||
import { DebugOwner } from '../debugName.js';
|
||||
import { observableFromEvent } from '../observables/observableFromEvent.js';
|
||||
|
||||
export class ValueWithChangeEventFromObservable<T> implements IValueWithChangeEvent<T> {
|
||||
constructor(public readonly observable: IObservable<T>) {
|
||||
}
|
||||
|
||||
get onDidChange(): Event<void> {
|
||||
return Event.fromObservableLight(this.observable);
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this.observable.get();
|
||||
}
|
||||
}
|
||||
|
||||
export function observableFromValueWithChangeEvent<T>(owner: DebugOwner, value: IValueWithChangeEvent<T>): IObservable<T> {
|
||||
if (value instanceof ValueWithChangeEventFromObservable) {
|
||||
return value.observable;
|
||||
}
|
||||
return observableFromEvent(owner, value.onDidChange, () => value.value);
|
||||
}
|
|
@ -7,12 +7,12 @@ import assert from 'assert';
|
|||
import { setUnexpectedErrorHandler } from '../../common/errors.js';
|
||||
import { Emitter, Event } from '../../common/event.js';
|
||||
import { DisposableStore, toDisposable } from '../../common/lifecycle.js';
|
||||
import { IDerivedReader, IObservableWithChange, autorun, autorunHandleChanges, autorunWithStoreHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, recordChanges, transaction, waitForState } from '../../common/observable.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { BaseObservable } from '../../common/observableInternal/base.js';
|
||||
import { IDerivedReader, IObservableWithChange, autorun, autorunHandleChanges, autorunWithStoreHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, recordChanges, transaction, waitForState, derivedHandleChanges, runOnChange } from '../../common/observable.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { observableReducer } from '../../common/observableInternal/reducer.js';
|
||||
import { observableReducer } from '../../common/observableInternal/experimental/reducer.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { BaseObservable } from '../../common/observableInternal/observables/baseObservable.js';
|
||||
|
||||
suite('observables', () => {
|
||||
const ds = ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
|
|
@ -10,7 +10,7 @@ import { equalsIfDefined, itemEquals } from '../../../../../base/common/equals.j
|
|||
import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChanges, transaction } from '../../../../../base/common/observable.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { observableReducerSettable } from '../../../../../base/common/observableInternal/reducer.js';
|
||||
import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js';
|
||||
import { isDefined } from '../../../../../base/common/types.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
import { strictEquals } from '../../../base/common/equals.js';
|
||||
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { ObservableValue } from '../../../base/common/observableInternal/base.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { DebugNameData } from '../../../base/common/observableInternal/debugName.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { ObservableValue } from '../../../base/common/observableInternal/observables/observableValue.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
|
||||
interface IObservableMementoOpts<T> {
|
||||
|
|
Loading…
Reference in New Issue