derived.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { BugIndicatingError } from '../errors.js';
  6. import { BaseObservable, _setDerived } from './base.js';
  7. import { getLogger } from './logging.js';
  8. export function derived(debugName, computeFn) {
  9. return new Derived(debugName, computeFn);
  10. }
  11. _setDerived(derived);
  12. export class Derived extends BaseObservable {
  13. get debugName() {
  14. return typeof this._debugName === 'function' ? this._debugName() : this._debugName;
  15. }
  16. constructor(_debugName, computeFn) {
  17. var _a;
  18. super();
  19. this._debugName = _debugName;
  20. this.computeFn = computeFn;
  21. this.state = 0 /* DerivedState.initial */;
  22. this.value = undefined;
  23. this.updateCount = 0;
  24. this.dependencies = new Set();
  25. this.dependenciesToBeRemoved = new Set();
  26. (_a = getLogger()) === null || _a === void 0 ? void 0 : _a.handleDerivedCreated(this);
  27. }
  28. onLastObserverRemoved() {
  29. /**
  30. * We are not tracking changes anymore, thus we have to assume
  31. * that our cache is invalid.
  32. */
  33. this.state = 0 /* DerivedState.initial */;
  34. this.value = undefined;
  35. for (const d of this.dependencies) {
  36. d.removeObserver(this);
  37. }
  38. this.dependencies.clear();
  39. }
  40. get() {
  41. if (this.observers.size === 0) {
  42. // Without observers, we don't know when to clean up stuff.
  43. // Thus, we don't cache anything to prevent memory leaks.
  44. const result = this.computeFn(this);
  45. // Clear new dependencies
  46. this.onLastObserverRemoved();
  47. return result;
  48. }
  49. else {
  50. do {
  51. if (this.state === 1 /* DerivedState.dependenciesMightHaveChanged */) {
  52. // We might not get a notification for a dependency that changed while it is updating,
  53. // thus we also have to ask all our depedencies if they changed in this case.
  54. this.state = 3 /* DerivedState.upToDate */;
  55. for (const d of this.dependencies) {
  56. /** might call {@link handleChange} indirectly, which could invalidate us */
  57. d.reportChanges();
  58. if (this.state === 2 /* DerivedState.stale */) {
  59. // The other dependencies will refresh on demand, so early break
  60. break;
  61. }
  62. }
  63. }
  64. this._recomputeIfNeeded();
  65. // In case recomputation changed one of our dependencies, we need to recompute again.
  66. } while (this.state !== 3 /* DerivedState.upToDate */);
  67. return this.value;
  68. }
  69. }
  70. _recomputeIfNeeded() {
  71. var _a;
  72. if (this.state === 3 /* DerivedState.upToDate */) {
  73. return;
  74. }
  75. const emptySet = this.dependenciesToBeRemoved;
  76. this.dependenciesToBeRemoved = this.dependencies;
  77. this.dependencies = emptySet;
  78. const hadValue = this.state !== 0 /* DerivedState.initial */;
  79. const oldValue = this.value;
  80. this.state = 3 /* DerivedState.upToDate */;
  81. try {
  82. /** might call {@link handleChange} indirectly, which could invalidate us */
  83. this.value = this.computeFn(this);
  84. }
  85. finally {
  86. // We don't want our observed observables to think that they are (not even temporarily) not being observed.
  87. // Thus, we only unsubscribe from observables that are definitely not read anymore.
  88. for (const o of this.dependenciesToBeRemoved) {
  89. o.removeObserver(this);
  90. }
  91. this.dependenciesToBeRemoved.clear();
  92. }
  93. const didChange = hadValue && oldValue !== this.value;
  94. (_a = getLogger()) === null || _a === void 0 ? void 0 : _a.handleDerivedRecomputed(this, {
  95. oldValue,
  96. newValue: this.value,
  97. change: undefined,
  98. didChange
  99. });
  100. if (didChange) {
  101. for (const r of this.observers) {
  102. r.handleChange(this, undefined);
  103. }
  104. }
  105. }
  106. toString() {
  107. return `LazyDerived<${this.debugName}>`;
  108. }
  109. // IObserver Implementation
  110. beginUpdate() {
  111. this.updateCount++;
  112. const propagateBeginUpdate = this.updateCount === 1;
  113. if (this.state === 3 /* DerivedState.upToDate */) {
  114. this.state = 1 /* DerivedState.dependenciesMightHaveChanged */;
  115. // If we propagate begin update, that will already signal a possible change.
  116. if (!propagateBeginUpdate) {
  117. for (const r of this.observers) {
  118. r.handlePossibleChange(this);
  119. }
  120. }
  121. }
  122. if (propagateBeginUpdate) {
  123. for (const r of this.observers) {
  124. r.beginUpdate(this); // This signals a possible change
  125. }
  126. }
  127. }
  128. endUpdate() {
  129. this.updateCount--;
  130. if (this.updateCount === 0) {
  131. // End update could change the observer list.
  132. const observers = [...this.observers];
  133. for (const r of observers) {
  134. r.endUpdate(this);
  135. }
  136. }
  137. if (this.updateCount < 0) {
  138. throw new BugIndicatingError();
  139. }
  140. }
  141. handlePossibleChange(observable) {
  142. // In all other states, observers already know that we might have changed.
  143. if (this.state === 3 /* DerivedState.upToDate */ && this.dependencies.has(observable)) {
  144. this.state = 1 /* DerivedState.dependenciesMightHaveChanged */;
  145. for (const r of this.observers) {
  146. r.handlePossibleChange(this);
  147. }
  148. }
  149. }
  150. handleChange(observable, _change) {
  151. const isUpToDate = this.state === 3 /* DerivedState.upToDate */;
  152. if ((this.state === 1 /* DerivedState.dependenciesMightHaveChanged */ || isUpToDate) && this.dependencies.has(observable)) {
  153. this.state = 2 /* DerivedState.stale */;
  154. if (isUpToDate) {
  155. for (const r of this.observers) {
  156. r.handlePossibleChange(this);
  157. }
  158. }
  159. }
  160. }
  161. // IReader Implementation
  162. readObservable(observable) {
  163. // Subscribe before getting the value to enable caching
  164. observable.addObserver(this);
  165. /** This might call {@link handleChange} indirectly, which could invalidate us */
  166. const value = observable.get();
  167. // Which is why we only add the observable to the dependencies now.
  168. this.dependencies.add(observable);
  169. this.dependenciesToBeRemoved.delete(observable);
  170. return value;
  171. }
  172. }