@mintjamsinc/ichigojs 0.1.89 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -219,6 +219,27 @@ first), and a computed whose recomputed value is unchanged does not trigger DOM
219
219
  updates or watchers that depend on it. Computed→computed chains resolve
220
220
  automatically and independently of declaration order.
221
221
 
222
+ **Dependencies:**
223
+
224
+ A computed property depends on the properties its source names (`this.firstName`,
225
+ including those read by the methods it calls) and on everything it actually read
226
+ the last time it ran. Reads that only happen at run time, such as a property
227
+ looked up by a key, are therefore followed too, and the record is replaced on
228
+ every run, so a computed that switches between branches follows the branch it
229
+ took:
230
+
231
+ ```javascript
232
+ computed: {
233
+ selected() {
234
+ // `this[key]` names no property in the source; the reads are recorded when it runs
235
+ return ['upper', 'lower', 'digit'].filter(key => this[key]);
236
+ }
237
+ }
238
+ ```
239
+
240
+ The same holds for template bindings that call a method, such as
241
+ `:disabled="isLast('upper')"` where `isLast` reads `this[key]`.
242
+
222
243
  A computed property can also be defined as an object with both a `get` and a
223
244
  `set` function. This makes it writable, so it can be used as a `v-model` target
224
245
  or assigned to directly. Reads go through `get`, while assignments are routed
package/dist/ichigo.cjs CHANGED
@@ -6813,6 +6813,13 @@
6813
6813
  * through a reactive proxy. Nested calls record into the innermost set only;
6814
6814
  * the previous set is restored when fn returns or throws.
6815
6815
  *
6816
+ * The set is kept closed under ancestors: whenever a path is recorded, every
6817
+ * path above it ("items" and "items[2]" for "items[2].name") is in the set as
6818
+ * well. A read reached through a proxy records them on its way down; a read
6819
+ * that starts partway (a proxy a computed returned, a v-for item) has them
6820
+ * added when it is recorded. changeAffectsRead relies on this to answer with
6821
+ * one lookup.
6822
+ *
6816
6823
  * @param paths The set that receives the paths read.
6817
6824
  * @param fn The function to run.
6818
6825
  * @returns The value fn returns.
@@ -6827,6 +6834,57 @@
6827
6834
  this.tracker = previous;
6828
6835
  }
6829
6836
  }
6837
+ /**
6838
+ * Records a read made outside any proxy while track() runs: a binding read by
6839
+ * name (`this.flag`, `this[key]`), which the bindings report here. Reads below
6840
+ * a binding are recorded by the proxy itself.
6841
+ *
6842
+ * @param path The binding name read.
6843
+ */
6844
+ static recordRead(path) {
6845
+ this.tracker?.add(path);
6846
+ }
6847
+ /**
6848
+ * Records a read made through a proxy while track() runs: the member `key` of
6849
+ * the object at `containerPath`. The container's own path and its ancestors are
6850
+ * added first when they are missing, which happens when the proxy was not
6851
+ * reached from a root binding during this run (see track()). A container that
6852
+ * is already recorded has its ancestors recorded too, so the common case is one
6853
+ * lookup.
6854
+ *
6855
+ * @param containerPath The source path of the object read from ("" for a root).
6856
+ * @param key The property key read.
6857
+ */
6858
+ static recordProxyRead(containerPath, key) {
6859
+ const tracker = this.tracker;
6860
+ if (containerPath && !tracker.has(containerPath)) {
6861
+ for (let i = 1; i < containerPath.length; i++) {
6862
+ const c = containerPath[i];
6863
+ if (c === '.' || c === '[') {
6864
+ tracker.add(containerPath.substring(0, i));
6865
+ }
6866
+ }
6867
+ tracker.add(containerPath);
6868
+ }
6869
+ tracker.add(this.joinPath(containerPath, key));
6870
+ }
6871
+ /**
6872
+ * Checks whether a change touches something read during a tracked run (see
6873
+ * track()): the path read was written, or an object or array holding it was
6874
+ * replaced. A change below a path read does not count, since a read of the
6875
+ * changed member itself would have been recorded too; this keeps a read of a
6876
+ * whole collection ("items") from matching a write to one element.
6877
+ *
6878
+ * The set holds the ancestors of every path read, so a replaced container is
6879
+ * found by the same lookup as the path itself.
6880
+ *
6881
+ * @param changePath The path that was changed (e.g., "items[2].name")
6882
+ * @param readPaths The paths read during the tracked run
6883
+ * @returns True if the change touches a path read
6884
+ */
6885
+ static changeAffectsRead(changePath, readPaths) {
6886
+ return readPaths.has(changePath);
6887
+ }
6830
6888
  /**
6831
6889
  * The `Object.prototype.toString` tags of the *only* value types we deeply
6832
6890
  * proxy. Everything else is returned untouched (raw).
@@ -6918,7 +6976,7 @@
6918
6976
  // Function-valued members (push, Map#get, …) are accesses to behaviour,
6919
6977
  // not data, so they are left out.
6920
6978
  if (ReactiveProxy.tracker && typeof key !== 'symbol' && typeof value !== 'function') {
6921
- ReactiveProxy.tracker.add(ReactiveProxy.joinPath(path, key));
6979
+ ReactiveProxy.recordProxyRead(path, key);
6922
6980
  }
6923
6981
  // If the value is an object or array, make it reactive too
6924
6982
  if (typeof value === 'object' && value !== null) {
@@ -7337,6 +7395,18 @@
7337
7395
  });
7338
7396
  return Array.from(identifiers);
7339
7397
  }
7398
+ /**
7399
+ * Whether an expression refers to one of the application's methods, judged by the
7400
+ * identifiers extractIdentifiers returned for it. A called method may read bindings
7401
+ * the source analysis cannot name (e.g. `this[key]`), so such an expression records
7402
+ * its reads when it runs.
7403
+ * @param identifiers The identifiers extracted from the expression.
7404
+ * @param functionDependencies The application's methods and their dependencies.
7405
+ * @returns True if an identifier names a method.
7406
+ */
7407
+ static callsFunction(identifiers, functionDependencies) {
7408
+ return identifiers.some(id => Object.prototype.hasOwnProperty.call(functionDependencies, id));
7409
+ }
7340
7410
  /**
7341
7411
  * Returns the binding name an identifier chain starts with
7342
7412
  * (e.g. "items" for "items[0].name" or "user.name").
@@ -7735,26 +7805,27 @@
7735
7805
  */
