@mintjamsinc/ichigojs 0.1.88 → 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) {
@@ -10795,8 +10918,7 @@ class VNode {
10795
10918
  const changed = this.bindings.isAffected(changes, this.#textEvaluator.identifiers, this.#textEvaluator.trackedPaths);
10796
10919
  // If the text node has changed, update its content
10797
10920
  if (changed) {
10798
- const text = this.#node;
10799
- text.data = this.#textEvaluator.evaluate(this.bindings);
10921
+ this.#renderText();
10800
10922
  }
10801
10923
  }
10802
10924
  else if (this.#nodeType === Node.ELEMENT_NODE) {
@@ -10817,7 +10939,7 @@ class VNode {
10817
10939
  for (const preparer of this.#directiveManager.bindingsPreparers) {
10818
10940
  const changed = this.bindings.isAffected(changes, preparer.dependentIdentifiers, preparer.trackedPaths);
10819
10941
  if (changed) {
10820
- preparer.prepareBindings();
10942
+ this.#renderSafely(() => preparer.prepareBindings());
10821
10943
  }
10822
10944
  }
10823
10945
  }
@@ -10826,14 +10948,13 @@ class VNode {
10826
10948
  for (const updater of this.#directiveManager.domUpdaters) {
10827
10949
  const changed = this.bindings.isAffected(changes, updater.dependentIdentifiers, updater.trackedPaths);
10828
10950
  if (changed) {
10829
- updater.applyToDOM();
10951
+ this.#renderSafely(() => updater.applyToDOM());
10830
10952
  }
10831
10953
  }
10832
10954
  }
10833
10955
  // Recursively update dependent virtual nodes
10834
10956
  this.#dependents?.forEach(dependentNode => {
10835
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10836
- if (changed) {
10957
+ if (dependentNode.isAffectedBy(changes)) {
10837
10958
  dependentNode.update();
10838
10959
  }
10839
10960
  });
@@ -10848,8 +10969,7 @@ class VNode {
10848
10969
  // For document fragments (e.g. from <template v-if>), propagate updates
10849
10970
  // to dependent virtual nodes so that child bindings stay reactive.
10850
10971
  this.#dependents?.forEach(dependentNode => {
10851
- const changed = dependentNode.dependentIdentifiers.some(id => changes.some(change => dependentNode.bindings.doesChangeMatchIdentifier(change, id)));
10852
- if (changed) {
10972
+ if (dependentNode.isAffectedBy(changes)) {
10853
10973
  dependentNode.update();
10854
10974
  }
10855
10975
  });
@@ -10862,10 +10982,58 @@ class VNode {
10862
10982
  * This is useful when an immediate update is needed, bypassing the usual change detection.
10863
10983
  */
10864
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() {
10865
11034
  if (this.#nodeType === Node.TEXT_NODE && this.#textEvaluator) {
10866
11035
  // If this is a text node with a text evaluator, update its content if needed
10867
- const text = this.#node;
10868
- text.data = this.#textEvaluator.evaluate(this.bindings);
11036
+ this.#renderText();
10869
11037
  }
10870
11038
  else if (this.#nodeType === Node.ELEMENT_NODE) {
10871
11039
  // If this is an element node, update directives and child nodes
@@ -10883,20 +11051,17 @@ class VNode {
10883
11051
  }
10884
11052
  // Prepare bindings for each preparer if relevant identifiers have changed
10885
11053
  for (const preparer of this.#directiveManager.bindingsPreparers) {
10886
- preparer.prepareBindings();
11054
+ this.#renderSafely(() => preparer.prepareBindings());
10887
11055
  }
10888
11056
  }
10889
11057
  // Apply DOM updaters from directives, if any
10890
11058
  if (this.#directiveManager?.domUpdaters) {
10891
11059
  for (const updater of this.#directiveManager.domUpdaters) {
10892
- updater.applyToDOM();
11060
+ this.#renderSafely(() => updater.applyToDOM());
10893
11061
  }
10894
11062
  }
10895
- // Recursively update child virtual nodes. A copy: an update can
10896
- // destroy a v-if / v-for clone, which removes it from this list.
10897
- this.#childVNodes?.slice().forEach(childVNode => {
10898
- childVNode.forceUpdate();
10899
- });
11063
+ // Recursively update child virtual nodes
11064
+ this.#forceChildren();
10900
11065
  // If no directive requires template preservation, call onUpdated for directives that do not templatize
10901
11066
  if (!this.#templatized) {
10902
11067
  this.#directiveManager?.directives?.forEach(d => {
@@ -10906,10 +11071,40 @@ class VNode {
10906
11071
  }
10907
11072
  else if (this.#nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10908
11073
  // For document fragments, recursively force update child VNodes
10909
- this.#childVNodes?.slice().forEach(childVNode => {
10910
- childVNode.forceUpdate();
10911
- });
11074
+ this.#forceChildren();
11075
+ }
11076
+ }
11077
+ /**
11078
+ * Evaluates this text node's expressions into its content.
11079
+ */
11080
+ #renderText() {
11081
+ const text = this.#node;
11082
+ this.#renderSafely(() => {
11083
+ text.data = this.#textEvaluator.evaluate(this.bindings);
11084
+ });
11085
+ }
11086
+ /**
11087
+ * Runs one binding's render step. A binding whose expression throws (for example one that
11088
+ * reads a member of a value that is still undefined) is logged and left as it was, so that
11089
+ * it does not stop the rest of the tree from rendering.
11090
+ * @param render The render step.
11091
+ */
11092
+ #renderSafely(render) {
11093
+ try {
11094
+ render();
11095
+ }
11096
+ catch (error) {
11097
+ this.#vApplication.logManager.getLogger(this.constructor.name).error(`Error rendering ${this.#describe()}: ${error}`);
11098
+ }
11099
+ }
11100
+ /**
11101
+ * Describes this node for a log message: a text node by its text, an element by its tag.
11102
+ */
11103
+ #describe() {
11104
+ if (this.#nodeType === Node.TEXT_NODE) {
11105
+ return `text "${(this.#node.textContent ?? '').trim()}"`;
10912
11106
  }
