@mintjamsinc/ichigojs 0.1.70 → 0.1.72

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.
@@ -6939,42 +6939,70 @@ class ExpressionUtils {
6939
6939
  functionDependencies[funcName] = [];
6940
6940
  }
6941
6941
  }
6942
- // Second pass: recursively resolve function dependencies
6943
- const resolvedDependencies = {};
6944
- const resolving = new Set(); // To detect circular dependencies
6945
- const resolveDependencies = (funcName) => {
6946
- // If already resolved, return cached result
6947
- if (resolvedDependencies[funcName]) {
6948
- return resolvedDependencies[funcName];
6949
- }
6950
- // Check for circular dependency
6951
- if (resolving.has(funcName)) {
6952
- console.warn(`Circular dependency detected for function: ${funcName}`);
6953
- return [];
6954
- }
6955
- resolving.add(funcName);
6956
- const allDependencies = new Set();
6957
- const directDependencies = functionDependencies[funcName] || [];
6958
- for (const dep of directDependencies) {
6942
+ // Second pass: resolve each function's transitive reactive dependencies.
6943
+ //
6944
+ // A function's direct dependency list mixes two kinds of identifiers: references to other
6945
+ // functions (call edges) and references to reactive data properties (the values we actually
6946
+ // care about). We want, for every function, the full set of reactive properties reachable by
6947
+ // following call edges to any depth.
6948
+ //
6949
+ // Functions are free to reference each other cyclically — a poller that reschedules itself
6950
+ // (`scheduleOperationPoll` -> `pollOperation` -> `scheduleOperationPoll`), a pair of mutually
6951
+ // recursive helpers, or any state machine. This is valid JavaScript, so the resolver must not
6952
+ // treat it as an error. Every function in a cycle (more precisely, a strongly connected
6953
+ // component) is reachable from the others, so they all share the same transitive dependency
6954
+ // set: the union of every member's direct reactive dependencies.
6955
+ //
6956
+ // A depth-first walk that bails out on back-edges cannot compute that correctly — it drops the
6957
+ // dependencies contributed around the cycle and caches incomplete, traversal-order-dependent
6958
+ // results, which would silently break reactivity for any computed that reads reactive data
6959
+ // through a cyclic method. We instead split each function's direct dependencies into reactive
6960
+ // properties and call edges, then propagate reactive properties along call edges until the sets
6961
+ // stop growing (a fixpoint). This converges regardless of cycles and yields the exact transitive
6962
+ // closure for every function, including those participating in a cycle.
6963
+ const directReactive = {};
6964
+ const callEdges = {};
6965
+ for (const funcName of Object.keys(functions)) {
6966
+ const reactive = new Set();
6967
+ const edges = [];
6968
+ for (const dep of functionDependencies[funcName] || []) {
6959
6969
  if (dep === funcName)
6960
- continue; // Skip self-references
6970
+ continue; // Skip self-references.
6961
6971
  if (functions[dep]) {
6962
- // It's a function, recursively resolve its dependencies
6963
- const subDependencies = resolveDependencies(dep);
6964
- subDependencies.forEach(subDep => allDependencies.add(subDep));
6972
+ edges.push(dep); // Call edge to another function.
6965
6973
  }
6966
6974
  else {
6967
- // It's a variable, add directly
6968
- allDependencies.add(dep);
6975
+ reactive.add(dep); // Reactive data property (terminal dependency).
6969
6976
  }
6970
6977
  }
6971
- resolving.delete(funcName);
6972
- resolvedDependencies[funcName] = Array.from(allDependencies);
6973
- return resolvedDependencies[funcName];
6974
- };
6975
- // Resolve all functions
6978
+ directReactive[funcName] = reactive;
6979
+ callEdges[funcName] = edges;
6980
+ }
6981
+ // Seed each function with its own direct reactive dependencies, then propagate along call edges
6982
+ // until no set changes. Each pass can only add properties, and the universe of properties is
6983
+ // finite, so the loop is guaranteed to terminate.
6984
+ const resolved = {};
6976
6985
  for (const funcName of Object.keys(functions)) {
6977
- resolveDependencies(funcName);
6986
+ resolved[funcName] = new Set(directReactive[funcName]);
6987
+ }
6988
+ let changed = true;
6989
+ while (changed) {
6990
+ changed = false;
6991
+ for (const funcName of Object.keys(functions)) {
6992
+ const target = resolved[funcName];
6993
+ for (const edge of callEdges[funcName]) {
6994
+ for (const dep of resolved[edge]) {
6995
+ if (!target.has(dep)) {
6996
+ target.add(dep);
6997
+ changed = true;
6998
+ }
6999
+ }
7000
+ }
7001
+ }
7002
+ }
7003
+ const resolvedDependencies = {};
7004
+ for (const funcName of Object.keys(functions)) {
7005
+ resolvedDependencies[funcName] = Array.from(resolved[funcName]);
6978
7006
  }
6979
7007
  return resolvedDependencies;
6980
7008
  }