7736
7806
  additionalContext;
7737
7807
  /**
7738
- * Whether any identifier has an index known only at run time ("items[*].name").
7739
- * Only then are the paths read during evaluation worth recording.
7808
+ * Whether the paths read during evaluation are recorded: when an identifier has an
7809
+ * index known only at run time ("items[*].name"), or when the expression calls a
7810
+ * method, whose reads (`this[key]` among them) the source analysis may not see.
7740
7811
  */
7741
- hasDynamicIndex;
7812
+ tracksReads;
7742
7813
  /**
7743
- * The source paths read through reactive proxies during the last evaluation,
7744
- * or undefined before the first evaluation and when nothing is recorded.
7814
+ * The source paths read during the last evaluation, or undefined before the
7815
+ * first evaluation and when nothing is recorded.
7745
7816
  */
7746
7817
  trackedPathSet;
7747
7818
  /**
7748
7819
  * Private constructor. Use ExpressionEvaluator.create() to create instances.
7749
7820
  */
7750
- constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
7821
+ constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, callsMethod, additionalContext) {
7751
7822
  this.expression = expression;
7752
7823
  this.bindings = bindings;
7753
7824
  this.identifiers = identifiers;
7754
7825
  this.originalIdentifiers = originalIdentifiers;
7755
7826
  this.evaluatorFunc = evaluatorFunc;
7756
7827
  this.additionalContext = additionalContext;
7757
- this.hasDynamicIndex = originalIdentifiers.some(id => id.includes('[*]'));
7828
+ this.tracksReads = callsMethod || originalIdentifiers.some(id => id.includes('[*]'));
7758
7829
  }
