@thyn/core 0.0.228 → 0.0.232

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/src/element.ts CHANGED
@@ -13,14 +13,22 @@ export function collectEffect(effectFn) {
13
13
  collectingHead = effectFn;
14
14
  }
15
15
 
16
+ // export function createReactiveTextNode(v) {
17
+ // let n;
18
+ // staticEffect(() => {
19
+ // if (n) {
20
+ // n.nodeValue = v();
21
+ // } else {
22
+ // n = document.createTextNode(v());
23
+ // }
24
+ // });
25
+ // return n;
26
+ // }
27
+
16
28
  export function createReactiveTextNode(v) {
17
- let n;
29
+ const n = document.createTextNode(v());
18
30
  staticEffect(() => {
19
- if (n) {
20
- n.nodeValue = v();
21
- } else {
22
- n = document.createTextNode(v());
23
- }
31
+ n.nodeValue = v();
24
32
  });
25
33
  return n;
26
34
  }
@@ -78,7 +86,7 @@ export function setReactiveAttribute(el, key, val) {
78
86
  else el.setAttribute(key, v);
79
87
  return;
80
88
  }
81
- if (v !== undefined) el.setAttribute(key, val());
89
+ if (v !== undefined) el.setAttribute(key, v);
82
90
  ran = true;
83
91
  }),
84
92
  );