@@ -13751,7 +13779,8 @@ class VApplication {
13751
13779
  this.#logManager = new VLogManager(options.logLevel);
13752
13780
  this.#logger = this.#logManager.getLogger('VApplication');
13753
13781
  // Analyze function dependencies
13754
- this.#functionDependencies = ExpressionUtils.analyzeFunctionDependencies(options.methods || {});
13782
+ const methods = (options.methods || {});
13783
+ this.#functionDependencies = ExpressionUtils.analyzeFunctionDependencies(methods);
13755
13784
  // Analyze computed dependencies based on getter functions only.
13756
13785
  // Writable computeds (defined as { get, set }) contribute their getter for dependency analysis.
13757
13786
  const computedGetters = {};
@@ -13760,7 +13789,23 @@ class VApplication {
13760
13789
  computedGetters[key] = VApplication.#getComputedGetter(def);
13761
13790
  }
13762
13791
  }
13763
- this.#computedDependencies = ExpressionUtils.analyzeFunctionDependencies(computedGetters);
13792
+ // Resolve computed dependencies against BOTH the computed getters AND the methods, so a
13793
+ // computed that delegates to a method inherits that method's reactive dependencies. A
13794
+ // computed whose only reactive reads happen inside a called method (e.g. a reactive i18n
13795
+ // `t()` helper that reads `this.localization`) would otherwise never be invalidated, leaving
13796
+ // its binding stale on a dependency change. Analyzing the combined set flattens every
13797
+ // computed→method (and computed→computed, method→method) edge down to the underlying
13798
+ // reactive paths — the same expansion that template-expression analysis already performs for
13799
+ // method calls (see ExpressionUtils.extractIdentifiers). We then keep only the computed
13800
+ // entries, since #computedDependencies must be keyed by computed name alone.
13801
+ const combinedDependencies = ExpressionUtils.analyzeFunctionDependencies({
13802
+ ...methods,
13803
+ ...computedGetters,
13804
+ });
13805
+ this.#computedDependencies = {};
13806
+ for (const key of Object.keys(computedGetters)) {
13807
+ this.#computedDependencies[key] = combinedDependencies[key] || [];
13808
+ }
13764
13809
  // Initialize watcher manager
13765
13810
  this.#watcher = new VWatcher(this.#logger);
13766
13811
  // Initialize bindings from data, computed, and methods
@@ -14114,10 +14159,10 @@ class VApplication {
14114
14159
  }
14115
14160
  /**
14116
14161
  * Marks computed properties as dirty (pull-based invalidation) when a dependency changes.
14117
- * Uses the statically analyzed dependency graph; because computed→computed dependencies are
14118
- * flattened to their underlying reactive paths during analysis, a single change marks every
14119
- * transitively dependent computed dirty in one pass. The actual recomputation is deferred until
14120
- * the value is read (see #recomputeOne).
14162
+ * Uses the statically analyzed dependency graph; because computed→computed and computed→method
14163
+ * dependencies are flattened to their underlying reactive paths during analysis, a single change
14164
+ * marks every transitively dependent computed dirty in one pass. The actual recomputation is
14165
+ * deferred until the value is read (see #recomputeOne).
14121
14166
  * @param identifier The changed identifier reported by the bindings change tracker.
14122
14167
  */
14123
14168
  #markDirtyComputeds(identifier) {