7759
7830
  /**
7760
7831
  * Creates an ExpressionEvaluator instance.
@@ -7796,7 +7867,7 @@
7796
7867
  // Cache the compiled function
7797
7868
  this.cache.set(cacheKey, evaluatorFunc);
7798
7869
  }
7799
- return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
7870
+ return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, ExpressionUtils.callsFunction(extractedIdentifiers, functionDependencies), options?.additionalContext);
7800
7871
  }
7801
7872
  /**
7802
7873
  * Compiles an expression into a function.
@@ -7851,9 +7922,11 @@
7851
7922
  values.push(...Object.values(this.additionalContext));
7852
7923
  }
7853
7924
  // Call the compiled function with the gathered values. When an index is
7854
- // known only at run time, record which concrete paths this evaluation
7855
- // read so that change matching can be narrowed to them (see trackedPaths).
7856
- if (!this.hasDynamicIndex) {
7925
+ // known only at run time or a method is called, record which concrete
7926
+ // paths this evaluation read (see trackedPaths). The values above are
7927
+ // gathered outside the tracked call on purpose: a binding passed in by
7928
+ // name is already among the identifiers.
7929
+ if (!this.tracksReads) {
7857
7930
  return this.evaluatorFunc(...values);
7858
7931
  }
7859
7932
  const paths = new Set();
@@ -7868,11 +7941,11 @@
7868
7941
  }
7869
7942
  }
7870
7943
  /**
7871
- * The source paths read through reactive proxies during the last evaluation,
7872
- * recorded only for an expression with a run-time index ("items[i].name").
7944
+ * The source paths read during the last evaluation, recorded only for an
7945
+ * expression with a run-time index ("items[i].name") or a method call.
7873
7946
  * Each evaluation replaces the previous set, so a path read for a former
7874
7947
  * index value is dropped as soon as the expression runs with the new one.
7875
- * Undefined before the first evaluation and for expressions without such an index.
7948
+ * Undefined before the first evaluation and for other expressions.
7876
7949
  */
7877
7950
  get trackedPaths() {
7878
7951
  return this.trackedPathSet;
@@ -8664,7 +8737,14 @@
8664
8737
  this.#resolveComputedIfDirty(key);
8665
8738
  }
8666
8739
  if (Reflect.has(obj, key)) {
8667
- return Reflect.get(obj, key);
8740
+ const value = Reflect.get(obj, key);
8741
+ // Record the read for a computed property or expression being tracked
8742
+ // (see ReactiveProxy.track). Methods are behaviour, not data; a binding of a
8743
+ // parent scope is recorded by the parent's own trap.
8744
+ if (typeof key === 'string' && typeof value !== 'function') {
8745
+ ReactiveProxy.recordRead(key);
8746
+ }
8747
+ return value;
8668
8748
  }
8669
8749
  return this.#parent?.raw[key];
8670
8750
  },
@@ -9009,6 +9089,13 @@
9009
9089
  markComputedDirty(key) {
9010
9090
  this.#dirtyComputeds.add(key);
9011
9091
  }
9092
+ /**
9093
+ * Indicates whether a computed property is marked dirty and not yet recomputed.
9094
+ * @param key The computed property name.
9095
+ */
9096
+ isComputedDirty(key) {
9097
+ return this.#dirtyComputeds.has(key);
9098
+ }
9012
9099
  /**
9013
9100
  * Reads the currently cached value of a computed property without triggering pull-based
9014
9101
  * re-evaluation. Used by the recompute routine to obtain the previous value for change
@@ -9128,10 +9215,14 @@
9128
9215
  * path fits the identifier (not evaluated yet, a raw object, a short-circuited read),
9129
9216
  * the identifier keeps matching every index.
9130
9217
  *
9218
+ * The recorded paths also count as dependencies of their own: they include what a
9219
+ * method called by the expression read, such as `this[key]`, which the identifiers
9220
+ * found in the source cannot name.
9221
+ *
9131
9222
  * @param changes The change paths of the current update cycle.
9132
9223
  * @param identifiers The identifiers the expression depends on.
9133
9224
  * @param trackedPaths The paths read during the expression's last evaluation, if recorded.
9134
- * @returns True if some change matches some identifier.
9225
+ * @returns True if some change matches some identifier or recorded path.
9135
9226
  */
9136
9227
  isAffected(changes, identifiers, trackedPaths) {
9137
9228
  if (changes.length === 0) {
@@ -9145,6 +9236,9 @@
9145
9236
  }
9146
9237
  }
9147
9238
  }
9239
+ if (trackedPaths && trackedPaths.size > 0) {
9240
+ return changes.some(change => ReactiveProxy.changeAffectsRead(change, trackedPaths));
9241
+ }
9148
9242
  return false;
9149
9243
  }
9150
9244
  /**
@@ -10211,8 +10305,8 @@
10211
10305
  */
10212
10306
  #evaluate;
10213
10307
  /**
10214
- * The source paths read through reactive proxies during the last evaluation,
10215
- * recorded only when some expression has a run-time index ("items[i].name").
10308
+ * The source paths read during the last evaluation, recorded only when some
10309
+ * expression has a run-time index ("items[i].name") or calls a method.
10216
10310
  */
10217
10311
  #trackedPaths;
