@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.
@@ -6807,6 +6807,13 @@ class ReactiveProxy {
6807
6807
  * through a reactive proxy. Nested calls record into the innermost set only;
6808
6808
  * the previous set is restored when fn returns or throws.
6809
6809
  *
6810
+ * The set is kept closed under ancestors: whenever a path is recorded, every
6811
+ * path above it ("items" and "items[2]" for "items[2].name") is in the set as
6812
+ * well. A read reached through a proxy records them on its way down; a read
6813
+ * that starts partway (a proxy a computed returned, a v-for item) has them
6814
+ * added when it is recorded. changeAffectsRead relies on this to answer with
6815
+ * one lookup.
6816
+ *
6810
6817
  * @param paths The set that receives the paths read.
6811
6818
  * @param fn The function to run.
6812
6819
  * @returns The value fn returns.
@@ -6821,6 +6828,57 @@ class ReactiveProxy {
6821
6828
  this.tracker = previous;
6822
6829
  }
6823
6830
  }
6831
+ /**
6832
+ * Records a read made outside any proxy while track() runs: a binding read by
6833
+ * name (`this.flag`, `this[key]`), which the bindings report here. Reads below
6834
+ * a binding are recorded by the proxy itself.
6835
+ *
6836
+ * @param path The binding name read.
6837
+ */
6838
+ static recordRead(path) {
6839
+ this.tracker?.add(path);
6840
+ }
6841
+ /**
6842
+ * Records a read made through a proxy while track() runs: the member `key` of
6843
+ * the object at `containerPath`. The container's own path and its ancestors are
6844
+ * added first when they are missing, which happens when the proxy was not
6845
+ * reached from a root binding during this run (see track()). A container that
6846
+ * is already recorded has its ancestors recorded too, so the common case is one
6847
+ * lookup.
6848
+ *
6849
+ * @param containerPath The source path of the object read from ("" for a root).
6850
+ * @param key The property key read.
6851
+ */
6852
+ static recordProxyRead(containerPath, key) {
6853
+ const tracker = this.tracker;
6854
+ if (containerPath && !tracker.has(containerPath)) {
6855
+ for (let i = 1; i < containerPath.length; i++) {
6856
+ const c = containerPath[i];
6857
+ if (c === '.' || c === '[') {
6858
+ tracker.add(containerPath.substring(0, i));
6859
+ }
6860
+ }
6861
+ tracker.add(containerPath);
6862
+ }
6863
+ tracker.add(this.joinPath(containerPath, key));
6864
+ }
6865
+ /**
6866
+ * Checks whether a change touches something read during a tracked run (see
6867
+ * track()): the path read was written, or an object or array holding it was
6868
+ * replaced. A change below a path read does not count, since a read of the
6869
+ * changed member itself would have been recorded too; this keeps a read of a
6870
+ * whole collection ("items") from matching a write to one element.
6871
+ *
6872
+ * The set holds the ancestors of every path read, so a replaced container is
6873
+ * found by the same lookup as the path itself.
6874
+ *
6875
+ * @param changePath The path that was changed (e.g., "items[2].name")
6876
+ * @param readPaths The paths read during the tracked run
6877
+ * @returns True if the change touches a path read
6878
+ */
6879
+ static changeAffectsRead(changePath, readPaths) {
6880
+ return readPaths.has(changePath);
6881
+ }
6824
6882
  /**
6825
6883
  * The `Object.prototype.toString` tags of the *only* value types we deeply
6826
6884
  * proxy. Everything else is returned untouched (raw).
@@ -6912,7 +6970,7 @@ class ReactiveProxy {
6912
6970
  // Function-valued members (push, Map#get, …) are accesses to behaviour,
6913
6971
  // not data, so they are left out.
6914
6972
  if (ReactiveProxy.tracker && typeof key !== 'symbol' && typeof value !== 'function') {
6915
- ReactiveProxy.tracker.add(ReactiveProxy.joinPath(path, key));
6973
+ ReactiveProxy.recordProxyRead(path, key);
6916
6974
  }
6917
6975
  // If the value is an object or array, make it reactive too
6918
6976
  if (typeof value === 'object' && value !== null) {
@@ -7331,6 +7389,18 @@ class ExpressionUtils {
7331
7389
  });
7332
7390
  return Array.from(identifiers);
7333
7391
  }
7392
+ /**
7393
+ * Whether an expression refers to one of the application's methods, judged by the
7394
+ * identifiers extractIdentifiers returned for it. A called method may read bindings
7395
+ * the source analysis cannot name (e.g. `this[key]`), so such an expression records
7396
+ * its reads when it runs.
7397
+ * @param identifiers The identifiers extracted from the expression.
7398
+ * @param functionDependencies The application's methods and their dependencies.
7399
+ * @returns True if an identifier names a method.
7400
+ */
7401
+ static callsFunction(identifiers, functionDependencies) {
7402
+ return identifiers.some(id => Object.prototype.hasOwnProperty.call(functionDependencies, id));
7403
+ }
7334
7404
  /**
7335
7405
  * Returns the binding name an identifier chain starts with
7336
7406
  * (e.g. "items" for "items[0].name" or "user.name").
@@ -7729,26 +7799,27 @@ class ExpressionEvaluator {
7729
7799
  */