11107
+ return `<${this.#nodeName.toLowerCase()}>`;
10913
11108
  }
10914
11109
  /**
10915
11110
  * Adds a child virtual node to this virtual node.
@@ -15447,6 +15642,13 @@ class VApplication {
15447
15642
  * A dictionary mapping computed property names to their dependencies.
15448
15643
  */
15449
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();
15450
15652
  /**
15451
15653
  * A map tracking source paths for computed property values.
15452
15654
  * Maps source path (e.g., "model.elements[0]") to computed property name (e.g., "selectedElement").
@@ -15637,8 +15839,11 @@ class VApplication {
15637
15839
  vApplication: this,
15638
15840
  bindings: this.#bindings
15639
15841
  });
15640
- // Initial rendering
15641
- this.#vNode.update();
15842
+ // Initial rendering. Every binding is applied, not only those whose identifiers were
15843
+ // recorded as changed while the data was set up: a value that starts undefined is never
15844
+ // recorded (undefined -> undefined is no change), and an expression over globals only
15845
+ // has no identifier to record at all.
15846
+ this.#vNode.forceUpdate();
15642
15847
  // Expand the components in the tree. Last, so every component element
15643
15848
  // has been compiled and has had its prop values delivered before its
15644
15849
  // template is cloned in. See VNode.expandComponents.
@@ -16013,20 +16218,33 @@ class VApplication {
16013
16218
  }
16014
16219
  /**
16015
16220
  * Marks computed properties as dirty (pull-based invalidation) when a dependency changes.
16016
- * Uses the statically analyzed dependency graph; because computed→computed and computed→method
16017
- * dependencies are flattened to their underlying reactive paths during analysis, a single change
16018
- * marks every transitively dependent computed dirty in one pass. The actual recomputation is
16019
- * 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).
16020
16228
  * @param identifier The changed identifier reported by the bindings change tracker.
16021
16229
  */
16022
16230
  #markDirtyComputeds(identifier) {
16023
16231
  if (!this.#options.computed || !this.#bindings) {
16024
16232
  return;
16025
16233
  }
16026
- for (const key of Object.keys(this.#computedDependencies)) {
16027
- const deps = this.#computedDependencies[key] || [];
16028
- if (deps.some(dep => this.#dependencyAffectedBy(dep, identifier))) {
16029
- 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
+ }
16030
16248
  }
16031
16249
  }
16032
16250
  }
@@ -16086,10 +16304,21 @@ class VApplication {
16086
16304
  return;
16087
16305
  }
16088
16306
  const computedFn = VApplication.#getComputedGetter(def);
16307
+ const bindings = this.#bindings;
16089
16308
  try {
16090
16309
  // Read the previous value without triggering re-resolution, for change detection.
16091
- const oldValue = this.#bindings.peekComputed(key);
16092
- 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
+ }
16093
16322
  // Check if the value actually changed.
16094
16323
  let hasChanged = oldValue !== newValue;
16095
16324
  // For arrays, always treat as changed (VBindings detects length changes via its length