Splits up observable files

This commit is contained in:
Henning Dieterichs 2025-05-20 20:38:10 +02:00 committed by Henning Dieterichs
parent 5c34839c25
commit bf56196802
32 changed files with 1590 additions and 1449 deletions

View File

@ -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.

View File

@ -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> {

View File

@ -4,4 +4,4 @@
*--------------------------------------------------------------------------------------------*/
export { CancellationError } from '../../errors.js';
export { CancellationToken, CancellationTokenSource } from '../../cancellation.js';
export { CancellationToken, CancellationTokenSource, cancelOnDispose } from '../../cancellation.js';

View File

@ -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';

View File

@ -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.

View File

@ -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));
}

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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}`;
}
}

View File

@ -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
);
}

View File

@ -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();

View File

@ -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.

View File

@ -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;
}
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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();
}
}

View File

@ -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>(

View File

@ -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()] });
}
});
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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));
});
}

View File

@ -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();

View File

@ -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));
});
}

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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();

View File

@ -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';

View File

@ -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> {