@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
301 lines • 15.7 kB
JavaScript
;
// *****************************************************************************
// Copyright (C) 2025 1C-Soft LLC and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// copied and modified from https://github.com/microsoft/vscode/blob/1.96.3/src/vs/base/test/common/observable.test.ts
Object.defineProperty(exports, "__esModule", { value: true });
const chai_1 = require("chai");
const disposable_1 = require("../disposable");
const observable_base_1 = require("./observable-base");
const settable_observable_1 = require("./settable-observable");
const derived_observable_1 = require("./derived-observable");
const observable_signal_1 = require("./observable-signal");
const autorun_1 = require("./autorun");
describe('Observables', () => {
let disposables;
beforeEach(() => disposables = new disposable_1.DisposableCollection());
afterEach(() => disposables.dispose());
// Read these tests to understand how to use observables.
describe('Tutorial', () => {
it('settable observable & autorun', () => {
const log = new Log();
// This creates an observable with an initial value that can later be changed with the `set` method.
const myObservable = settable_observable_1.SettableObservable.create(0);
// This creates an autorun. The autorun has to be disposed!
disposables.push(autorun_1.Autorun.create(() => {
// Observables are automatically added to the tracked dependencies of the autorun as they are accessed with `get`.
log.log(`myAutorun.run(myObservable: ${myObservable.get()})`);
// Now that all dependencies are tracked, the autorun is re-run whenever any of the dependencies change.
}));
// The autorun runs immediately.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['myAutorun.run(myObservable: 0)']);
myObservable.set(1);
// The autorun runs again, because its dependency changed.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['myAutorun.run(myObservable: 1)']);
myObservable.set(1);
// The autorun didn't run, because the observable was set to the same value (no change).
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
// An update scope can be used to batch autorun runs.
observable_base_1.Observable.update(() => {
myObservable.set(2);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]); // The autorun didn't run, even though its dependency changed!
myObservable.set(3);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
});
// The autorun re-runs only at the end of the update scope.
// Note that the autorun didn't see the intermediate value `2`!
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['myAutorun.run(myObservable: 3)']);
});
it('derived observable & autorun', () => {
const log = new Log();
const observable1 = settable_observable_1.SettableObservable.create(0);
const observable2 = settable_observable_1.SettableObservable.create(0);
// This creates an observable that is derived from other observables.
const myDerived = derived_observable_1.DerivedObservable.create(() => {
// Dependencies are automatically tracked as they are accessed with `get`.
const value1 = observable1.get();
const value2 = observable2.get();
const sum = value1 + value2;
log.log(`myDerived.recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});
// This creates an autorun that reacts to changes of the derived observable.
disposables.push(autorun_1.Autorun.create(() => {
log.log(`myAutorun(myDerived: ${myDerived.get()})`);
}));
// The autorun runs immediately...
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: 0 + 0 = 0',
'myAutorun(myDerived: 0)',
]);
observable1.set(1);
// ...and on changes...
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: 1 + 0 = 1',
'myAutorun(myDerived: 1)',
]);
observable2.set(1);
// ...of the derived observable.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: 1 + 1 = 2',
'myAutorun(myDerived: 2)',
]);
// Multiple observables can be updated in a batch.
observable_base_1.Observable.update(() => {
observable1.set(5);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
observable2.set(5);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
});
// The autorun re-runs only at the end of the update scope.
// Derived observables are only recomputed on demand.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: 5 + 5 = 10',
'myAutorun(myDerived: 10)',
]);
observable_base_1.Observable.update(() => {
observable1.set(6);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
observable2.set(4);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
});
// The autorun didn't run, because its dependency changed from 10 to 10 (no change).
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['myDerived.recompute: 6 + 4 = 10']);
});
it('derived observable: get within update scope', () => {
const log = new Log();
const observable1 = settable_observable_1.SettableObservable.create(0);
const observable2 = settable_observable_1.SettableObservable.create(0);
const myDerived = derived_observable_1.DerivedObservable.create(() => {
const value1 = observable1.get();
const value2 = observable2.get();
const sum = value1 + value2;
log.log(`myDerived.recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});
disposables.push(autorun_1.Autorun.create(() => {
log.log(`myAutorun(myDerived: ${myDerived.get()})`);
}));
// The autorun runs immediately.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: 0 + 0 = 0',
'myAutorun(myDerived: 0)',
]);
observable_base_1.Observable.update(() => {
observable1.set(-10);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
myDerived.get(); // This forces a (sync) recomputation of the current value!
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['myDerived.recompute: -10 + 0 = -10']);
// This means that, even within an update scope, all observable values you get are up-to-date.
// It might just cause additional (potentially unneeded) recomputations.
observable2.set(10);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
});
// The autorun runs again, because its dependency changed from 0 to -10 and then back to 0.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'myDerived.recompute: -10 + 10 = 0',
'myAutorun(myDerived: 0)',
]);
});
it('derived observable: get without observers', () => {
const log = new Log();
const observable1 = settable_observable_1.SettableObservable.create(0);
const computed1 = derived_observable_1.DerivedObservable.create(() => {
const value1 = observable1.get();
const result = value1 % 3;
log.log(`recompute1: ${value1} % 3 = ${result}`);
return result;
});
const computed2 = derived_observable_1.DerivedObservable.create(() => {
const value1 = computed1.get();
const result = value1 * 2;
log.log(`recompute2: ${value1} * 2 = ${result}`);
return result;
});
const computed3 = derived_observable_1.DerivedObservable.create(() => {
const value1 = computed1.get();
const result = value1 * 3;
log.log(`recompute3: ${value1} * 3 = ${result}`);
return result;
});
const computedSum = derived_observable_1.DerivedObservable.create(() => {
const value1 = computed2.get();
const value2 = computed3.get();
const result = value1 + value2;
log.log(`recompute4: ${value1} + ${value2} = ${result}`);
return result;
});
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
observable1.set(1);
// Derived observables are only recomputed on demand.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
log.log(`value: ${computedSum.get()}`);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
'recompute3: 1 * 3 = 3',
'recompute4: 2 + 3 = 5',
'value: 5',
]);
log.log(`value: ${computedSum.get()}`);
// Because there are no observers, the derived observable values are not cached (!) but recomputed from scratch.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
'recompute3: 1 * 3 = 3',
'recompute4: 2 + 3 = 5',
'value: 5',
]);
// keepObserved can be used to keep the cache alive.
const disposable = observable_base_1.Observable.keepObserved(computedSum);
log.log(`value: ${computedSum.get()}`);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
'recompute3: 1 * 3 = 3',
'recompute4: 2 + 3 = 5',
'value: 5',
]);
log.log(`value: ${computedSum.get()}`);
// Tada, no recomputations!
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'value: 5',
]);
observable1.set(2);
// keepObserved does not force derived observables to be recomputed.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([]);
log.log(`value: ${computedSum.get()}`);
// Derived observables are only recomputed on demand...
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 2 % 3 = 2',
'recompute2: 2 * 2 = 4',
'recompute3: 2 * 3 = 6',
'recompute4: 4 + 6 = 10',
'value: 10',
]);
log.log(`value: ${computedSum.get()}`);
// ...and then cached again.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal(['value: 10']);
// Don't forget to dispose the disposable returned by keepObserved!
disposable.dispose();
log.log(`value: ${computedSum.get()}`);
// The cache is disabled again.
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 2 % 3 = 2',
'recompute2: 2 * 2 = 4',
'recompute3: 2 * 3 = 6',
'recompute4: 4 + 6 = 10',
'value: 10',
]);
log.log(`value: ${computedSum.get()}`);
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'recompute1: 2 % 3 = 2',
'recompute2: 2 * 2 = 4',
'recompute3: 2 * 3 = 6',
'recompute4: 4 + 6 = 10',
'value: 10',
]);
});
it('autorun that receives change information of signals', () => {
const log = new Log();
// A signal is an observable without a value.
// However, it can ship change information when it is triggered.
const signal = observable_signal_1.ObservableSignal.create();
disposables.push(autorun_1.Autorun.create(({ changeSummary }) => {
signal.get(); // This makes sure the signal is tracked as a dependency of the autorun.
log.log('msgs: ' + changeSummary.msgs.join(', '));
}, {
// A change summary can be used to collect the reported changes.
createChangeSummary: () => ({ msgs: [] }),
willHandleChange: (context, changeSummary) => {
if (context.isChangeOf(signal)) {
changeSummary.msgs.push(context.change.msg);
}
return true;
},
}));
signal.trigger({ msg: 'foobar' });
// An update scope can be used to batch triggering signals.
// No change information is lost!
observable_base_1.Observable.update(() => {
signal.trigger({ msg: 'hello' });
signal.trigger({ msg: 'world' });
});
(0, chai_1.expect)(log.getAndClearEntries()).to.be.deep.equal([
'msgs: ',
'msgs: foobar',
'msgs: hello, world'
]);
});
});
});
class Log {
constructor() {
this.entries = [];
}
log(message) {
this.entries.push(message);
}
getAndClearEntries() {
const entries = [...this.entries];
this.entries.length = 0;
return entries;
}
}
//# sourceMappingURL=observable.spec.js.map