10218
10312
  /**
@@ -10255,10 +10349,15 @@
10255
10349
  func: new Function(args, funcBody)
10256
10350
  };
10257
10351
  });
10258
- // Only an expression with an index known at run time needs its reads recorded
10259
- const hasDynamicIndex = this.#identifiers.some(id => id.includes('[*]'));
10260
- // Create a function to reconstruct the text with evaluated expressions
10261
- const evaluateText = (bindings) => {
10352
+ // Only an expression with an index known at run time, or one that calls a method
10353
+ // (whose reads, such as `this[key]`, the source analysis may not see), needs its
10354
+ // reads recorded
10355
+ const tracksReads = this.#identifiers.some(id => id.includes('[*]')) ||
10356
+ ExpressionUtils.callsFunction(this.#identifiers, functionDependencies);
10357
+ // Create a function to reconstruct the text with evaluated expressions. The values
10358
+ // are gathered outside the tracked call: a binding passed in by name is already
10359
+ // among the identifiers.
10360
+ const evaluateText = (bindings, paths) => {
10262
10361
  let result = text;
10263
10362
  evaluators.forEach((evaluator, i) => {
10264
10363
  // Build values for baseArgs (no dots) in the same order as function parameters
@@ -10272,15 +10371,16 @@
10272
10371
  });
10273
10372
  // Evaluate the expression and replace {{...}} in the text.
10274
10373
  // A function replacement keeps `$`-patterns in the value literal.
10275
- result = result.replace(matches[i][0], () => toDisplayString(evaluator.func(...values)));
10374
+ const value = paths ? ReactiveProxy.track(paths, () => evaluator.func(...values)) : evaluator.func(...values);
10375
+ result = result.replace(matches[i][0], () => toDisplayString(value));
10276
10376
  });
10277
10377
  return result;
10278
10378
  };
10279
- this.#evaluate = hasDynamicIndex
10379
+ this.#evaluate = tracksReads
10280
10380
  ? (bindings) => {
10281
10381
  const paths = new Set();
10282
10382
  this.#trackedPaths = undefined;
10283
- const result = ReactiveProxy.track(paths, () => evaluateText(bindings));
10383
+ const result = evaluateText(bindings, paths);
10284
10384
  this.#trackedPaths = paths;
10285
10385
  return result;
10286
10386
  }
@@ -10302,9 +10402,9 @@
10302
10402
  return this.#identifiers;
10303
10403
  }
10304
10404
  /**
10305
- * The source paths read through reactive proxies during the last evaluation.
10306
- * Recorded only when an expression has a run-time index ("items[i].name");
10307
- * undefined before the first evaluation and for texts without such an index.
10405
+ * The source paths read during the last evaluation. Recorded only when an
10406
+ * expression has a run-time index ("items[i].name") or calls a method;
10407
+ * undefined before the first evaluation and for other texts.
10308
10408
  */
10309
10409
  get trackedPaths() {
10310
10410
  return this.#trackedPaths;
@@ -10725,6 +10825,29 @@
10725
10825
  this.#dependentIdentifiers = [...new Set(ids)];
10726
10826
  return this.#dependentIdentifiers;
10727
10827
  }
