@mintjamsinc/ichigojs 0.1.68 → 0.1.69

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.
@@ -10270,9 +10270,18 @@
10270
10270
  #sourceName;
10271
10271
  #useOfSyntax = false; // Track if 'of' syntax was used
10272
10272
  /**
10273
- * Map to track rendered items by their keys
10273
+ * Ordered list of currently rendered items.
10274
+ *
10275
+ * This is intentionally an ordered array of { key, vNode } entries rather
10276
+ * than a Map<key, VNode>. The :key attribute is a *reconciliation hint*
10277
+ * used to identify and reuse the same logical row across re-renders — it is
10278
+ * not the identity of the rendered set. Keying the rendered set by a Map
10279
+ * would make it structurally impossible to hold two rows that resolve to
10280
+ * the same key, which would silently drop application data. The most
10281
+ * fundamental invariant of a list directive is "N items in => N rows out",
10282
+ * so the rendered set must be a positional list that can carry duplicates.
10274
10283
  */
10275
- #renderedItems = new Map();
10284
+ #renderedItems = [];
10276
10285
  /**
10277
10286
  * Previous iterations to detect changes
10278
10287
  */
@@ -10395,11 +10404,11 @@
10395
10404
  destroy() {
10396
10405
  // Clean up all rendered items
10397
10406
  // First destroy all VNodes (calls @unmount hooks), then remove from DOM
10398
- for (const vNode of this.#renderedItems.values()) {
10407
+ for (const { vNode } of this.#renderedItems) {
10399
10408
  vNode.destroy();
10400
10409
  }
10401
10410
  // Then remove DOM nodes
10402
- for (const vNode of this.#renderedItems.values()) {
10411
+ for (const { vNode } of this.#renderedItems) {
10403
10412
  const range = vNode.fragmentRange;
10404
10413
  if (range) {
10405
10414
  range.remove();
@@ -10410,7 +10419,7 @@
10410
10419
  vNode.node.parentNode.removeChild(vNode.node);
10411
10420
  }
10412
10421
  }
10413
- this.#renderedItems.clear();
10422
+ this.#renderedItems = [];
10414
10423
  this.#previousIterations = [];
10415
10424
  }
10416
10425
  /**
@@ -10451,7 +10460,24 @@
10451
10460
  this.#previousIterations = iterations;
10452
10461
  }
10453
10462
  /**
10454
- * Key-based diffing for efficient DOM updates
10463
+ * Key-based diffing for efficient DOM updates.
10464
+ *
10465
+ * Reconciliation model
10466
+ * --------------------
10467
+ * The :key attribute is treated as a *hint* for reusing the same logical
10468
+ * row across re-renders, not as the identity of the rendered set. The
10469
+ * previously rendered rows are placed into a pool keyed by :key (a queue
10470
+ * per key, so equal keys can hold more than one row). Each incoming
10471
+ * iteration then claims a row from its key's queue when one is available,
10472
+ * otherwise a fresh row is created. Whatever stays in the pool at the end
10473
+ * is genuinely gone and is destroyed and removed.
10474
+ *
10475
+ * This guarantees the directive's most fundamental invariant — "N items in
10476
+ * => N rows out" — even when the application supplies duplicate keys. We
10477
+ * still warn on duplicates because they make reuse ambiguous (reordering
10478
+ * becomes positional rather than identity-stable), but we never silently
10479
+ * drop the application's data, and no row is ever orphaned: every old row
10480
+ * is either reused or explicitly removed.
10455
10481
  */
10456
10482
  #updateList(newIterations) {
10457
10483
  const parent = this.#vNode.anchorNode?.parentNode;
@@ -10459,22 +10485,38 @@
10459
10485
  if (!parent || !anchor) {
10460
10486
  throw new Error('v-for element must have a parent and anchor');
10461
10487
  }
10462
- const newRenderedItems = new Map();
10463
- // Track which keys are still needed and detect duplicates
10464
- const neededKeys = new Set();
10488
+ // Build a reuse pool from the currently rendered rows. A queue per key
10489
+ // (FIFO) lets duplicate keys reuse multiple rows: the first incoming
10490
+ // occurrence claims the first existing row, the second claims the next,
10491
+ // and so on.
10492
+ const pool = new Map();
10493
+ for (const { key, vNode } of this.#renderedItems) {
10494
+ let queue = pool.get(key);
10495
+ if (!queue) {
10496
+ queue = [];
10497
+ pool.set(key, queue);
10498
+ }
10499
+ queue.push(vNode);
10500
+ }
10501
+ // Decide, for each incoming iteration in order, whether it reuses an
10502
+ // existing row or needs a new one. Reused rows are taken out of the
10503
+ // pool so that what remains afterwards is exactly the set to remove.
10465
10504
  const seenKeys = new Set();
10466
- for (const ctx of newIterations) {
10467
- if (seenKeys.has(ctx.key)) {
10468
- console.warn(`[ichigo.js] Duplicate key detected in v-for: "${ctx.key}". This may cause unexpected behavior. Keys should be unique.`);
10505
+ const plan = [];
10506
+ for (const context of newIterations) {
10507
+ if (seenKeys.has(context.key)) {
10508
+ 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.`);
10469
10509
  }
10470
- seenKeys.add(ctx.key);
10471
- neededKeys.add(ctx.key);
10510
+ seenKeys.add(context.key);
10511
+ const queue = pool.get(context.key);
10512
+ const reused = queue && queue.length ? queue.shift() : undefined;
10513
+ plan.push({ context, reused });
10472
10514
  }
10473
- // Remove items that are no longer needed
10515
+ // Remove rows that were not reused.
10474
10516
  // First destroy VNodes (calls @unmount hooks while DOM is still accessible)
10475
10517
  const nodesToRemove = [];
10476
- for (const [key, vNode] of this.#renderedItems) {
10477
- if (!neededKeys.has(key)) {
10518
+ for (const queue of pool.values()) {
10519
+ for (const vNode of queue) {
10478
10520
  nodesToRemove.push(vNode);
10479
10521
  vNode.destroy();
10480
10522
  }
@@ -10493,11 +10535,12 @@
10493
10535
  parentOfNode.removeChild(vNode.node);
10494
10536
  }
10495
10537
  }
10496
- // Add or reorder items
10538
+ // Add or reorder rows, building the new ordered rendered set.
10539
+ const newRenderedItems = [];
10497
10540
  let prevNode = anchor;
10498
- for (const context of newIterations) {
10541
+ for (const { context, reused } of plan) {
10499
10542
  const { key } = context;
10500
- let vNode = this.#renderedItems.get(key);
10543
+ let vNode = reused;
10501
10544
  if (!vNode) {
10502
10545
  // Create new item
10503
10546
  const clone = this.#cloneNode();
@@ -10529,7 +10572,7 @@
10529
10572
  if (clone.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
10530
10573
  const range = VFragmentRange.insert(parent, prevNode.nextSibling, 'vfor-fragment', clone);
10531
10574
  vNode.fragmentRange = range;
10532
- newRenderedItems.set(key, vNode);
10575
+ newRenderedItems.push({ key, vNode });
10533
10576
  vNode.forceUpdate();
10534
10577
  prevNode = range.lastNode;
10535
10578
  continue;
@@ -10543,12 +10586,12 @@
10543
10586
  else {
10544
10587
  parent.appendChild(nodeToInsert);
10545
10588
  }
10546
- newRenderedItems.set(key, vNode);
10589
+ newRenderedItems.push({ key, vNode });
10547
10590
  vNode.forceUpdate();
10548
10591
  }
10549
10592
  else {
10550
10593
  // Reuse existing item
10551
- newRenderedItems.set(key, vNode);
10594
+ newRenderedItems.push({ key, vNode });
10552
10595
  // Update bindings
10553
10596
  this.#updateItemBindings(vNode, context);
10554
10597
  // For fragment-backed iterations, move the entire range atomically.
@@ -10574,7 +10617,7 @@
10574
10617
  // Advance prevNode to this iteration's last DOM node
10575
10618
  prevNode = vNode.fragmentRange?.lastNode ?? vNode.anchorNode ?? vNode.node;
10576
10619
  }
10577
- // Update rendered items map
10620
+ // Update the ordered rendered set
10578
10621
  this.#renderedItems = newRenderedItems;
10579
10622
  }
10580
10623
  /**