@@ -262,358 +270,342 @@ export function list(props, terminal = false) {
262
270
  const teardownNode = terminal ? shallowTeardown : teardown;
263
271
  let parent;
264
272
  let outlet = document.createDocumentFragment();
265
- let prevItems;
273
+
274
+ // State
275
+ let prevItems = [];
276
+ let rowNodes = []; // The "Shadow Array" - avoids reading DOM
277
+
266
278
  const startBookend = document.createComment("") as any;
267
279
  const endBookend = document.createComment("") as any;
280
+
268
281
  startBookend.$frag = outlet;
269
282
  startBookend.$end = endBookend;
283
+
270
284
  const render = props.render;
271
285
 
272
286
  staticEffect(() => {
273
287
  parent = startBookend.parentNode;
288
+
289
+ // 1. Initialization / First Render
274
290
  if (!parent) {
275
- prevItems = props.items();
276
- outlet.append(startBookend, ...prevItems.map(render), endBookend);
291
+ const items = props.items();
292
+ // Render all items
293
+ const newNodes = items.map(render);
294
+
295
+ outlet.append(startBookend, ...newNodes, endBookend);
296
+
297
+ // Save state
298
+ prevItems = items;
299
+ rowNodes = newNodes;
277
300
  return;
278
301
  }
279
- let nextItems = props.items();
280
- let newLength = nextItems.length;
302
+
303
+ const nextItems = props.items();
304
+ const newLength = nextItems.length;
281
305
  let oldLength = prevItems.length;
282
- if (!oldLength && newLength) {
283
- endBookend.before(...nextItems.map(render))
284
- prevItems = nextItems;
285
- nextItems = null;
286
- return;
287
- }
288
- const childNodes = [];
289
- let ptr = startBookend.nextSibling;
290
- while (ptr !== endBookend) {
291
- childNodes.push(ptr);
292
- ptr = ptr.nextSibling;
293
- }
294
- const offset = 0;
295
- if (!newLength) {
296
- const removalQueue = [];
297
- const end = prevItems.length + offset;
298
- for (let i = offset; i < end; i++) {
299
- const ch = childNodes[i];
300
- teardownNode(ch);
301
- removalQueue.push(ch);
302
- }
303
- for (const ch of removalQueue) {
304
- remove(ch);
306
+
307
+ // 2. Fast Path: Clear All
308
+ if (newLength === 0) {
309
+ if (oldLength !== 0) {
310
+ for (let i = 0; i < oldLength; i++) {
311
+ teardownNode(rowNodes[i]);
312
+ remove(rowNodes[i]);
313
+ }
314
+ rowNodes = [];
315
+ prevItems = [];
305
316
  }
306
- prevItems = nextItems;
307
- nextItems = null;
308
317
  return;
309
318
  }
310
319
 
311
- let start = nextItems.findIndex((item, index) => prevItems[index] !== item);
312
- if (start === oldLength) {
313
- endBookend.before(...nextItems.slice(start).map(render));
320
+ // 3. Fast Path: Create All (from empty)
321
+ if (oldLength === 0) {
322
+ const newNodes = nextItems.map(render);
323
+ endBookend.before(...newNodes);
324
+ rowNodes = newNodes;
314
325
  prevItems = nextItems;
315
- nextItems = null;
316
326
  return;
317
327
  }
318
328
 
319
- if (start < 0) {
320
- for (let i = nextItems.length; i < oldLength; i++) {
321
- const e = childNodes[offset + --oldLength];
322
- teardownNode(e);
323
- remove(e);
324
- }
325
- prevItems = nextItems;
326
- nextItems = null;
327
- return;
329
+ // 4. Reconciliation
330
+ let start = 0;
331
+ let minLen = Math.min(oldLength, newLength);
332
+
333
+ // Prefix Scan (Cheaper JS Array read vs DOM read)
334
+ while (start < minLen && prevItems[start] === nextItems[start]) {
335
+ start++;
328
336
  }
329
337
 
330
- if (start >= newLength) {
331
- while (start < oldLength) {
332
- const e = childNodes[offset + --oldLength];
333
- teardownNode(e);
334
- remove(e);
335
- }
338
+ // Optimization: Append only
339
+ if (start === oldLength && newLength > oldLength) {
340
+ const newPart = nextItems.slice(start);
341
+ const newNodes = newPart.map(render);
342
+ endBookend.before(...newNodes);
343
+
344
+ rowNodes = rowNodes.concat(newNodes);
336
345
  prevItems = nextItems;
337
- nextItems = null;
338
346
  return;
339
347
  }
340
348
 
341
- // suffix
342
- for (
343
- oldLength--, newLength--;
344
- newLength > start &&
345
- oldLength >= start &&
346
- nextItems[newLength] === prevItems[oldLength];
347
- oldLength--, newLength--
348
- );
349
-
350
- const nextKeys = new Set(nextItems);
351
- const removalQueue = [];
352
- for (let i = start; i <= oldLength; i++) {
353
- if (!nextKeys.has(prevItems[i])) {
354
- const ch = childNodes[i + offset];
355
- teardownNode(ch);
356
- removalQueue.push(ch);
357
- childNodes[i + offset] = null;
349
+ // Optimization: Truncate only
350
+ if (start === newLength && oldLength > newLength) {
351
+ for (let i = start; i < oldLength; i++) {
352
+ teardownNode(rowNodes[i]);
353
+ remove(rowNodes[i]);
358
354
  }
359
- }
360
- for (const e of removalQueue) {
361
- remove(e);
362
- }
363
- if (oldLength - start === removalQueue.length) {
355
+ rowNodes.length = newLength; // JS Array Truncate
364
356
  prevItems = nextItems;
365
- nextItems = null;
366
357
  return;
367
358
  }
368
- let keyMap = new Map();
369
- for (let i = start; i <= oldLength; i++) {
370
- if (
371
- childNodes[i + offset] &&
372
- (!nextItems[i] ||
373
- prevItems[i] !== nextItems[i])
374
- ) {
375
- keyMap.set(prevItems[i], {
376
- el: childNodes[i + offset],
377
- item: prevItems[i],
378
- });
379
- }
359
+
360
+ // Suffix Scan
361
+ let end = 0;
362
+ // We stop if the suffix hits the prefix
363
+ while (
364
+ newLength - 1 - end >= start &&
365
+ oldLength - 1 - end >= start &&
366
+ nextItems[newLength - 1 - end] === prevItems[oldLength - 1 - end]
367
+ ) {
368
+ end++;
369
+ }
370
+
371
+ // 5. Complex Diff (The Middle)
372
+ const oldStart = start;
373
+ const oldEnd = oldLength - end;
374
+ const newEnd = newLength - end;
375
+
376
+ // A. Build Map of existing items in the "changed" region
377
+ // key -> { node, index }
378
+ const keyMap = new Map();
379
+ for (let i = oldStart; i < oldEnd; i++) {
380
+ const item = prevItems[i];
381
+ // If duplicate items exist, first one wins or logic needs to be more robust.
382
+ // Assuming unique keys for simplicity or using last-write-wins:
383
+ // if (!keyMap.has(item)) {
384
+ keyMap.set(item, rowNodes[i]);
385
+ // } else {
386
+ // Handle duplicates by removing the extra immediately?
387
+ // Or handle collision. For now, assume distinct items or standard behavior.
388
+ // }
389
+ }
390
+
391
+ // B. Setup for new node list construction
392
+ const nextRowNodes = new Array(newLength);
393
+
394
+ // Copy Prefix
395
+ for (let i = 0; i < start; i++) {
396
+ nextRowNodes[i] = rowNodes[i];
380
397
  }
381
- if (newLength === oldLength && keyMap.size > (newLength - start + 1) / 2) {
382
- const lastOrdered = start > 0 ? childNodes[start + offset - 1] : startBookend;
383
- const set = [];
384
- for (let i = start; i <= newLength; i++) {
385
- set.push(keyMap.get(nextItems[i])?.el ?? childNodes[i + offset]);
386
- }
387
- lastOrdered.after(...set);
388
- prevItems = nextItems;
389
- keyMap = null;
390
- nextItems = null;
391
- return;
398
+ // Copy Suffix
399
+ for (let i = 0; i < end; i++) {
400
+ nextRowNodes[newLength - 1 - i] = rowNodes[oldLength - 1 - i];
392
401
  }
393
402
 
394
- let cursor = startBookend.nextSibling;
395
- for (let i = 0; i < start; i++) {
396
- cursor = cursor.nextSibling;
397
- }
398
- while (start <= newLength) {
399
- const newChd = nextItems[start];
400
- const oldChd = prevItems[start];
401
- if (newChd === oldChd) {
402
- start++;
403
- cursor = cursor.nextSibling;
404
- continue;
405
- }
406
- if (oldChd === undefined) {
407
- parent.insertBefore(render(newChd), endBookend);
408
- start++;
409
- continue;
410
- }
411
- const mappedOld = keyMap.get(newChd);
412
- if (mappedOld) {
413
- const oldDom = cursor;
414
- const { el, item } = mappedOld;
415
- if (oldDom !== el) {
416
- const tmp = el.nextSibling;
417
- parent.insertBefore(el, oldDom);
418
- parent.insertBefore(oldDom, tmp);
419
- cursor = el.nextSibling;
420
- } else if (item !== newChd) {
421
- const next = el.nextSibling;
422
- replaceWith(newChd, el, render);
423
- cursor = next;
424
- } else {
425
- cursor = el.nextSibling;
426
- }
427
- keyMap.delete(newChd);
428
- } else if (oldChd !== newChd) {
429
- parent.insertBefore(render(newChd), cursor);
403
+ // C. Find anchor for insertions
404
+ // We insert before the first node of the suffix, or the endBookend.
405
+ const anchor = (end > 0) ? rowNodes[oldLength - end] : endBookend;
406
+
407
+ // D. Iterate new middle
408
+ for (let i = oldStart; i < newEnd; i++) {
409
+ const newItem = nextItems[i];
410
+ let node;
411
+
412
+ if (keyMap.has(newItem)) {
413
+ // Reuse existing
414
+ node = keyMap.get(newItem);
415
+ keyMap.delete(newItem);
416
+
417
+ // DOM Move:
418
+ // We always insertBefore the anchor.
419
+ // Since we are iterating forward, "anchor" isn't static.
420
+ // Actually, simply inserting before the *current* anchor works if we
421
+ // process carefully, but standard "place and move cursor" is safer.
422
+ parent.insertBefore(node, anchor);
423
+ } else {
424
+ // Create new
425
+ node = render(newItem);
426
+ parent.insertBefore(node, anchor);
430
427
  }
431
- start++;
428
+ nextRowNodes[i] = node;
432
429
  }
433
- for (const { el } of keyMap.values()) {
434
- teardownNode(el);
435
- remove(el);
430
+
431
+ // E. Cleanup
432
+ // Anything remaining in keyMap is gone
433
+ for (const node of keyMap.values()) {
434
+ teardownNode(node);
435
+ remove(node);
436
436
  }
437
- keyMap = null;
437
+
438
+ // Handle "middle" items that weren't in keyMap (duplicates logic)
439
+ // or implicit removals handled by the map logic.
440
+ // Specifically: We iterated [oldStart...oldEnd] to build the map.
441
+ // If an item was in that range but NOT in the new range, it's in the map.
442
+ // If it WAS in the new range, we removed it from the map.
443
+ // So map.values() is exactly what needs to die.
444
+
445
+ // Update State
446
+ rowNodes = nextRowNodes;
438
447
  prevItems = nextItems;
439
- nextItems = null;
440
448
  });
449
+
441
450
  return outlet;
442
451
  }
443
-
444
452
  export function isolatedTerminalList(props) {
445
453
  let parent;
446
454
  let outlet = document.createDocumentFragment();
447
- let prevItems;
455
+
456
+ // State
457
+ let prevItems = [];
458
+ let rowNodes = []; // The "Shadow Array"
459
+
448
460
  const startBookend = document.createComment("") as any;
449
461
  const endBookend = document.createComment("") as any;
462
+
450
463
  startBookend.$frag = outlet;
451
464
  startBookend.$end = endBookend;
465
+
452
466
  const render = props.render;
453
467
 
454
468
  staticEffect(() => {
455
469
  parent = startBookend.parentNode;
470
+
471
+ // 1. Initialization
456
472
  if (!parent) {
457
- prevItems = props.items();
458
- outlet.append(startBookend, ...prevItems.map(render), endBookend);
459
- return;
460
- }
461
- let nextItems = props.items();
462
- let newLength = nextItems.length;
463
- let oldLength = prevItems.length;
464
- if (!oldLength && newLength) {
465
- endBookend.before(...nextItems.map(render))
466
- prevItems = nextItems;
467
- nextItems = null;
473
+ const items = props.items();
474
+ const newNodes = items.map(render);
475
+
476
+ outlet.append(startBookend, ...newNodes, endBookend);
477
+
478
+ prevItems = items;
479
+ rowNodes = newNodes;
468
480
  return;
469
481
  }
470
- const childNodeList = parent.childNodes as NodeListOf<ChildNode>;
471
- if (!newLength) {
472
- const end = childNodeList.length - 1;
473
- for (let i = 1; i < end; i++) {
474
- shallowTeardown(childNodeList[i]);
482
+
483
+ const nextItems = props.items();
484
+ const newLength = nextItems.length;
485
+ const oldLength = prevItems.length;
486
+
487
+ // 2. Fast Path: Clear All
488
+ if (newLength === 0) {
489
+ if (oldLength !== 0) {
490
+ // Optimization: If the parent only contains our list (between bookends),
491
+ // we might want to use textContent = "". However, to be safe with
492
+ // bookends logic, we remove specifically known nodes.
493
+ for (let i = 0; i < oldLength; i++) {
494
+ const node = rowNodes[i];
495
+ shallowTeardown(node);
496
+ // node.remove();
497
+ }
498
+ parent.textContent = "";
499
+ parent.append(startBookend, endBookend);
500
+ rowNodes = [];
501
+ prevItems = [];
475
502
  }
476
- parent.textContent = "";
477
- parent.append(startBookend, endBookend);
478
- prevItems = nextItems;
479
- nextItems = null;
480
503
  return;
481
504
  }
482
505
 
483
- let start = nextItems.findIndex((item, index) => prevItems[index] !== item);
484
- if (start === oldLength) {
485
- endBookend.before(...nextItems.slice(start).map(render));
506
+ // 3. Fast Path: Create All
507
+ if (oldLength === 0) {
508
+ const newNodes = nextItems.map(render);
509
+ endBookend.before(...newNodes);
510
+
511
+ rowNodes = newNodes;
486
512
  prevItems = nextItems;
487
- nextItems = null;
488
513
  return;
489
514
  }
490
515
 
491
- let childNodes = Array.from(childNodeList);
492
- if (start < 0) {
493
- for (let i = nextItems.length; i < oldLength; i++) {
494
- const e = childNodes[1 + --oldLength];
495
- shallowTeardown(e);
496
- e.remove();
497
- }
498
- prevItems = nextItems;
499
- nextItems = null;
500
- childNodes = null;
501
- return;
516
+ // 4. Reconciliation
517
+ let start = 0;
518
+ const minLen = Math.min(oldLength, newLength);
519
+
520
+ // Prefix Scan
521
+ while (start < minLen && prevItems[start] === nextItems[start]) {
522
+ start++;
502
523
  }
503
524
 
504
- if (start >= newLength) {
505
- while (start < oldLength) {
506
- const e = childNodes[1 + --oldLength];
507
- shallowTeardown(e);
508
- e.remove();
509
- }
525
+ // Optimization: Append Suffix
526
+ if (start === oldLength && newLength > oldLength) {
527
+ const newPart = nextItems.slice(start);
528
+ const newNodes = newPart.map(render);
529
+ endBookend.before(...newNodes);
530
+
531
+ rowNodes = rowNodes.concat(newNodes);
510
532
  prevItems = nextItems;
511
- nextItems = null;
512
- childNodes = null;
513
533
  return;
514
534
  }
515
535
 
516
- // suffix
517
- for (
518
- oldLength--, newLength--;
519
- newLength > start &&
520
- oldLength >= start &&
521
- nextItems[newLength] === prevItems[oldLength];
522
- oldLength--, newLength--
523
- );
524
-
525
- const nextKeys = new Set(nextItems);
526
- const removalQueue = [];
527
- for (let i = start; i <= oldLength; i++) {
528
- if (!nextKeys.has(prevItems[i])) {
529
- const ch = childNodes[i + 1];
530
- shallowTeardown(ch);
531
- removalQueue.push(ch);
532
- childNodes[i + 1] = null;
536
+ // Optimization: Truncate Suffix
537
+ if (start === newLength && oldLength > newLength) {
538
+ for (let i = start; i < oldLength; i++) {
539
+ const node = rowNodes[i];
540
+ shallowTeardown(node);
541
+ node.remove();
533
542
  }
534
- }
535
- if (removalQueue.length === prevItems.length) {
536
- parent.textContent = "";
537
- parent.append(startBookend, ...nextItems.map(render), endBookend);
543
+ rowNodes.length = newLength;
538
544
  prevItems = nextItems;
539
- nextItems = null;
540
- childNodes = null;
541
545
  return;
542
546
  }
543
- for (const e of removalQueue) {
544
- e.remove();
547
+
548
+ // Suffix Scan
549
+ let end = 0;
550
+ while (
551
+ newLength - 1 - end >= start &&
552
+ oldLength - 1 - end >= start &&
553
+ nextItems[newLength - 1 - end] === prevItems[oldLength - 1 - end]
554
+ ) {
555
+ end++;
545
556
  }
546
- if (oldLength - start === removalQueue.length) {
547
- prevItems = nextItems;
548
- nextItems = null;
549
- childNodes = null;
550
- return;
557
+
558
+ // 5. Complex Diff (The Middle)
559
+ const oldStart = start;
560
+ const oldEnd = oldLength - end;
561
+ const newEnd = newLength - end;
562
+
563
+ // A. Build Map
564
+ const keyMap = new Map();
565
+ for (let i = oldStart; i < oldEnd; i++) {
566
+ const item = prevItems[i];
567
+ keyMap.set(item, rowNodes[i]);
551
568
  }
552
- let keyMap = new Map();
553
- for (let i = start; i <= oldLength; i++) {
554
- if (
555
- childNodes[i + 1] &&
556
- (!nextItems[i] ||
557
- prevItems[i] !== nextItems[i])
558
- ) {
559
- keyMap.set(prevItems[i], {
560
- el: childNodes[i + 1],
561
- item: prevItems[i],
562
- });
563
- }
569
+
570
+ const nextRowNodes = new Array(newLength);
571
+
572
+ // Copy Prefix
573
+ for (let i = 0; i < start; i++) {
574
+ nextRowNodes[i] = rowNodes[i];
564
575
  }
565
- if (newLength === oldLength && keyMap.size > (newLength - start + 1) / 2) {
566
- const lastOrdered = childNodes[start];
567
- const set = [];
568
- for (let i = start; i <= newLength; i++) {
569
- set.push(keyMap.get(nextItems[i])?.el ?? childNodes[i + 1]);
570
- }
571
- lastOrdered.after(...set);
572
- prevItems = nextItems;
573
- keyMap = null;
574
- nextItems = null;
575
- childNodes = null;
576
- return;
576
+ // Copy Suffix
577
+ for (let i = 0; i < end; i++) {
578
+ nextRowNodes[newLength - 1 - i] = rowNodes[oldLength - 1 - i];
577
579
  }
578
580
 
579
- while (start <= newLength) {
580
- const newChd = nextItems[start];
581
- const oldChd = prevItems[start];
582
- if (newChd === oldChd) {
583
- start++;
584
- continue;
585
- }
586
- if (oldChd === undefined) {
587
- parent.insertBefore(render(newChd), endBookend);
588
- start++;
589
- continue;
590
- }
591
- const mappedOld = keyMap.get(newChd);
592
- if (mappedOld) {
593
- const oldDom = childNodeList[start + 1];
594
- const { el, item } = mappedOld;
595
- if (oldDom !== el) {
596
- const tmp = el.nextSibling;
597
- parent.insertBefore(el, oldDom);
598
- parent.insertBefore(oldDom, tmp);
599
- } else if (item !== newChd) {
600
- replaceWith(newChd, el, render);
601
- }
602
- keyMap.delete(newChd);
603
- } else if (oldChd !== newChd) {
604
- parent.insertBefore(render(newChd), childNodeList[start + 1]);
581
+ // Determine Anchor (The first node of the suffix, or the end bookend)
582
+ const anchor = (end > 0) ? rowNodes[oldLength - end] : endBookend;
583
+
584
+ // B. Process Middle
585
+ for (let i = oldStart; i < newEnd; i++) {
586
+ const newItem = nextItems[i];
587
+
588
+ if (keyMap.has(newItem)) {
589
+ const node = keyMap.get(newItem);
590
+ keyMap.delete(newItem);
591
+ parent.insertBefore(node, anchor);
592
+ nextRowNodes[i] = node;
593
+ } else {
594
+ const node = render(newItem);
595
+ parent.insertBefore(node, anchor);
596
+ nextRowNodes[i] = node;
605
597
  }
606
- start++;
607
598
  }
608
- for (const { el } of keyMap.values()) {
609
- shallowTeardown(el);
610
- el.remove();
599
+
600
+ // C. Cleanup Unused
601
+ for (const node of keyMap.values()) {
602
+ shallowTeardown(node);
603
+ node.remove();
611
604
  }
612
- keyMap = null;
605
+
606
+ rowNodes = nextRowNodes;
613
607
  prevItems = nextItems;
614
- nextItems = null;
615
- childNodes = null;
616
608
  });
617
- return outlet;
618
- }
619
609
 
610
+ return outlet;
611
+ }
package/src/signals.ts CHANGED
@@ -102,7 +102,6 @@ export function $effect(fn: (() => (() => void) | void) & any) {
102
102
  }
103
103
 
104
104
  export function staticEffect(fn: (() => (() => void) | void) & any) {
105
- fn.td = null;
106
105
  const prev = currentEffect;
107
106
  currentEffect = fn;
108
107
  fn();