7730
7800
  additionalContext;
7731
7801
  /**
7732
- * Whether any identifier has an index known only at run time ("items[*].name").
7733
- * Only then are the paths read during evaluation worth recording.
7802
+ * Whether the paths read during evaluation are recorded: when an identifier has an
7803
+ * index known only at run time ("items[*].name"), or when the expression calls a
7804
+ * method, whose reads (`this[key]` among them) the source analysis may not see.
7734
7805
  */
7735
- hasDynamicIndex;
7806
+ tracksReads;
7736
7807
  /**
7737
- * The source paths read through reactive proxies during the last evaluation,
7738
- * or undefined before the first evaluation and when nothing is recorded.
7808
+ * The source paths read during the last evaluation, or undefined before the
7809
+ * first evaluation and when nothing is recorded.
7739
7810
  */
7740
7811
  trackedPathSet;
7741
7812
  /**
7742
7813
  * Private constructor. Use ExpressionEvaluator.create() to create instances.
7743
7814
  */
7744
- constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, additionalContext) {
7815
+ constructor(expression, bindings, identifiers, originalIdentifiers, evaluatorFunc, callsMethod, additionalContext) {
7745
7816
  this.expression = expression;
7746
7817
  this.bindings = bindings;
7747
7818
  this.identifiers = identifiers;
7748
7819
  this.originalIdentifiers = originalIdentifiers;
7749
7820
  this.evaluatorFunc = evaluatorFunc;
7750
7821
  this.additionalContext = additionalContext;
7751
- this.hasDynamicIndex = originalIdentifiers.some(id => id.includes('[*]'));
7822
+ this.tracksReads = callsMethod || originalIdentifiers.some(id => id.includes('[*]'));
7752
7823
  }
7753
7824
  /**
7754
7825
  * Creates an ExpressionEvaluator instance.
@@ -7790,7 +7861,7 @@ class ExpressionEvaluator {
7790
7861
  // Cache the compiled function
7791
7862
  this.cache.set(cacheKey, evaluatorFunc);
7792
7863
  }
7793
- return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, options?.additionalContext);
7864
+ return new ExpressionEvaluator(expression, bindings, baseIdentifiers, extractedIdentifiers, evaluatorFunc, ExpressionUtils.callsFunction(extractedIdentifiers, functionDependencies), options?.additionalContext);
7794
7865
  }
7795
7866
  /**
7796
7867
  * Compiles an expression into a function.
@@ -7845,9 +7916,11 @@ class ExpressionEvaluator {
7845
7916
  values.push(...Object.values(this.additionalContext));
7846
7917
  }
7847
7918
  // Call the compiled function with the gathered values. When an index is
7848
- // known only at run time, record which concrete paths this evaluation
7849
- // read so that change matching can be narrowed to them (see trackedPaths).
7850
- if (!this.hasDynamicIndex) {
7919
+ // known only at run time or a method is called, record which concrete
7920
+ // paths this evaluation read (see trackedPaths). The values above are
7921
+ // gathered outside the tracked call on purpose: a binding passed in by
7922
+ // name is already among the identifiers.
7923
+ if (!this.tracksReads) {
7851
7924
  return this.evaluatorFunc(...values);
7852
7925
  }
7853
7926
  const paths = new Set();
@@ -7862,11 +7935,11 @@ class ExpressionEvaluator {
7862
7935
  }
7863
7936
  }
7864
7937
  /**
7865
- * The source paths read through reactive proxies during the last evaluation,
7866
- * recorded only for an expression with a run-time index ("items[i].name").
7938
+ * The source paths read during the last evaluation, recorded only for an
7939
+ * expression with a run-time index ("items[i].name") or a method call.
7867
7940
  * Each evaluation replaces the previous set, so a path read for a former
7868
7941
  * index value is dropped as soon as the expression runs with the new one.
7869
- * Undefined before the first evaluation and for expressions without such an index.
7942
+ * Undefined before the first evaluation and for other expressions.
7870
7943
  */
7871
7944
  get trackedPaths() {
7872
7945
  return this.trackedPathSet;
@@ -8658,7 +8731,14 @@ class VBindings {
8658
8731
  this.#resolveComputedIfDirty(key);
8659
8732
  }
8660
8733
  if (Reflect.has(obj, key)) {
8661
- return Reflect.get(obj, key);
8734
+ const value = Reflect.get(obj, key);
8735
+ // Record the read for a computed property or expression being tracked
8736
+ // (see ReactiveProxy.track). Methods are behaviour, not data; a binding of a
8737
+ // parent scope is recorded by the parent's own trap.
8738
+ if (typeof key === 'string' && typeof value !== 'function') {
8739
+ ReactiveProxy.recordRead(key);
8740
+ }
8741
+ return value;
8662
8742
  }
8663
8743
  return this.#parent?.raw[key];
8664
8744
  },
@@ -9003,6 +9083,13 @@ class VBindings {
9003
9083
  markComputedDirty(key) {
9004
9084
  this.#dirtyComputeds.add(key);
9005
9085
  }
9086
+ /**
9087
+ * Indicates whether a computed property is marked dirty and not yet recomputed.
9088
+ * @param key The computed property name.
9089
+ */
9090
+ isComputedDirty(key) {
9091
+ return this.#dirtyComputeds.has(key);
9092
+ }
9006
9093
  /**
9007
9094
  * Reads the currently cached value of a computed property without triggering pull-based
9008
9095
  * re-evaluation. Used by the recompute routine to obtain the previous value for change
@@ -9122,10 +9209,14 @@ class VBindings {
9122
9209
  * path fits the identifier (not evaluated yet, a raw object, a short-circuited read),
9123
9210
  * the identifier keeps matching every index.
9124
9211
  *
9212
+ * The recorded paths also count as dependencies of their own: they include what a
9213
+ * method called by the expression read, such as `this[key]`, which the identifiers
9214
+ * found in the source cannot name.
9215
+ *
9125
9216
  * @param changes The change paths of the current update cycle.
9126
9217
  * @param identifiers The identifiers the expression depends on.
9127
9218
  * @param trackedPaths The paths read during the expression's last evaluation, if recorded.
9128
- * @returns True if some change matches some identifier.
9219
+ * @returns True if some change matches some identifier or recorded path.
9129
9220
  */
9130
9221
  isAffected(changes, identifiers, trackedPaths) {
9131
9222
  if (changes.length === 0) {
@@ -9139,6 +9230,9 @@ class VBindings {
9139
9230
  }
9140
9231
  }
9141
9232
  }
9233
+ if (trackedPaths && trackedPaths.size > 0) {
9234
+ return changes.some(change => ReactiveProxy.changeAffectsRead(change, trackedPaths));
9235
+ }
9142
9236
  return false;
9143
9237
  }
9144
9238
  /**
@@ -10205,8 +10299,8 @@ class VTextEvaluator {
10205
10299
  */
10206
10300
  #evaluate;
10207
10301
  /**
10208
- * The source paths read through reactive proxies during the last evaluation,
10209
- * recorded only when some expression has a run-time index ("items[i].name").
10302
+ * The source paths read during the last evaluation, recorded only when some
10303
+ * expression has a run-time index ("items[i].name") or calls a method.
10210
10304
  */
10211
10305
  #trackedPaths;
10212
10306
  /**
@@ -10249,10 +10343,15 @@ class VTextEvaluator {
10249
10343
  func: new Function(args, funcBody)
10250
10344
  };
10251
10345
  });
10252
- // Only an expression with an index known at run time needs its reads recorded
10253
- const hasDynamicIndex = this.#identifiers.some(id => id.includes('[*]'));
10254
- // Create a function to reconstruct the text with evaluated expressions
10255
- const evaluateText = (bindings) => {
10346
+ // Only an expression with an index known at run time, or one that calls a method
10347
+ // (whose reads, such as `this[key]`, the source analysis may not see), needs its
10348
+ // reads recorded
10349
+ const tracksReads = this.#identifiers.some(id => id.includes('[*]')) ||
10350
+ ExpressionUtils.callsFunction(this.#identifiers, functionDependencies);
10351
+ // Create a function to reconstruct the text with evaluated expressions. The values
10352
+ // are gathered outside the tracked call: a binding passed in by name is already
10353
+ // among the identifiers.
10354
+ const evaluateText = (bindings, paths) => {
10256
10355
  let result = text;
10257
10356
  evaluators.forEach((evaluator, i) => {
10258
10357
  // Build values for baseArgs (no dots) in the same order as function parameters
@@ -10266,15 +10365,16 @@ class VTextEvaluator {
10266
10365
  });
10267
10366
  // Evaluate the expression and replace {{...}} in the text.
10268
10367
  // A function replacement keeps `$`-patterns in the value literal.
10269
- result = result.replace(matches[i][0], () => toDisplayString(evaluator.func(...values)));
10368
+ const value = paths ? ReactiveProxy.track(paths, () => evaluator.func(...values)) : evaluator.func(...values);
10369
+ result = result.replace(matches[i][0], () => toDisplayString(value));
10270
10370
  });
10271
10371
  return result;
10272
10372
  };
10273
- this.#evaluate = hasDynamicIndex
10373
+ this.#evaluate = tracksReads
10274
10374
  ? (bindings) => {
10275
10375
  const paths = new Set();
10276
10376
  this.#trackedPaths = undefined;
10277
- const result = ReactiveProxy.track(paths, () => evaluateText(bindings));
10377
+ const result = evaluateText(bindings, paths);
10278
10378
  this.#trackedPaths = paths;
10279
10379
  return result;
10280
10380
  }
@@ -10296,9 +10396,9 @@ class VTextEvaluator {
10296
10396
  return this.#identifiers;
10297
10397
  }
10298
10398
  /**
10299
- * The source paths read through reactive proxies during the last evaluation.
10300
- * Recorded only when an expression has a run-time index ("items[i].name");
10301
- * undefined before the first evaluation and for texts without such an index.
10399
+ * The source paths read during the last evaluation. Recorded only when an
10400
+ * expression has a run-time index ("items[i].name") or calls a method;
10401
+ * undefined before the first evaluation and for other texts.
10302
10402
  */
10303
10403
  get trackedPaths() {
10304
10404
  return this.#trackedPaths;
@@ -10719,6 +10819,29 @@ class VNode {
10719
10819
  this.#dependentIdentifiers = [...new Set(ids)];
10720
10820
  return this.#dependentIdentifiers;
10721
10821
  }
10822
+ /**
10823
+ * Whether a change of the current update cycle concerns this node, so that a node it
10824
+ * depends on should update it: a change matches one of its identifiers, or touches a
10825
+ * path one of its bindings read the last time it ran. The recorded reads cover what a
10826
+ * called method read, such as `this[key]`, which the identifiers cannot name. Each
10827
+ * binding decides again for itself when the node updates.
10828
+ * @param changes The change paths of the current update cycle.
10829
+ */
10830
+ isAffectedBy(changes) {
10831
+ if (changes.length === 0) {
10832
+ return false;
10833
+ }
10834
+ if (this.dependentIdentifiers.some(id => changes.some(change => this.bindings.doesChangeMatchIdentifier(change, id)))) {
10835
+ return true;
10836
+ }
10837
+ const recorded = [
10838
+ this.#textEvaluator?.trackedPaths,
10839
+ ...(this.#directiveManager?.bindingsPreparers ?? []).map(preparer => preparer.trackedPaths),
10840
+ ...(this.#directiveManager?.domUpdaters ?? []).map(updater => updater.trackedPaths),
10841
+ ];
10842
+ return recorded.some(paths => paths !== undefined && paths.size > 0 &&
10843
+ changes.some(change => ReactiveProxy.changeAffectsRead(change, paths)));
10844
+ }
10722
10845
  get preparableIdentifiers() {
10723
10846
  // If already computed, return the cached preparable identifiers
10724
10847
  if (this.#preparableIdentifiers) {
@@ -10831,8 +10954,7 @@ class VNode {
10831
10954
  }
10832
10955
  // Recursively update dependent virtual nodes
10833
10956
  this.#dependents?.forEach(dependentNode => {
10834
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10835
- if (changed) {
10957
+ if (dependentNode.isAffectedBy(changes)) {
10836
10958
  dependentNode.update();
10837
10959
  }
10838
10960
  });
@@ -10847,8 +10969,7 @@ class VNode {
10847
10969
  // For document fragments (e.g. from <template v-if>), propagate updates
10848
10970
  // to dependent virtual nodes so that child bindings stay reactive.
10849
10971
  this.#dependents?.forEach(dependentNode => {
10850
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10851
- if (changed) {
10972
+ if (dependentNode.isAffectedBy(changes)) {
10852
10973
  dependentNode.update();
10853
10974
  }
10854
10975
  });
@@ -10861,6 +10982,55 @@ class VNode {
10861
10982
  * This is useful when an immediate update is needed, bypassing the usual change detection.
10862
10983
  */
10863
10984
  forceUpdate() {
10985
+ // One pass per outermost call; a call made while a pass runs (a directive rendering
10986
+ // a fresh clone, a component mounting) joins it. The node is stamped with the pass,
10987
+ // so that the recursion over the children below skips a child a directive has
10988
+ // already rendered in this pass (see #forceChildren).
10989
+ const outermost = VNode.#forcePass === 0;
10990
+ if (outermost) {
10991
+ VNode.#forcePass = ++VNode.#forcePassCounter;
10992
+ }
10993
+ this.#forcedInPass = VNode.#forcePass;
10994
+ try {
10995
+ this.#forceUpdate();
10996
+ }
10997
+ finally {
10998
+ if (outermost) {
10999
+ VNode.#forcePass = 0;
11000
+ }
11001
+ }
11002
+ }
11003
+ /**
11004
+ * The pass counter behind {@link forceUpdate}: the last pass number handed out.
11005
+ */
11006
+ static #forcePassCounter = 0;
11007
+ /**
11008
+ * The pass that is running, or 0 between passes.
11009
+ */
11010
+ static #forcePass = 0;
11011
+ /**
11012
+ * The pass this node was last forced in, or 0 if never.
11013
+ */
11014
+ #forcedInPass = 0;
11015
+ /**
11016
+ * Forces the children that have not been forced in the running pass. A v-if / v-for /
11017
+ * slot clone is a child of its host, and the host's directive renders it with its own
11018
+ * forceUpdate() call as soon as it is created, so recursing into it here would render it a
11019
+ * second time. A clone that already existed, and that the directive updated with
11020
+ * update(), has not been forced in this pass and is still forced here.
11021
+ */
11022
+ #forceChildren() {
11023
+ // A copy: an update can destroy a v-if / v-for clone, which removes it from this list.
11024
+ this.#childVNodes?.slice().forEach(childVNode => {
11025
+ if (childVNode.#forcedInPass !== VNode.#forcePass) {
11026
+ childVNode.forceUpdate();
11027
+ }
11028
+ });
11029
+ }
11030
+ /**
11031
+ * The body of {@link forceUpdate}, run within a pass.
11032
+ */
11033
+ #forceUpdate() {
10864
11034
  if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
10865
11035
  // If this is a text node with a text evaluator, update its content if needed
10866
11036
  this.#renderText();
@@ -10890,11 +11060,8 @@ class VNode {
10890
11060
  this.#renderSafely(() => updater.applyToDOM());
10891
11061
  }
10892
11062
  }
10893
- // Recursively update child virtual nodes. A copy: an update can
10894
- // destroy a v-if / v-for clone, which removes it from this list.
10895
- this.#childVNodes?.slice().forEach(childVNode => {
10896
- childVNode.forceUpdate();
10897
- });
11063
+ // Recursively update child virtual nodes
11064
+ this.#forceChildren();
10898
11065
  // If no directive requires template preservation, call onUpdated for directives that do not templatize
