@mintjamsinc/ichigojs 0.1.68 → 0.1.70
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/README.md +312 -6
- package/dist/ichigo.cjs +290 -124
- package/dist/ichigo.cjs.map +1 -1
- package/dist/ichigo.esm.js +290 -124
- package/dist/ichigo.esm.js.map +1 -1
- package/dist/ichigo.esm.min.js +1 -1
- package/dist/ichigo.min.cjs +1 -1
- package/dist/ichigo.umd.js +290 -124
- package/dist/ichigo.umd.js.map +1 -1
- package/dist/ichigo.umd.min.js +1 -1
- package/dist/types/ichigo/VBindings.d.ts +27 -0
- package/package.json +1 -1
package/dist/ichigo.esm.js
CHANGED
|
@@ -8393,6 +8393,31 @@ class VBindings {
|
|
|
8393
8393
|
* is replaced, the binding is set to null/undefined, or the bindings are destroyed.
|
|
8394
8394
|
*/
|
|
8395
8395
|
#externalSubscriptions = new Map();
|
|
8396
|
+
/**
|
|
8397
|
+
* Recompute callbacks for computed properties registered on these bindings.
|
|
8398
|
+
* Maps a computed property name to a function that recomputes and caches its value.
|
|
8399
|
+
* Only the bindings instance that owns the computed (typically the root) holds an entry;
|
|
8400
|
+
* child bindings resolve computed reads by delegating to their parent through the proxy.
|
|
8401
|
+
*/
|
|
8402
|
+
#computedRecomputers = new Map();
|
|
8403
|
+
/**
|
|
8404
|
+
* The set of computed properties whose cached value is stale and must be recomputed on next
|
|
8405
|
+
* access (pull-based evaluation). Populated by markComputedDirty when a dependency changes,
|
|
8406
|
+
* and cleared as each computed is resolved.
|
|
8407
|
+
*/
|
|
8408
|
+
#dirtyComputeds = new Set();
|
|
8409
|
+
/**
|
|
8410
|
+
* Guard set used to detect re-entrant (circular) computed resolution. While a computed is
|
|
8411
|
+
* being recomputed its name is held here; a nested read of the same computed returns the
|
|
8412
|
+
* currently cached (stale) value instead of recursing infinitely.
|
|
8413
|
+
*/
|
|
8414
|
+
#resolvingComputeds = new Set();
|
|
8415
|
+
/**
|
|
8416
|
+
* The plain backing object behind the #local proxy. Kept as a direct reference so the cached
|
|
8417
|
+
* value of a computed property can be read without triggering pull-based re-evaluation
|
|
8418
|
+
* (see peekComputed).
|
|
8419
|
+
*/
|
|
8420
|
+
#store = {};
|
|
8396
8421
|
/**
|
|
8397
8422
|
* Creates a new instance of VBindings.
|
|
8398
8423
|
* @param parent The parent bindings, if any.
|
|
@@ -8404,8 +8429,13 @@ class VBindings {
|
|
|
8404
8429
|
if (this.#logger?.isDebugEnabled) {
|
|
8405
8430
|
this.#logger.debug(`VBindings created. Parent: ${this.#parent ? 'yes' : 'no'}`);
|
|
8406
8431
|
}
|
|
8407
|
-
this.#local = new Proxy(
|
|
8432
|
+
this.#local = new Proxy(this.#store, {
|
|
8408
8433
|
get: (obj, key) => {
|
|
8434
|
+
// Pull-based evaluation: if this key is a dirty computed property, recompute it now
|
|
8435
|
+
// so the read returns a fresh value (synchronously, even before the update microtask).
|
|
8436
|
+
if (typeof key === 'string') {
|
|
8437
|
+
this.#resolveComputedIfDirty(key);
|
|
8438
|
+
}
|
|
8409
8439
|
if (Reflect.has(obj, key)) {
|
|
8410
8440
|
return Reflect.get(obj, key);
|
|
8411
8441
|
}
|
|
@@ -8663,6 +8693,79 @@ class VBindings {
|
|
|
8663
8693
|
registerWritableComputed(key, setter) {
|
|
8664
8694
|
this.#writableComputeds.set(key, setter);
|
|
8665
8695
|
}
|
|
8696
|
+
/**
|
|
8697
|
+
* Registers a computed property for pull-based (lazy) evaluation. The provided recompute
|
|
8698
|
+
* callback is invoked the first time the property is read after it has been marked dirty
|
|
8699
|
+
* (or during the pre-render flush), and is expected to update the cached value (typically
|
|
8700
|
+
* via setSilent).
|
|
8701
|
+
* @param key The computed property name.
|
|
8702
|
+
* @param recompute The callback that recomputes and caches the property's value.
|
|
8703
|
+
*/
|
|
8704
|
+
registerComputed(key, recompute) {
|
|
8705
|
+
this.#computedRecomputers.set(key, recompute);
|
|
8706
|
+
}
|
|
8707
|
+
/**
|
|
8708
|
+
* Marks a computed property as dirty so it will be recomputed on next access.
|
|
8709
|
+
* @param key The computed property name.
|
|
8710
|
+
*/
|
|
8711
|
+
markComputedDirty(key) {
|
|
8712
|
+
this.#dirtyComputeds.add(key);
|
|
8713
|
+
}
|
|
8714
|
+
/**
|
|
8715
|
+
* Reads the currently cached value of a computed property without triggering pull-based
|
|
8716
|
+
* re-evaluation. Used by the recompute routine to obtain the previous value for change
|
|
8717
|
+
* detection. Falls back to the parent bindings when the key is not stored locally.
|
|
8718
|
+
* @param key The computed property name.
|
|
8719
|
+
* @returns The cached value, or undefined if not cached.
|
|
8720
|
+
*/
|
|
8721
|
+
peekComputed(key) {
|
|
8722
|
+
if (Object.prototype.hasOwnProperty.call(this.#store, key)) {
|
|
8723
|
+
return this.#store[key];
|
|
8724
|
+
}
|
|
8725
|
+
return this.#parent?.peekComputed(key);
|
|
8726
|
+
}
|
|
8727
|
+
/**
|
|
8728
|
+
* Forces resolution of every computed property currently marked dirty. Called before the DOM
|
|
8729
|
+
* diff and watcher notification so that the set of changed identifiers is complete.
|
|
8730
|
+
*/
|
|
8731
|
+
flushDirtyComputeds() {
|
|
8732
|
+
for (const key of [...this.#dirtyComputeds]) {
|
|
8733
|
+
this.#resolveComputedIfDirty(key);
|
|
8734
|
+
}
|
|
8735
|
+
}
|
|
8736
|
+
/**
|
|
8737
|
+
* Resolves a computed property if its cached value is dirty (pull-based evaluation), by
|
|
8738
|
+
* invoking its registered recompute callback. Computed→computed chains resolve naturally:
|
|
8739
|
+
* reading another computed inside the getter re-enters this method for that key. Re-entrant
|
|
8740
|
+
* resolution of the same key (a circular dependency) is detected and short-circuited, leaving
|
|
8741
|
+
* the previously cached value in place.
|
|
8742
|
+
* @param key The property name to resolve.
|
|
8743
|
+
*/
|
|
8744
|
+
#resolveComputedIfDirty(key) {
|
|
8745
|
+
const recompute = this.#computedRecomputers.get(key);
|
|
8746
|
+
if (!recompute) {
|
|
8747
|
+
// Not a computed owned by these bindings; a parent (if any) resolves it when the value
|
|
8748
|
+
// is read through the proxy delegation in the get trap.
|
|
8749
|
+
return;
|
|
8750
|
+
}
|
|
8751
|
+
if (!this.#dirtyComputeds.has(key)) {
|
|
8752
|
+
return;
|
|
8753
|
+
}
|
|
8754
|
+
if (this.#resolvingComputeds.has(key)) {
|
|
8755
|
+
this.#logger?.warn(`Circular dependency detected while resolving computed property '${key}'.`);
|
|
8756
|
+
return;
|
|
8757
|
+
}
|
|
8758
|
+
this.#resolvingComputeds.add(key);
|
|
8759
|
+
try {
|
|
8760
|
+
// Clear the dirty flag before recomputing so reads of this same key during recomputation
|
|
8761
|
+
// (other than a true cycle) do not attempt to resolve it again.
|
|
8762
|
+
this.#dirtyComputeds.delete(key);
|
|
8763
|
+
recompute();
|
|
8764
|
+
}
|
|
8765
|
+
finally {
|
|
8766
|
+
this.#resolvingComputeds.delete(key);
|
|
8767
|
+
}
|
|
8768
|
+
}
|
|
8666
8769
|
/**
|
|
8667
8770
|
* Manually adds an identifier to the set of changed identifiers.
|
|
8668
8771
|
* This is useful for computed properties that need to mark themselves as changed
|
|
@@ -10264,9 +10367,18 @@ class VForDirective {
|
|
|
10264
10367
|
#sourceName;
|
|
10265
10368
|
#useOfSyntax = false; // Track if 'of' syntax was used
|
|
10266
10369
|
/**
|
|
10267
|
-
*
|
|
10370
|
+
* Ordered list of currently rendered items.
|
|
10371
|
+
*
|
|
10372
|
+
* This is intentionally an ordered array of { key, vNode } entries rather
|
|
10373
|
+
* than a Map<key, VNode>. The :key attribute is a *reconciliation hint*
|
|
10374
|
+
* used to identify and reuse the same logical row across re-renders — it is
|
|
10375
|
+
* not the identity of the rendered set. Keying the rendered set by a Map
|
|
10376
|
+
* would make it structurally impossible to hold two rows that resolve to
|
|
10377
|
+
* the same key, which would silently drop application data. The most
|
|
10378
|
+
* fundamental invariant of a list directive is "N items in => N rows out",
|
|
10379
|
+
* so the rendered set must be a positional list that can carry duplicates.
|
|
10268
10380
|
*/
|
|
10269
|
-
#renderedItems =
|
|
10381
|
+
#renderedItems = [];
|
|
10270
10382
|
/**
|
|
10271
10383
|
* Previous iterations to detect changes
|
|
10272
10384
|
*/
|
|
@@ -10389,11 +10501,11 @@ class VForDirective {
|
|
|
10389
10501
|
destroy() {
|
|
10390
10502
|
// Clean up all rendered items
|
|
10391
10503
|
// First destroy all VNodes (calls @unmount hooks), then remove from DOM
|
|
10392
|
-
for (const vNode of this.#renderedItems
|
|
10504
|
+
for (const { vNode } of this.#renderedItems) {
|
|
10393
10505
|
vNode.destroy();
|
|
10394
10506
|
}
|
|
10395
10507
|
// Then remove DOM nodes
|
|
10396
|
-
for (const vNode of this.#renderedItems
|
|
10508
|
+
for (const { vNode } of this.#renderedItems) {
|
|
10397
10509
|
const range = vNode.fragmentRange;
|
|
10398
10510
|
if (range) {
|
|
10399
10511
|
range.remove();
|
|
@@ -10404,7 +10516,7 @@ class VForDirective {
|
|
|
10404
10516
|
vNode.node.parentNode.removeChild(vNode.node);
|
|
10405
10517
|
}
|
|
10406
10518
|
}
|
|
10407
|
-
this.#renderedItems
|
|
10519
|
+
this.#renderedItems = [];
|
|
10408
10520
|
this.#previousIterations = [];
|
|
10409
10521
|
}
|
|
10410
10522
|
/**
|
|
@@ -10445,7 +10557,24 @@ class VForDirective {
|
|
|
10445
10557
|
this.#previousIterations = iterations;
|
|
10446
10558
|
}
|
|
10447
10559
|
/**
|
|
10448
|
-
* Key-based diffing for efficient DOM updates
|
|
10560
|
+
* Key-based diffing for efficient DOM updates.
|
|
10561
|
+
*
|
|
10562
|
+
* Reconciliation model
|
|
10563
|
+
* --------------------
|
|
10564
|
+
* The :key attribute is treated as a *hint* for reusing the same logical
|
|
10565
|
+
* row across re-renders, not as the identity of the rendered set. The
|
|
10566
|
+
* previously rendered rows are placed into a pool keyed by :key (a queue
|
|
10567
|
+
* per key, so equal keys can hold more than one row). Each incoming
|
|
10568
|
+
* iteration then claims a row from its key's queue when one is available,
|
|
10569
|
+
* otherwise a fresh row is created. Whatever stays in the pool at the end
|
|
10570
|
+
* is genuinely gone and is destroyed and removed.
|
|
10571
|
+
*
|
|
10572
|
+
* This guarantees the directive's most fundamental invariant — "N items in
|
|
10573
|
+
* => N rows out" — even when the application supplies duplicate keys. We
|
|
10574
|
+
* still warn on duplicates because they make reuse ambiguous (reordering
|
|
10575
|
+
* becomes positional rather than identity-stable), but we never silently
|
|
10576
|
+
* drop the application's data, and no row is ever orphaned: every old row
|
|
10577
|
+
* is either reused or explicitly removed.
|
|
10449
10578
|
*/
|
|
10450
10579
|
#updateList(newIterations) {
|
|
10451
10580
|
const parent = this.#vNode.anchorNode?.parentNode;
|
|
@@ -10453,22 +10582,38 @@ class VForDirective {
|
|
|
10453
10582
|
if (!parent || !anchor) {
|
|
10454
10583
|
throw new Error('v-for element must have a parent and anchor');
|
|
10455
10584
|
}
|
|
10456
|
-
|
|
10457
|
-
//
|
|
10458
|
-
|
|
10585
|
+
// Build a reuse pool from the currently rendered rows. A queue per key
|
|
10586
|
+
// (FIFO) lets duplicate keys reuse multiple rows: the first incoming
|
|
10587
|
+
// occurrence claims the first existing row, the second claims the next,
|
|
10588
|
+
// and so on.
|
|
10589
|
+
const pool = new Map();
|
|
10590
|
+
for (const { key, vNode } of this.#renderedItems) {
|
|
10591
|
+
let queue = pool.get(key);
|
|
10592
|
+
if (!queue) {
|
|
10593
|
+
queue = [];
|
|
10594
|
+
pool.set(key, queue);
|
|
10595
|
+
}
|
|
10596
|
+
queue.push(vNode);
|
|
10597
|
+
}
|
|
10598
|
+
// Decide, for each incoming iteration in order, whether it reuses an
|
|
10599
|
+
// existing row or needs a new one. Reused rows are taken out of the
|
|
10600
|
+
// pool so that what remains afterwards is exactly the set to remove.
|
|
10459
10601
|
const seenKeys = new Set();
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10602
|
+
const plan = [];
|
|
10603
|
+
for (const context of newIterations) {
|
|
10604
|
+
if (seenKeys.has(context.key)) {
|
|
10605
|
+
console.warn(`[ichigo.js] Duplicate key detected in v-for: "${context.key}". All entries are still rendered, but reordering may be unstable. Keys should be unique.`);
|
|
10463
10606
|
}
|
|
10464
|
-
seenKeys.add(
|
|
10465
|
-
|
|
10607
|
+
seenKeys.add(context.key);
|
|
10608
|
+
const queue = pool.get(context.key);
|
|
10609
|
+
const reused = queue && queue.length ? queue.shift() : undefined;
|
|
10610
|
+
plan.push({ context, reused });
|
|
10466
10611
|
}
|
|
10467
|
-
// Remove
|
|
10612
|
+
// Remove rows that were not reused.
|
|
10468
10613
|
// First destroy VNodes (calls @unmount hooks while DOM is still accessible)
|
|
10469
10614
|
const nodesToRemove = [];
|
|
10470
|
-
for (const
|
|
10471
|
-
|
|
10615
|
+
for (const queue of pool.values()) {
|
|
10616
|
+
for (const vNode of queue) {
|
|
10472
10617
|
nodesToRemove.push(vNode);
|
|
10473
10618
|
vNode.destroy();
|
|
10474
10619
|
}
|
|
@@ -10487,11 +10632,12 @@ class VForDirective {
|
|
|
10487
10632
|
parentOfNode.removeChild(vNode.node);
|
|
10488
10633
|
}
|
|
10489
10634
|
}
|
|
10490
|
-
// Add or reorder
|
|
10635
|
+
// Add or reorder rows, building the new ordered rendered set.
|
|
10636
|
+
const newRenderedItems = [];
|
|
10491
10637
|
let prevNode = anchor;
|
|
10492
|
-
for (const context of
|
|
10638
|
+
for (const { context, reused } of plan) {
|
|
10493
10639
|
const { key } = context;
|
|
10494
|
-
let vNode =
|
|
10640
|
+
let vNode = reused;
|
|
10495
10641
|
if (!vNode) {
|
|
10496
10642
|
// Create new item
|
|
10497
10643
|
const clone = this.#cloneNode();
|
|
@@ -10523,7 +10669,7 @@ class VForDirective {
|
|
|
10523
10669
|
if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
10524
10670
|
const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
|
|
10525
10671
|
vNode.fragmentRange = range;
|
|
10526
|
-
newRenderedItems.
|
|
10672
|
+
newRenderedItems.push({ key, vNode });
|
|
10527
10673
|
vNode.forceUpdate();
|
|
10528
10674
|
prevNode = range.lastNode;
|
|
10529
10675
|
continue;
|
|
@@ -10537,12 +10683,12 @@ class VForDirective {
|
|
|
10537
10683
|
else {
|
|
10538
10684
|
parent.appendChild(nodeToInsert);
|
|
10539
10685
|
}
|
|
10540
|
-
newRenderedItems.
|
|
10686
|
+
newRenderedItems.push({ key, vNode });
|
|
10541
10687
|
vNode.forceUpdate();
|
|
10542
10688
|
}
|
|
10543
10689
|
else {
|
|
10544
10690
|
// Reuse existing item
|
|
10545
|
-
newRenderedItems.
|
|
10691
|
+
newRenderedItems.push({ key, vNode });
|
|
10546
10692
|
// Update bindings
|
|
10547
10693
|
this.#updateItemBindings(vNode, context);
|
|
10548
10694
|
// For fragment-backed iterations, move the entire range atomically.
|
|
@@ -10568,7 +10714,7 @@ class VForDirective {
|
|
|
10568
10714
|
// Advance prevNode to this iteration's last DOM node
|
|
10569
10715
|
prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
|
|
10570
10716
|
}
|
|
10571
|
-
// Update rendered
|
|
10717
|
+
// Update the ordered rendered set
|
|
10572
10718
|
this.#renderedItems = newRenderedItems;
|
|
10573
10719
|
}
|
|
10574
10720
|
/**
|
|
@@ -13805,7 +13951,11 @@ class VApplication {
|
|
|
13805
13951
|
#initializeBindings() {
|
|
13806
13952
|
// Create bindings with change tracking
|
|
13807
13953
|
this.#bindings = new VBindings({
|
|
13808
|
-
onChange: () => {
|
|
13954
|
+
onChange: (identifier) => {
|
|
13955
|
+
// Pull-based invalidation: mark dependent computed properties dirty synchronously
|
|
13956
|
+
// so a subsequent synchronous read returns a fresh value, then schedule the
|
|
13957
|
+
// batched DOM update for the next microtask.
|
|
13958
|
+
this.#markDirtyComputeds(identifier);
|
|
13809
13959
|
this.#scheduleUpdate();
|
|
13810
13960
|
},
|
|
13811
13961
|
vApplication: this
|
|
@@ -13855,8 +14005,17 @@ class VApplication {
|
|
|
13855
14005
|
}
|
|
13856
14006
|
}
|
|
13857
14007
|
}
|
|
13858
|
-
//
|
|
13859
|
-
|
|
14008
|
+
// Register computed properties for pull-based (lazy) evaluation. Each computed is
|
|
14009
|
+
// recomputed on first access after being marked dirty, rather than eagerly in the update
|
|
14010
|
+
// microtask. We mark them all dirty and resolve once here so that initial values are
|
|
14011
|
+
// cached and recorded as changes for the first render.
|
|
14012
|
+
if (this.#options.computed) {
|
|
14013
|
+
for (const key of Object.keys(this.#options.computed)) {
|
|
14014
|
+
this.#bindings.registerComputed(key, () => this.#recomputeOne(key));
|
|
14015
|
+
this.#bindings.markComputedDirty(key);
|
|
14016
|
+
}
|
|
14017
|
+
}
|
|
14018
|
+
this.#flushDirtyComputeds();
|
|
13860
14019
|
}
|
|
13861
14020
|
/**
|
|
13862
14021
|
* Initializes watchers from the watch option.
|
|
@@ -13907,8 +14066,9 @@ class VApplication {
|
|
|
13907
14066
|
* Executes an immediate DOM update.
|
|
13908
14067
|
*/
|
|
13909
14068
|
#update() {
|
|
13910
|
-
//
|
|
13911
|
-
|
|
14069
|
+
// Resolve any computed properties still marked dirty (those not already pulled by a
|
|
14070
|
+
// synchronous read) so that the set of changed identifiers is complete before the DOM diff.
|
|
14071
|
+
this.#flushDirtyComputeds();
|
|
13912
14072
|
// Apply computed source path mappings to changes
|
|
13913
14073
|
// This converts paths like "model.elements[0].executionListeners"
|
|
13914
14074
|
// to "selectedElement.executionListeners" when selectedElement points to model.elements[0]
|
|
@@ -13953,110 +14113,116 @@ class VApplication {
|
|
|
13953
14113
|
}
|
|
13954
14114
|
}
|
|
13955
14115
|
/**
|
|
13956
|
-
*
|
|
13957
|
-
*
|
|
14116
|
+
* 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).
|
|
14121
|
+
* @param identifier The changed identifier reported by the bindings change tracker.
|
|
13958
14122
|
*/
|
|
13959
|
-
#
|
|
13960
|
-
if (!this.#options.computed) {
|
|
14123
|
+
#markDirtyComputeds(identifier) {
|
|
14124
|
+
if (!this.#options.computed || !this.#bindings) {
|
|
13961
14125
|
return;
|
|
13962
14126
|
}
|
|
13963
|
-
const
|
|
13964
|
-
const processing = new Set();
|
|
13965
|
-
// Gather all changed identifiers, including all parent paths
|
|
13966
|
-
// e.g., for "model.elements[0].messageRef", also add:
|
|
13967
|
-
// "model.elements[0]", "model.elements", "model"
|
|
13968
|
-
const allChanges = new Set();
|
|
13969
|
-
this.#bindings?.changes.forEach(id => {
|
|
13970
|
-
allChanges.add(id);
|
|
13971
|
-
// Add all parent paths by progressively stripping from the end
|
|
13972
|
-
let path = id;
|
|
13973
|
-
while (path.length > 0) {
|
|
13974
|
-
// Find last separator (either '[' or '.')
|
|
13975
|
-
const bracketIdx = path.lastIndexOf('[');
|
|
13976
|
-
const dotIdx = path.lastIndexOf('.');
|
|
13977
|
-
const lastSep = Math.max(bracketIdx, dotIdx);
|
|
13978
|
-
if (lastSep === -1) {
|
|
13979
|
-
break;
|
|
13980
|
-
}
|
|
13981
|
-
path = path.substring(0, lastSep);
|
|
13982
|
-
if (path.length > 0) {
|
|
13983
|
-
allChanges.add(path);
|
|
13984
|
-
}
|
|
13985
|
-
}
|
|
13986
|
-
});
|
|
13987
|
-
// Helper function to recursively compute a property
|
|
13988
|
-
const compute = (key) => {
|
|
13989
|
-
// Skip if already computed in this update cycle
|
|
13990
|
-
if (computed.has(key)) {
|
|
13991
|
-
return;
|
|
13992
|
-
}
|
|
13993
|
-
// Detect circular dependency
|
|
13994
|
-
if (processing.has(key)) {
|
|
13995
|
-
this.#logger.error(`Circular dependency detected for computed property '${key}'`);
|
|
13996
|
-
return;
|
|
13997
|
-
}
|
|
13998
|
-
processing.add(key);
|
|
13999
|
-
// Get the dependencies for this computed property
|
|
14127
|
+
for (const key of Object.keys(this.#computedDependencies)) {
|
|
14000
14128
|
const deps = this.#computedDependencies[key] || [];
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
// dependency chains are resolved and allChanges is up-to-date.
|
|
14004
|
-
for (const dep of deps) {
|
|
14005
|
-
if (this.#options.computed[dep]) {
|
|
14006
|
-
compute(dep);
|
|
14007
|
-
}
|
|
14008
|
-
}
|
|
14009
|
-
// If none of the dependencies have changed, skip recomputation (unless it's initialization).
|
|
14010
|
-
// Checked after recursive computation to detect transitive changes through computed properties.
|
|
14011
|
-
if (!isInitialization && !deps.some(dep => allChanges.has(dep))) {
|
|
14012
|
-
computed.add(key);
|
|
14013
|
-
return;
|
|
14129
|
+
if (deps.some(dep => this.#dependencyAffectedBy(dep, identifier))) {
|
|
14130
|
+
this.#bindings.markComputedDirty(key);
|
|
14014
14131
|
}
|
|
14015
|
-
|
|
14016
|
-
|
|
14017
|
-
|
|
14018
|
-
|
|
14019
|
-
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
14025
|
-
|
|
14026
|
-
|
|
14027
|
-
|
|
14028
|
-
|
|
14029
|
-
|
|
14030
|
-
|
|
14031
|
-
|
|
14032
|
-
|
|
14033
|
-
|
|
14034
|
-
|
|
14035
|
-
|
|
14036
|
-
|
|
14037
|
-
|
|
14038
|
-
|
|
14039
|
-
|
|
14040
|
-
|
|
14041
|
-
|
|
14042
|
-
|
|
14132
|
+
}
|
|
14133
|
+
}
|
|
14134
|
+
/**
|
|
14135
|
+
* Determines whether a change to `changePath` affects a computed dependency `dep`, taking both
|
|
14136
|
+
* path directions into account:
|
|
14137
|
+
* - an exact match;
|
|
14138
|
+
* - `changePath` is a descendant of `dep` (e.g. dep "cartItems", change "cartItems.0.quantity")
|
|
14139
|
+
* — a nested mutation of the dependency;
|
|
14140
|
+
* - `dep` is a descendant of `changePath` (e.g. dep "user.name", change "user") — the container
|
|
14141
|
+
* holding the dependency was replaced wholesale.
|
|
14142
|
+
* Local and global path aliases (e.g. computed source paths) are also honored via the bindings'
|
|
14143
|
+
* own alias-aware matcher.
|
|
14144
|
+
* @param dep The dependency path declared by a computed property.
|
|
14145
|
+
* @param changePath The identifier reported by the bindings change tracker.
|
|
14146
|
+
*/
|
|
14147
|
+
#dependencyAffectedBy(dep, changePath) {
|
|
14148
|
+
if (changePath === dep) {
|
|
14149
|
+
return true;
|
|
14150
|
+
}
|
|
14151
|
+
if (changePath.startsWith(dep + '.') || changePath.startsWith(dep + '[')) {
|
|
14152
|
+
return true;
|
|
14153
|
+
}
|
|
14154
|
+
if (dep.startsWith(changePath + '.') || dep.startsWith(changePath + '[')) {
|
|
14155
|
+
return true;
|
|
14156
|
+
}
|
|
14157
|
+
// Fall back to alias-aware matching for aliased paths (computed source paths, props, etc.).
|
|
14158
|
+
return this.#bindings.doesChangeMatchIdentifier(changePath, dep);
|
|
14159
|
+
}
|
|
14160
|
+
/**
|
|
14161
|
+
* Forces resolution of all computed properties currently marked dirty so that the set of
|
|
14162
|
+
* changed identifiers is complete before watcher notification and the DOM diff. Computeds that
|
|
14163
|
+
* were already pulled by a synchronous read earlier in the cycle are no longer dirty and are
|
|
14164
|
+
* skipped, so each computed is recomputed at most once per update cycle.
|
|
14165
|
+
*/
|
|
14166
|
+
#flushDirtyComputeds() {
|
|
14167
|
+
this.#bindings?.flushDirtyComputeds();
|
|
14168
|
+
}
|
|
14169
|
+
/**
|
|
14170
|
+
* Recomputes a single computed property and updates its cached value. Registered with the
|
|
14171
|
+
* bindings as the pull-based recompute callback, so it runs lazily the first time the property
|
|
14172
|
+
* is read after being marked dirty (or during the pre-render flush). Computed→computed chains
|
|
14173
|
+
* resolve naturally and order-independently: reading a dependent computed inside the getter
|
|
14174
|
+
* triggers its own lazy resolution through the bindings proxy.
|
|
14175
|
+
*
|
|
14176
|
+
* When the value actually changes, the computed name is recorded as a change so that
|
|
14177
|
+
* dependency-precise DOM updates and watcher notifications still fire, and the source-path
|
|
14178
|
+
* mapping is refreshed so nested changes to the underlying object map back to the computed.
|
|
14179
|
+
* @param key The computed property name.
|
|
14180
|
+
*/
|
|
14181
|
+
#recomputeOne(key) {
|
|
14182
|
+
if (!this.#options.computed || !this.#bindings) {
|
|
14183
|
+
return;
|
|
14184
|
+
}
|
|
14185
|
+
const def = this.#options.computed[key];
|
|
14186
|
+
if (!def) {
|
|
14187
|
+
return;
|
|
14188
|
+
}
|
|
14189
|
+
const computedFn = VApplication.#getComputedGetter(def);
|
|
14190
|
+
try {
|
|
14191
|
+
// Read the previous value without triggering re-resolution, for change detection.
|
|
14192
|
+
const oldValue = this.#bindings.peekComputed(key);
|
|
14193
|
+
const newValue = computedFn.call(this.#bindings.raw);
|
|
14194
|
+
// Check if the value actually changed.
|
|
14195
|
+
let hasChanged = oldValue !== newValue;
|
|
14196
|
+
// For arrays, always treat as changed (VBindings detects length changes via its length
|
|
14197
|
+
// cache). This preserves the precise-update behavior of the previous eager implementation.
|
|
14198
|
+
if (!hasChanged && Array.isArray(newValue)) {
|
|
14199
|
+
hasChanged = true;
|
|
14200
|
+
}
|
|
14201
|
+
// Cache the value silently so the read returns it without re-triggering reactivity.
|
|
14202
|
+
this.#bindings.setSilent(key, newValue);
|
|
14203
|
+
if (hasChanged) {
|
|
14204
|
+
// Mark the computed property as changed so UI and watchers depending on it update.
|
|
14205
|
+
this.#bindings.markChanged(key);
|
|
14206
|
+
// Track source path mapping for computed property values.
|
|
14207
|
+
// This allows changes like "model.elements[0].x" to be mapped to "selectedElement.x".
|
|
14208
|
+
if (typeof newValue === 'object' && newValue !== null) {
|
|
14209
|
+
const sourcePath = ReactiveProxy.getPath(newValue);
|
|
14210
|
+
if (sourcePath) {
|
|
14211
|
+
// Remove old mapping for this computed property
|
|
14212
|
+
for (const [path, name] of this.#computedSourcePaths) {
|
|
14213
|
+
if (name === key) {
|
|
14214
|
+
this.#computedSourcePaths.delete(path);
|
|
14215
|
+
break;
|
|
14043
14216
|
}
|
|
14044
|
-
// Add new mapping
|
|
14045
|
-
this.#computedSourcePaths.set(sourcePath, key);
|
|
14046
14217
|
}
|
|
14218
|
+
// Add new mapping
|
|
14219
|
+
this.#computedSourcePaths.set(sourcePath, key);
|
|
14047
14220
|
}
|
|
14048
14221
|
}
|
|
14049
14222
|
}
|
|
14050
|
-
|
|
14051
|
-
|
|
14052
|
-
}
|
|
14053
|
-
computed.add(key);
|
|
14054
|
-
processing.delete(key);
|
|
14055
|
-
};
|
|
14056
|
-
// Compute all properties; the recursive logic inside compute() handles
|
|
14057
|
-
// dependency ordering and skips properties whose dependencies did not change.
|
|
14058
|
-
for (const key of Object.keys(this.#computedDependencies)) {
|
|
14059
|
-
compute(key);
|
|
14223
|
+
}
|
|
14224
|
+
catch (error) {
|
|
14225
|
+
this.#logger.error(`Error evaluating computed property '${key}': ${error}`);
|
|
14060
14226
|
}
|
|
14061
14227
|
}
|
|
14062
14228
|
/**
|