Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improves observable tests. #202494

Merged
merged 1 commit into from
Jan 15, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
61 changes: 50 additions & 11 deletions src/vs/base/test/common/observable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,72 @@ suite('observables', () => {
suite('tutorial', () => {
test('observable + autorun', () => {
const log = new Log();
// This creates a new observable value. The name is only used for debugging purposes.
// The second arg is the initial value.
const myObservable = observableValue('myObservable', 0);

// This creates an autorun. The @description is only used for debugging purposes.
// The autorun has to be disposed! This is very important.
ds.add(autorun(reader => {
/** @description myAutorun */

// This code is run immediately.

// Use the `reader` to read observable values and track the dependency to them.
// If you use `observable.get()` instead of `observable.read(reader)`, you will just
// get the value and not track the dependency.
log.log(`myAutorun.run(myObservable: ${myObservable.read(reader)})`);

// Now that all dependencies are tracked, the autorun is re-run whenever any of the
// dependencies change.
}));
// The autorun runs immediately
assert.deepStrictEqual(log.getAndClearEntries(), ['myAutorun.run(myObservable: 0)']);

// We set the observable.
myObservable.set(1, undefined);
// The autorun runs again when any read observable changed
// -> The autorun runs again when any read observable changed
assert.deepStrictEqual(log.getAndClearEntries(), ['myAutorun.run(myObservable: 1)']);

// We set the observable again.
myObservable.set(1, undefined);
// But only if the value changed
// -> The autorun does not run again, because the observable didn't change.
assert.deepStrictEqual(log.getAndClearEntries(), []);

// Transactions batch autorun runs
transaction((tx) => {
myObservable.set(2, tx);
// No auto-run ran yet, even though the value changed
// No auto-run ran yet, even though the value changed!
assert.deepStrictEqual(log.getAndClearEntries(), []);

myObservable.set(3, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
});
// Only at the end of the transaction the autorun re-runs
assert.deepStrictEqual(log.getAndClearEntries(), ['myAutorun.run(myObservable: 3)']);

// Note that the autorun did not see the intermediate value `2`!
});

test('computed + autorun', () => {
test('derived + autorun', () => {
const log = new Log();
const observable1 = observableValue('myObservable1', 0);
const observable2 = observableValue('myObservable2', 0);

// A derived value is an observable that is derived from other observables.
const myDerived = derived(reader => {
/** @description myDerived */
const value1 = observable1.read(reader);
const value1 = observable1.read(reader); // Use the reader to track dependencies.
const value2 = observable2.read(reader);
const sum = value1 + value2;
log.log(`myDerived.recompute: ${value1} + ${value2} = ${sum}`);
return sum;
});

// We create an autorun that reacts on changes to our derived value.
ds.add(autorun(reader => {
/** @description myAutorun */
// Autoruns work with observable values and deriveds - in short, they work with any observable.
log.log(`myAutorun(myDerived: ${myDerived.read(reader)})`);
}));
// autorun runs immediately
Expand All @@ -86,6 +106,7 @@ suite('observables', () => {
"myAutorun(myDerived: 2)",
]);

// Now we change multiple observables in a transaction to batch process the effects.
transaction((tx) => {
observable1.set(5, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
Expand All @@ -95,6 +116,7 @@ suite('observables', () => {
});
// When changing multiple observables in a transaction,
// deriveds are only recomputed on demand.
// (Note that you cannot see the intermediate value when `obs1 == 5` and `obs2 == 1`)
assert.deepStrictEqual(log.getAndClearEntries(), [
"myDerived.recompute: 5 + 5 = 10",
"myAutorun(myDerived: 10)",
Expand Down Expand Up @@ -139,8 +161,10 @@ suite('observables', () => {
observable1.set(-10, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);

myDerived.get(); // This forces a (sync) recomputation of the current value
myDerived.get(); // This forces a (sync) recomputation of the current value!
assert.deepStrictEqual(log.getAndClearEntries(), (["myDerived.recompute: -10 + 0 = -10"]));
// This means, that even in transactions you can assume that all values you can read with `get` and `read` are up-to-date.
// Read these values just might cause additional (potentially unneeded) recomputations.

observable2.set(10, tx);
assert.deepStrictEqual(log.getAndClearEntries(), []);
Expand All @@ -155,6 +179,8 @@ suite('observables', () => {
test('get without observers', () => {
const log = new Log();
const observable1 = observableValue('myObservableValue1', 0);

// We set up some computeds.
const computed1 = derived((reader) => {
/** @description computed */
const value1 = observable1.read(reader);
Expand Down Expand Up @@ -189,6 +215,7 @@ suite('observables', () => {
observable1.set(1, undefined);
assert.deepStrictEqual(log.getAndClearEntries(), []);

// And now read the computed that dependens on all the others.
log.log(`value: ${computedSum.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
Expand All @@ -199,7 +226,7 @@ suite('observables', () => {
]);

log.log(`value: ${computedSum.get()}`);
// Because there are no observers, the derived values are not cached, but computed from scratch.
// Because there are no observers, the derived values are not cached (!), but computed from scratch.
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
'recompute2: 1 * 2 = 2',
Expand All @@ -208,7 +235,8 @@ suite('observables', () => {
'value: 5',
]);

const disposable = keepObserved(computedSum); // Use keepAlive to keep the cache
const disposable = keepObserved(computedSum); // Use keepObserved to keep the cache.
// You can also use `computedSum.keepObserved(store)` for an inline experience.
log.log(`value: ${computedSum.get()}`);
assert.deepStrictEqual(log.getAndClearEntries(), [
'recompute1: 1 % 3 = 1',
Expand All @@ -222,13 +250,14 @@ suite('observables', () => {
assert.deepStrictEqual(log.getAndClearEntries(), [
'value: 5',
]);
// Tada, no recomputations!

observable1.set(2, undefined);
// The keep alive does not force deriveds to be recomputed
// The keepObserved does not force deriveds to be recomputed! They are still lazy.
assert.deepStrictEqual(log.getAndClearEntries(), ([]));

log.log(`value: ${computedSum.get()}`);
// Those deriveds are recomputed on demand
// Those deriveds are recomputed on demand, i.e. when someone reads them.
assert.deepStrictEqual(log.getAndClearEntries(), [
"recompute1: 2 % 3 = 2",
"recompute2: 2 * 2 = 4",
Expand All @@ -240,7 +269,7 @@ suite('observables', () => {
// ... and then cached again
assert.deepStrictEqual(log.getAndClearEntries(), (["value: 10"]));

disposable.dispose(); // Don't forget to dispose the keepAlive to prevent memory leaks
disposable.dispose(); // Don't forget to dispose the keepAlive to prevent memory leaks!

log.log(`value: ${computedSum.get()}`);
// Which disables the cache again
Expand All @@ -260,7 +289,17 @@ suite('observables', () => {
"recompute4: 4 + 6 = 10",
"value: 10",
]);

// Why don't we just always keep the cache alive?
// This is because in order to keep the cache alive, we have to keep our subscriptions to our dependencies alive,
// which could cause memory-leaks.
// So instead, when the last observer of a derived is disposed, we dispose our subscriptions to our dependencies.
// `keepObserved` just prevents this from happening.
});

// That is the end of the tutorial.
// There are lots of utilities you can explore now, like `observableFromEvent`, `Event.fromObservableLight`,
// autorunWithStore, observableWithStore and so on.
});

test('topological order', () => {
Expand Down