10828
+ /**
10829
+ * Whether a change of the current update cycle concerns this node, so that a node it
10830
+ * depends on should update it: a change matches one of its identifiers, or touches a
10831
+ * path one of its bindings read the last time it ran. The recorded reads cover what a
10832
+ * called method read, such as `this[key]`, which the identifiers cannot name. Each
10833
+ * binding decides again for itself when the node updates.
10834
+ * @param changes The change paths of the current update cycle.
10835
+ */
10836
+ isAffectedBy(changes) {
10837
+ if (changes.length === 0) {
10838
+ return false;
10839
+ }
10840
+ if (this.dependentIdentifiers.some(id => changes.some(change => this.bindings.doesChangeMatchIdentifier(change, id)))) {
10841
+ return true;
10842
+ }
10843
+ const recorded = [
10844
+ this.#textEvaluator?.trackedPaths,
10845
+ ...(this.#directiveManager?.bindingsPreparers ?? []).map(preparer => preparer.trackedPaths),
10846
+ ...(this.#directiveManager?.domUpdaters ?? []).map(updater => updater.trackedPaths),
10847
+ ];
10848
+ return recorded.some(paths => paths !== undefined && paths.size > 0 &&
10849
+ changes.some(change => ReactiveProxy.changeAffectsRead(change, paths)));
10850
+ }
10728
10851
  get preparableIdentifiers() {
10729
10852
  // If already computed, return the cached preparable identifiers
10730
10853
  if (this.#preparableIdentifiers) {
@@ -10837,8 +10960,7 @@
10837
10960
  }
10838
10961
  // Recursively update dependent virtual nodes
10839
10962
  this.#dependents?.forEach(dependentNode => {
10840
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10841
- if (changed) {
10963
+ if (dependentNode.isAffectedBy(changes)) {
10842
10964
  dependentNode.update();
10843
10965
  }
10844
10966
  });
@@ -10853,8 +10975,7 @@
10853
10975
  // For document fragments (e.g. from <template v-if>), propagate updates
10854
10976
  // to dependent virtual nodes so that child bindings stay reactive.
10855
10977
  this.#dependents?.forEach(dependentNode => {
10856
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10857
- if (changed) {
10978
+ if (dependentNode.isAffectedBy(changes)) {
10858
10979
  dependentNode.update();
10859
10980
  }
10860
10981
  });
@@ -10867,6 +10988,55 @@
10867
10988
  * This is useful when an immediate update is needed, bypassing the usual change detection.
10868
10989
  */
10869
10990
  forceUpdate() {
10991
+ // One pass per outermost call; a call made while a pass runs (a directive rendering
10992
+ // a fresh clone, a component mounting) joins it. The node is stamped with the pass,
10993
+ // so that the recursion over the children below skips a child a directive has
10994
+ // already rendered in this pass (see #forceChildren).
10995
+ const outermost = VNode.#forcePass === 0;
10996
+ if (outermost) {
10997
+ VNode.#forcePass = ++VNode.#forcePassCounter;
10998
+ }
10999
+ this.#forcedInPass = VNode.#forcePass;
11000
+ try {
11001
+ this.#forceUpdate();
11002
+ }
11003
+ finally {
11004
+ if (outermost) {
11005
+ VNode.#forcePass = 0;
11006
+ }
11007
+ }
11008
+ }
11009
+ /**
11010
+ * The pass counter behind {@link forceUpdate}: the last pass number handed out.
11011
+ */
11012
+ static #forcePassCounter = 0;
11013
+ /**
11014
+ * The pass that is running, or 0 between passes.
11015
+ */
11016
+ static #forcePass = 0;
11017
+ /**
11018
+ * The pass this node was last forced in, or 0 if never.
11019
+ */
11020
+ #forcedInPass = 0;
11021
+ /**
11022
+ * Forces the children that have not been forced in the running pass. A v-if / v-for /
11023
+ * slot clone is a child of its host, and the host's directive renders it with its own
11024
+ * forceUpdate() call as soon as it is created, so recursing into it here would render it a
11025
+ * second time. A clone that already existed, and that the directive updated with
11026
+ * update(), has not been forced in this pass and is still forced here.
11027
+ */
11028
+ #forceChildren() {
11029
+ // A copy: an update can destroy a v-if / v-for clone, which removes it from this list.
11030
+ this.#childVNodes?.slice().forEach(childVNode => {
11031
+ if (childVNode.#forcedInPass !== VNode.#forcePass) {
11032
+ childVNode.forceUpdate();
11033
+ }
11034
+ });
11035
+ }
11036
+ /**
11037
+ * The body of {@link forceUpdate}, run within a pass.
11038
+ */
11039
+ #forceUpdate() {
10870
11040
  if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
10871
11041
  // If this is a text node with a text evaluator, update its content if needed
10872
11042
  this.#renderText();
@@ -10896,11 +11066,8 @@
10896
11066
  this.#renderSafely(() => updater.applyToDOM());
10897
11067
  }
10898
11068
  }
10899
- // Recursively update child virtual nodes. A copy: an update can
10900
- // destroy a v-if / v-for clone, which removes it from this list.
10901
- this.#childVNodes?.slice().forEach(childVNode => {
10902
- childVNode.forceUpdate();
10903
- });
11069
+ // Recursively update child virtual nodes
11070
+ this.#forceChildren();
10904
11071
  // If no directive requires template preservation, call onUpdated for directives that do not templatize
10905
11072
  if (!this.#templatized) {
10906
11073
  this.#directiveManager?.directives?.forEach(d => {
@@ -10910,9 +11077,7 @@
10910
11077
  }
10911
11078
  else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10912
11079
  // For document fragments, recursively force update child VNodes
10913
- this.#childVNodes?.slice().forEach(childVNode => {
10914
- childVNode.forceUpdate();
10915
- });
11080
+ this.#forceChildren();
10916
11081
  }