10899
11066
  if (!this.#templatized) {
10900
11067
  this.#directiveManager?.directives?.forEach(d => {
@@ -10904,9 +11071,7 @@ class VNode {
10904
11071
  }
10905
11072
  else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10906
11073
  // For document fragments, recursively force update child VNodes
10907
- this.#childVNodes?.slice().forEach(childVNode => {
10908
- childVNode.forceUpdate();
10909
- });
11074
+ this.#forceChildren();
10910
11075
  }
10911
11076
  }
10912
11077
  /**
@@ -15477,6 +15642,13 @@ class VApplication {
15477
15642
  * A dictionary mapping computed property names to their dependencies.
15478
15643
  */
15479
15644
  #computedDependencies;
15645
+ /**
15646
+ * The paths each computed property read the last time it was evaluated (see
15647
+ * ReactiveProxy.track). A change to one of them marks the computed dirty as well as
15648
+ * a change to the dependencies found in its source, since a read the source does not
15649
+ * spell out, such as `this[key]`, is seen only here.
15650
+ */
15651
+ #computedReads = new Map();
15480
15652
  /**
15481
15653
  * A map tracking source paths for computed property values.
15482
15654
  * Maps source path (e.g., "model.elements[0]") to computed property name (e.g., "selectedElement").
@@ -16046,20 +16218,33 @@ class VApplication {
16046
16218
  }
16047
16219
  /**
16048
16220
  * Marks computed properties as dirty (pull-based invalidation) when a dependency changes.
16049
- * Uses the statically analyzed dependency graph; because computed→computed and computed→method
16050
- * dependencies are flattened to their underlying reactive paths during analysis, a single change
16051
- * marks every transitively dependent computed dirty in one pass. The actual recomputation is
16052
- * deferred until the value is read (see #recomputeOne).
16221
+ * A computed depends on what its source names (statically analyzed, with computed→computed and
16222
+ * computed→method dependencies flattened to their underlying reactive paths) and on what it read
16223
+ * the last time it ran (#computedReads). A computed marked dirty is itself a change for the
16224
+ * computeds that read it, so marking follows those reads too. A computed already dirty is
16225
+ * skipped: the computeds that read it were marked when it was, and a computed cannot be
16226
+ * recomputed without reading — and so resolving — the dirty ones it depends on. The actual
16227
+ * recomputation is deferred until the value is read (see #recomputeOne).
16053
16228
  * @param identifier The changed identifier reported by the bindings change tracker.
16054
16229
  */
