@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.
package/dist/ichigo.cjs CHANGED
@@ -8849,6 +8849,7 @@
8849
8849
  */
8850
8850
  clearChanges() {
8851
8851
  this.#changes.clear();
8852
+ this.#changeLog.length = 0;
8852
8853
  if (this.#dirtyScopes) {
8853
8854
  for (const scope of this.#dirtyScopes) {
8854
8855
  scope.#changes.clear();
@@ -8862,14 +8863,39 @@
8862
8863
  */
8863
8864
  #addChange(key) {
8864
8865
  this.#changes.add(key);
8865
- if (!this.#parent) {
8866
- return;
8866
+ let root = this;
8867
+ while (root.#parent) {
8868
+ root = root.#parent;
8867
8869
  }
8870
+ root.#changeLog.push(key);
8871
+ if (root !== this) {
8872
+ (root.#dirtyScopes ??= new Set()).add(this);
8873
+ }
8874
+ }
8875
+ /**
8876
+ * Every change recorded since the last clearChanges(), in order and with repeats, on the
8877
+ * root; the change set itself cannot tell a write made during an update cycle from the one
8878
+ * the cycle was scheduled for when both touch the same identifier.
8879
+ */
8880
+ #changeLog = [];
8881
+ /**
8882
+ * A marker for {@link changesSince}: the number of changes recorded so far.
8883
+ */
8884
+ get changeLogLength() {
8885
+ return this.#root.#changeLog.length;
8886
+ }
8887
+ /**
8888
+ * The distinct identifiers recorded after the marker was taken.
8889
+ */
8890
+ changesSince(marker) {
8891
+ return Array.from(new Set(this.#root.#changeLog.slice(marker)));
8892
+ }
8893
+ get #root() {
8868
8894
  let root = this;
8869
8895
  while (root.#parent) {
8870
8896
  root = root.#parent;
8871
8897
  }
8872
- (root.#dirtyScopes ??= new Set()).add(this);
8898
+ return root;
8873
8899
  }
8874
8900
  /**
8875
8901
  * Sets a binding value.
@@ -15452,6 +15478,14 @@
15452
15478
  * Flag to indicate if an update is already scheduled.
15453
15479
  */
15454
15480
  #updateScheduled = false;
15481
+ /**
15482
+ * How many update cycles in a row ended with changes made during the cycle (by a watcher
15483
+ * or a lifecycle hook), each of which runs another cycle. Reset when a cycle ends clean;
15484
+ * past MAX_CASCADED_UPDATES the late changes are dropped, so a watcher that writes on every
15485
+ * cycle cannot keep the application updating forever.
15486
+ */
15487
+ #cascadedUpdates = 0;
15488
+ static #MAX_CASCADED_UPDATES = 100;
15455
15489
  /**
15456
15490
  * The custom element hosting this application when it powers a component
15457
15491
  * defined via defineComponent(). Set by IchigoElement before mounting.
@@ -15895,8 +15929,10 @@
15895
15929
  }
15896
15930
  this.#updateScheduled = true;
15897
15931
  queueMicrotask(() => {
15898
- this.#update();
15932
+ // Cleared before the update runs, not after: a change made during the update (by a
15933
+ // watcher or a lifecycle hook) then schedules the next cycle instead of being dropped.
15899
15934
  this.#updateScheduled = false;
15935
+ this.#update();
15900
15936
  });
15901
15937
  }
15902
15938
  /**
@@ -15910,14 +15946,46 @@
15910
15946
  // This converts paths like "model.elements[0].executionListeners"
15911
15947
  // to "selectedElement.executionListeners" when selectedElement points to model.elements[0]
15912
15948
  this.#applyComputedPathMappings();
15949
+ // The changes this cycle was scheduled for. Whatever is recorded after this point was
15950
+ // written during the cycle itself (see below); the marker tells the two apart even when
15951
+ // a watcher writes the very identifier it watches.
15952
+ const scheduledChanges = this.#bindings ? this.#bindings.changes : [];
15953
+ const lateMarker = this.#bindings ? this.#bindings.changeLogLength : 0;
15913
15954
  // Notify watchers before DOM update
15914
15955
  if (this.#bindings) {
15915
- this.#watcher.notify(this.#bindings.changes, (p) => this.#getValueByPath(p), this.#bindings.raw);
15956
+ this.#watcher.notify(scheduledChanges, (p) => this.#getValueByPath(p), this.#bindings.raw);
15957
+ // A watcher may have written data. The computed properties that depend on it were
15958
+ // only marked dirty, which the DOM diff below does not see; resolve them now, so this
15959
+ // render already shows what the watcher did, and so the computed names are in the
15960
+ // change set when the diff runs.
15961
+ this.#flushDirtyComputeds();
15962
+ this.#applyComputedPathMappings();
15916
15963
  }
15917
15964
  // Update the DOM
15918
15965
  this.#vNode?.update();
15919
- // Clear the set of changed identifiers after the update
15920
- this.#bindings?.clearChanges();
15966
+ // Clear the set of changed identifiers after the update. Changes recorded during the
15967
+ // cycle (by watchers or lifecycle hooks) are carried into the next cycle instead: the DOM
15968
+ // diff above has seen them, but watchers that depend on them have not, since watcher
15969
+ // notification ran before they were written.
15970
+ if (this.#bindings) {
15971
+ const late = this.#bindings.changesSince(lateMarker);
15972
+ this.#bindings.clearChanges();
15973
+ if (late.length > 0) {
15974
+ this.#cascadedUpdates++;
15975
+ if (this.#cascadedUpdates > VApplication.#MAX_CASCADED_UPDATES) {
15976
+ this.#logger.warn(`Update cycle did not settle after ${VApplication.#MAX_CASCADED_UPDATES} cascaded updates; dropping changes: ${late.join(', ')}`);
15977
+ this.#cascadedUpdates = 0;
15978
+ return;
15979
+ }
15980
+ for (const change of late) {
15981
+ this.#bindings.markChanged(change);
15982
+ }
15983
+ this.#scheduleUpdate();
15984
+ }
15985
+ else {
15986
+ this.#cascadedUpdates = 0;
15987
+ }
15988
+ }
15921
15989
  }
15922
15990
  /**
15923
15991
  * Applies computed source path mappings to the current changes.