10917
11082
  }
10918
11083
  /**
@@ -15483,6 +15648,13 @@
15483
15648
  * A dictionary mapping computed property names to their dependencies.
15484
15649
  */
15485
15650
  #computedDependencies;
15651
+ /**
15652
+ * The paths each computed property read the last time it was evaluated (see
15653
+ * ReactiveProxy.track). A change to one of them marks the computed dirty as well as
15654
+ * a change to the dependencies found in its source, since a read the source does not
15655
+ * spell out, such as `this[key]`, is seen only here.
15656
+ */
15657
+ #computedReads = new Map();
15486
15658
  /**
15487
15659
  * A map tracking source paths for computed property values.
15488
15660
  * Maps source path (e.g., "model.elements[0]") to computed property name (e.g., "selectedElement").
@@ -16052,20 +16224,33 @@
16052
16224
  }
16053
16225
  /**
16054
16226
  * Marks computed properties as dirty (pull-based invalidation) when a dependency changes.
16055
- * Uses the statically analyzed dependency graph; because computed→computed and computed→method
16056
- * dependencies are flattened to their underlying reactive paths during analysis, a single change
16057
- * marks every transitively dependent computed dirty in one pass. The actual recomputation is
16058
- * deferred until the value is read (see #recomputeOne).
16227
+ * A computed depends on what its source names (statically analyzed, with computed→computed and
16228
+ * computed→method dependencies flattened to their underlying reactive paths) and on what it read
16229
+ * the last time it ran (#computedReads). A computed marked dirty is itself a change for the
16230
+ * computeds that read it, so marking follows those reads too. A computed already dirty is
16231
+ * skipped: the computeds that read it were marked when it was, and a computed cannot be
16232
+ * recomputed without reading — and so resolving — the dirty ones it depends on. The actual
16233
+ * recomputation is deferred until the value is read (see #recomputeOne).
16059
16234
  * @param identifier The changed identifier reported by the bindings change tracker.
16060
16235
  */
16061
16236
  #markDirtyComputeds(identifier) {
16062
16237
  if (!this.#options.computed || !this.#bindings) {
16063
16238
  return;
16064
16239
  }
16065
- for (const key of Object.keys(this.#computedDependencies)) {
16066
- const deps = this.#computedDependencies[key] || [];
16067
- if (deps.some(dep => this.#dependencyAffectedBy(dep, identifier))) {
16068
- this.#bindings.markComputedDirty(key);
16240
+ const pending = [identifier];
16241
+ while (pending.length > 0) {
16242
+ const changed = pending.pop();
16243
+ for (const key of Object.keys(this.#computedDependencies)) {
16244
+ if (this.#bindings.isComputedDirty(key)) {
16245
+ continue;
16246
+ }
16247
+ const deps = this.#computedDependencies[key] || [];
16248
+ const reads = this.#computedReads.get(key);
16249
+ if (deps.some(dep => this.#dependencyAffectedBy(dep, changed)) ||
16250
+ (reads !== undefined && ReactiveProxy.changeAffectsRead(changed, reads))) {
16251
+ this.#bindings.markComputedDirty(key);
16252
+ pending.push(key);
16253
+ }
16069
16254
  }
16070
16255
  }
16071
16256
  }
@@ -16125,10 +16310,21 @@
16125
16310
  return;
16126
16311
  }
16127
16312
  const computedFn = VApplication.#getComputedGetter(def);
16313
+ const bindings = this.#bindings;
16128
16314
  try {
16129
16315
  // Read the previous value without triggering re-resolution, for change detection.
16130
- const oldValue = this.#bindings.peekComputed(key);
16131
- const newValue = computedFn.call(this.#bindings.raw);
16316
+ const oldValue = bindings.peekComputed(key);
16317
+ // Record what the getter reads, replacing the previous record: the reads can differ
16318
+ // from run to run (a branch, a dynamic key). Kept even when the getter throws, so a
16319
+ // change to what it read before failing retries it.
16320
+ const reads = new Set();
16321
+ let newValue;
16322
+ try {
16323
+ newValue = ReactiveProxy.track(reads, () => computedFn.call(bindings.raw));
16324
+ }
16325
+ finally {
16326
+ this.#computedReads.set(key, reads);
16327
+ }
16132
16328
  // Check if the value actually changed.
16133
16329
  let hasChanged = oldValue !== newValue;
16134
16330
  // For arrays, always treat as changed (VBindings detects length changes via its length