16055
16230
  #markDirtyComputeds(identifier) {
16056
16231
  if (!this.#options.computed || !this.#bindings) {
16057
16232
  return;
16058
16233
  }
16059
- for (const key of Object.keys(this.#computedDependencies)) {
16060
- const deps = this.#computedDependencies[key] || [];
16061
- if (deps.some(dep => this.#dependencyAffectedBy(dep, identifier))) {
16062
- this.#bindings.markComputedDirty(key);
16234
+ const pending = [identifier];
16235
+ while (pending.length > 0) {
16236
+ const changed = pending.pop();
16237
+ for (const key of Object.keys(this.#computedDependencies)) {
16238
+ if (this.#bindings.isComputedDirty(key)) {
16239
+ continue;
16240
+ }
16241
+ const deps = this.#computedDependencies[key] || [];
16242
+ const reads = this.#computedReads.get(key);
16243
+ if (deps.some(dep => this.#dependencyAffectedBy(dep, changed)) ||
16244
+ (reads !== undefined && ReactiveProxy.changeAffectsRead(changed, reads))) {
16245
+ this.#bindings.markComputedDirty(key);
16246
+ pending.push(key);
16247
+ }
16063
16248
  }
16064
16249
  }
16065
16250
  }
@@ -16119,10 +16304,21 @@ class VApplication {
16119
16304
  return;
16120
16305
  }
16121
16306
  const computedFn = VApplication.#getComputedGetter(def);
16307
+ const bindings = this.#bindings;
16122
16308
  try {
16123
16309
  // Read the previous value without triggering re-resolution, for change detection.
16124
- const oldValue = this.#bindings.peekComputed(key);
16125
- const newValue = computedFn.call(this.#bindings.raw);
16310
+ const oldValue = bindings.peekComputed(key);
16311
+ // Record what the getter reads, replacing the previous record: the reads can differ
16312
+ // from run to run (a branch, a dynamic key). Kept even when the getter throws, so a
16313
+ // change to what it read before failing retries it.
16314
+ const reads = new Set();
16315
+ let newValue;
16316
+ try {
16317
+ newValue = ReactiveProxy.track(reads, () => computedFn.call(bindings.raw));
16318
+ }
16319
+ finally {
16320
+ this.#computedReads.set(key, reads);
16321
+ }
16126
16322
  // Check if the value actually changed.
16127
16323
  let hasChanged = oldValue !== newValue;
16128
16324
  // For arrays, always treat as changed (VBindings detects length changes via its length