@mintjamsinc/ichigojs 0.1.87 → 0.1.88

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.
@@ -8843,6 +8843,7 @@ class VBindings {
8843
8843
  */
8844
8844
  clearChanges() {
8845
8845
  this.#changes.clear();
8846
+ this.#changeLog.length = 0;
8846
8847
  if (this.#dirtyScopes) {
8847
8848
  for (const scope of this.#dirtyScopes) {
8848
8849
  scope.#changes.clear();
@@ -8856,14 +8857,39 @@ class VBindings {
8856
8857
  */
8857
8858
  #addChange(key) {
8858
8859
  this.#changes.add(key);
8859
- if (!this.#parent) {
8860
- return;
8860
+ let root = this;
8861
+ while (root.#parent) {
8862
+ root = root.#parent;
8861
8863
  }
8864
+ root.#changeLog.push(key);
8865
+ if (root !== this) {
8866
+ (root.#dirtyScopes ??= new Set()).add(this);
8867
+ }
8868
+ }
8869
+ /**
8870
+ * Every change recorded since the last clearChanges(), in order and with repeats, on the
8871
+ * root; the change set itself cannot tell a write made during an update cycle from the one
8872
+ * the cycle was scheduled for when both touch the same identifier.
8873
+ */
8874
+ #changeLog = [];
8875
+ /**
8876
+ * A marker for {@link changesSince}: the number of changes recorded so far.
8877
+ */
8878
+ get changeLogLength() {
8879
+ return this.#root.#changeLog.length;
8880
+ }
8881
+ /**
8882
+ * The distinct identifiers recorded after the marker was taken.
8883
+ */
8884
+ changesSince(marker) {
8885
+ return Array.from(new Set(this.#root.#changeLog.slice(marker)));
8886
+ }
8887
+ get #root() {
8862
8888
  let root = this;
8863
8889
  while (root.#parent) {
8864
8890
  root = root.#parent;
8865
8891
  }
8866
- (root.#dirtyScopes ??= new Set()).add(this);
8892
+ return root;
8867
8893
  }
8868
8894
  /**
8869
8895
  * Sets a binding value.
@@ -15446,6 +15472,14 @@ class VApplication {
15446
15472
  * Flag to indicate if an update is already scheduled.
15447
15473
  */
15448
15474
  #updateScheduled = false;
15475
+ /**
15476
+ * How many update cycles in a row ended with changes made during the cycle (by a watcher
15477
+ * or a lifecycle hook), each of which runs another cycle. Reset when a cycle ends clean;
15478
+ * past MAX_CASCADED_UPDATES the late changes are dropped, so a watcher that writes on every
15479
+ * cycle cannot keep the application updating forever.
15480
+ */
15481
+ #cascadedUpdates = 0;
15482
+ static #MAX_CASCADED_UPDATES = 100;
15449
15483
  /**
15450
15484
  * The custom element hosting this application when it powers a component
15451
15485
  * defined via defineComponent(). Set by IchigoElement before mounting.
@@ -15889,8 +15923,10 @@ class VApplication {
15889
15923
  }
15890
15924
  this.#updateScheduled = true;
15891
15925
  queueMicrotask(() => {
15892
- this.#update();
15926
+ // Cleared before the update runs, not after: a change made during the update (by a
15927
+ // watcher or a lifecycle hook) then schedules the next cycle instead of being dropped.
15893
15928
  this.#updateScheduled = false;
15929
+ this.#update();
15894
15930
  });
15895
15931
  }
15896
15932
  /**
@@ -15904,14 +15940,46 @@ class VApplication {
15904
15940
  // This converts paths like "model.elements[0].executionListeners"
15905
15941
  // to "selectedElement.executionListeners" when selectedElement points to model.elements[0]
15906
15942
  this.#applyComputedPathMappings();
15943
+ // The changes this cycle was scheduled for. Whatever is recorded after this point was
15944
+ // written during the cycle itself (see below); the marker tells the two apart even when
15945
+ // a watcher writes the very identifier it watches.
15946
+ const scheduledChanges = this.#bindings ? this.#bindings.changes : [];
15947
+ const lateMarker = this.#bindings ? this.#bindings.changeLogLength : 0;
15907
15948
  // Notify watchers before DOM update
15908
15949
  if (this.#bindings) {
15909
- this.#watcher.notify(this.#bindings.changes, (p) => this.#getValueByPath(p), this.#bindings.raw);
15950
+ this.#watcher.notify(scheduledChanges, (p) => this.#getValueByPath(p), this.#bindings.raw);
15951
+ // A watcher may have written data. The computed properties that depend on it were
15952
+ // only marked dirty, which the DOM diff below does not see; resolve them now, so this
15953
+ // render already shows what the watcher did, and so the computed names are in the
15954
+ // change set when the diff runs.
15955
+ this.#flushDirtyComputeds();
15956
+ this.#applyComputedPathMappings();
15910
15957
  }
15911
15958
  // Update the DOM
15912
15959
  this.#vNode?.update();
15913
- // Clear the set of changed identifiers after the update
15914
- this.#bindings?.clearChanges();
15960
+ // Clear the set of changed identifiers after the update. Changes recorded during the
15961
+ // cycle (by watchers or lifecycle hooks) are carried into the next cycle instead: the DOM
15962
+ // diff above has seen them, but watchers that depend on them have not, since watcher
15963
+ // notification ran before they were written.
15964
+ if (this.#bindings) {
15965
+ const late = this.#bindings.changesSince(lateMarker);
15966
+ this.#bindings.clearChanges();
15967
+ if (late.length > 0) {
15968
+ this.#cascadedUpdates++;
15969
+ if (this.#cascadedUpdates > VApplication.#MAX_CASCADED_UPDATES) {
15970
+ this.#logger.warn(`Update cycle did not settle after ${VApplication.#MAX_CASCADED_UPDATES} cascaded updates; dropping changes: ${late.join(', ')}`);
15971
+ this.#cascadedUpdates = 0;
15972
+ return;
15973
+ }
15974
+ for (const change of late) {
15975
+ this.#bindings.markChanged(change);
15976
+ }
15977
+ this.#scheduleUpdate();
15978
+ }
15979
+ else {
15980
+ this.#cascadedUpdates = 0;
15981
+ }
15982
+ }
15915
15983
  }
15916
15984
  /**
15917
15985
  * Applies computed source path mappings to the current changes.