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