@mmstack/primitives 21.0.4 → 21.0.6

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.
@@ -228,7 +228,7 @@ function derived(source, optOrKey, opt) {
228
228
  return baseEqual(a, b);
229
229
  }
230
230
  : baseEqual;
231
- const sig = toWritable(computed(() => from(source()), { ...rest, equal }), (newVal) => onChange(newVal));
231
+ const sig = toWritable(computed(() => from(source()), { ...rest, equal }), (newVal) => onChange(newVal), undefined, { pure: false });
232
232
  sig.from = from;
233
233
  if (isMutable(source)) {
234
234
  sig.mutate = (updater) => {
@@ -292,7 +292,6 @@ function isDerivation(sig) {
292
292
  function isWritableSignal(value) {
293
293
  return isWritableSignal$1(value);
294
294
  }
295
-
296
295
  /**
297
296
  * @internal
298
297
  * Creates a setter function for a source signal of type `Signal<T[]>` or a function returning `T[]`.
@@ -353,8 +352,11 @@ function indexArray(source, map, opt = {}) {
353
352
  if (len === prev.value.length)
354
353
  return prev.value;
355
354
  if (len < prev.value.length) {
356
- if (opt.onDestroy)
357
- prev.value.forEach((v) => opt.onDestroy?.(v));
355
+ if (opt.onDestroy) {
356
+ for (let i = len; i < prev.value.length; i++) {
357
+ opt.onDestroy(prev.value[i]);
358
+ }
359
+ }
358
360
  return prev.value.slice(0, len);
359
361
  }
360
362
  const next = prev.value.slice();
@@ -370,75 +372,188 @@ function indexArray(source, map, opt = {}) {
370
372
  */
371
373
  const mapArray = indexArray;
372
374
 
373
- function keyArray(source, keyFn, map, opt = {}) {
374
- const data = isSignal(source) ? source : computed(source);
375
- const setter = createSetter(data);
376
- const writableData = isWritableSignal(data)
377
- ? data
378
- : toWritable(data, () => {
379
- // noop
380
- });
381
- if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
382
- opt.equal = (a, b) => {
383
- if (a !== b)
384
- return false;
385
- return false; // opt out for same refs
386
- };
387
- }
388
- let freeMap = new Map();
389
- const createRecord = (i) => {
390
- const idx = signal(i, ...(ngDevMode ? [{ debugName: "idx" }] : []));
391
- const value = derived(writableData, {
392
- from: (v) => v[idx()],
393
- onChange: (next) => setter(next, untracked(idx)),
394
- }, opt);
395
- return {
396
- source: {
397
- idx,
398
- value,
399
- },
400
- computation: map(value, idx),
401
- };
402
- };
403
- const internal = linkedSignal({ ...(ngDevMode ? { debugName: "internal" } : {}), source: () => writableData(),
404
- computation: (src, prev) => {
405
- const prevCache = prev?.value.cache ?? new Map();
406
- const nextCache = freeMap;
407
- const nextValues = [];
408
- let changed = false;
409
- for (let i = 0; i < src.length; i++) {
410
- const k = untracked(() => keyFn(src[i]));
411
- let record = prevCache.get(k);
412
- if (!record) {
413
- changed = true;
414
- record = createRecord(i);
375
+ /**
376
+ * Reactively maps items from a source array to a new array by value (identity).
377
+ *
378
+ * similar to `Array.prototype.map`, but:
379
+ * 1. The `mapFn` receives the `index` as a Signal.
380
+ * 2. If an item in the `source` array moves to a new position, the *result* of the map function is reused and moved.
381
+ * The `index` signal is updated to the new index.
382
+ * 3. The `mapFn` is only run for *new* items.
383
+ *
384
+ * This is useful for building efficient lists where DOM nodes or heavy instances should be reused
385
+ * when the list is reordered.
386
+ *
387
+ * @param source A `Signal<T[]>` or a function returning `T[]`.
388
+ * @param mapFn The mapping function. Receives the item and its index as a Signal.
389
+ * @param options Optional configuration:
390
+ * - `onDestroy`: A callback invoked when a mapped item is removed from the array.
391
+ * @returns A `Signal<U[]>` containing the mapped array.
392
+ */
393
+ function keyArray(source, mapFn, options = {}) {
394
+ const sourceSignal = isSignal(source) ? source : computed(source);
395
+ const items = [];
396
+ let mapped = [];
397
+ const indexes = [];
398
+ const getKey = options.key || ((v) => v);
399
+ const newIndices = new Map();
400
+ const temp = [];
401
+ const tempIndexes = [];
402
+ const newIndicesNext = [];
403
+ const newIndexesCache = new Array();
404
+ return computed(() => {
405
+ const newItems = sourceSignal() || [];
406
+ return untracked(() => {
407
+ let i;
408
+ let j;
409
+ const newLen = newItems.length;
410
+ let len = items.length;
411
+ const newMapped = new Array(newLen);
412
+ const newIndexes = newIndexesCache;
413
+ newIndexes.length = 0;
414
+ newIndexes.length = newLen;
415
+ let start;
416
+ let end;
417
+ let newEnd;
418
+ let item;
419
+ let key;
420
+ if (newLen === 0) {
421
+ if (len !== 0) {
422
+ if (options.onDestroy) {
423
+ for (let k = 0; k < len; k++)
424
+ options.onDestroy(mapped[k]);
425
+ }
426
+ items.length = 0;
427
+ mapped = [];
428
+ indexes.length = 0;
415
429
  }
416
- prevCache.delete(k);
417
- nextCache.set(k, record);
418
- nextValues.push(record.computation);
419
- if (untracked(record.source.idx) !== i) {
420
- untracked(() => record.source.idx.set(i));
421
- changed = true;
430
+ return mapped;
431
+ }
432
+ // Fast path for new create (init)
433
+ if (len === 0) {
434
+ for (j = 0; j < newLen; j++) {
435
+ item = newItems[j];
436
+ items[j] = item;
437
+ const indexSignal = signal(j, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
438
+ newIndexes[j] = indexSignal;
439
+ newMapped[j] = mapFn(item, indexSignal);
422
440
  }
423
441
  }
424
- if (prevCache.size > 0)
425
- changed = true;
426
- if (opt.onDestroy)
427
- prevCache.values().forEach((v) => opt.onDestroy?.(v.computation));
428
- // clear for next run
429
- prevCache.clear();
430
- freeMap = prevCache;
431
- return {
432
- cache: nextCache,
433
- values: changed ? nextValues : (prev?.value.values ?? []),
434
- };
435
- } });
436
- return computed(() => internal().values);
442
+ else {
443
+ newIndices.clear();
444
+ temp.length = 0;
445
+ tempIndexes.length = 0;
446
+ newIndicesNext.length = 0;
447
+ // Skip common prefix
448
+ for (start = 0, end = Math.min(len, newLen); start < end && getKey(items[start]) === getKey(newItems[start]); start++) {
449
+ newMapped[start] = mapped[start];
450
+ newIndexes[start] = indexes[start];
451
+ }
452
+ // Common suffix
453
+ for (end = len - 1, newEnd = newLen - 1; end >= start &&
454
+ newEnd >= start &&
455
+ getKey(items[end]) === getKey(newItems[newEnd]); end--, newEnd--) {
456
+ temp[newEnd] = mapped[end];
457
+ tempIndexes[newEnd] = indexes[end];
458
+ }
459
+ // 0) Prepare a map of all indices in newItems, scanning backwards
460
+ for (j = newEnd; j >= start; j--) {
461
+ item = newItems[j];
462
+ key = getKey(item);
463
+ i = newIndices.get(key);
464
+ newIndicesNext[j] = i === undefined ? -1 : i;
465
+ newIndices.set(key, j);
466
+ }
467
+ // 1) Step through old items: check if they are in new set
468
+ for (i = start; i <= end; i++) {
469
+ item = items[i];
470
+ key = getKey(item);
471
+ j = newIndices.get(key);
472
+ if (j !== undefined && j !== -1) {
473
+ temp[j] = mapped[i];
474
+ tempIndexes[j] = indexes[i];
475
+ j = newIndicesNext[j];
476
+ newIndices.set(key, j);
477
+ }
478
+ else {
479
+ if (options.onDestroy)
480
+ options.onDestroy(mapped[i]);
481
+ }
482
+ }
483
+ // 2) Set all new values
484
+ for (j = start; j < newLen; j++) {
485
+ if (j in temp) {
486
+ newMapped[j] = temp[j];
487
+ newIndexes[j] = tempIndexes[j];
488
+ untracked(() => newIndexes[j].set(j)); // Update index signal
489
+ }
490
+ else {
491
+ const indexSignal = signal(j, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
492
+ newIndexes[j] = indexSignal;
493
+ newMapped[j] = mapFn(newItems[j], indexSignal);
494
+ }
495
+ }
496
+ // 4) Save items for next update
497
+ items.length = newLen;
498
+ for (let k = 0; k < newLen; k++)
499
+ items[k] = newItems[k];
500
+ }
501
+ mapped = newMapped;
502
+ indexes.length = newLen;
503
+ for (let k = 0; k < newLen; k++)
504
+ indexes[k] = newIndexes[k];
505
+ return mapped;
506
+ });
507
+ });
508
+ }
509
+
510
+ function pooledKeys(src) {
511
+ const aBuf = new Set();
512
+ const bBuf = new Set();
513
+ let active = aBuf;
514
+ let spare = bBuf;
515
+ return computed(() => {
516
+ const val = src();
517
+ spare.clear();
518
+ for (const k in val)
519
+ if (Object.prototype.hasOwnProperty.call(val, k))
520
+ spare.add(k);
521
+ if (active.size === spare.size && active.isSubsetOf(spare))
522
+ return active;
523
+ const temp = active;
524
+ active = spare;
525
+ spare = temp;
526
+ return active;
527
+ });
528
+ }
529
+ function mapObject(source, mapFn, options = {}) {
530
+ const src = isSignal(source) ? source : computed(source);
531
+ const writable = (isWritableSignal(src)
532
+ ? src
533
+ : toWritable(src, () => {
534
+ // noop
535
+ })); // maximal overload internally
536
+ return linkedSignal({
537
+ source: pooledKeys(src),
538
+ computation: (next, prev) => {
539
+ const nextObj = {};
540
+ for (const k of next)
541
+ nextObj[k] =
542
+ prev && prev.source.has(k)
543
+ ? prev.value[k]
544
+ : mapFn(k, derived(writable, k));
545
+ if (options.onDestroy && prev && prev.source.size)
546
+ for (const k of prev.source)
547
+ if (!next.has(k))
548
+ options.onDestroy(prev.value[k]);
549
+ return nextObj;
550
+ },
551
+ }).asReadonly();
437
552
  }
438
553
 
439
554
  const frameStack = [];
440
- function current(frameAt = -1) {
441
- return frameStack.at(frameAt) ?? null;
555
+ function current() {
556
+ return frameStack.at(-1) ?? null;
442
557
  }
443
558
  function clearFrame(frame, userCleanups) {
444
559
  frame.parent = null;
@@ -501,42 +616,43 @@ function clearFrame(frame, userCleanups) {
501
616
  * @example
502
617
  * ```ts
503
618
  * const users = signal([
504
- { id: 1, name: 'Alice' },
505
- { id: 2, name: 'Bob' }
506
- ]);
507
-
508
- // The fine-grained mapped list
509
- const mappedUsers = mapArray(
510
- users,
511
- (userSignal, index) => {
512
- // 1. Create a fine-grained SIDE EFFECT for *this item*
513
- // This effect's lifetime is now tied to this specific item. created once on init of this index.
514
- const effectRef = nestedEffect(() => {
515
- // This only runs if *this* userSignal changes,
516
- // not if the whole list changes.
517
- console.log(`User ${index} updated:`, userSignal().name);
518
- });
519
-
520
- // 2. Return the data AND the cleanup logic
521
- return {
522
- // The mapped data
523
- label: computed(() => `User: ${userSignal().name}`),
524
-
525
- // The cleanup function
526
- destroyEffect: () => effectRef.destroy()
527
- };
528
- },
529
- {
530
- // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
531
- onDestroy: (mappedItem) => {
532
- mappedItem.destroyEffect();
533
- }
534
- }
535
- );
619
+ * { id: 1, name: 'Alice' },
620
+ * { id: 2, name: 'Bob' }
621
+ * ]);
622
+ *
623
+ * // The fine-grained mapped list
624
+ * const mappedUsers = mapArray(
625
+ * users,
626
+ * (userSignal, index) => {
627
+ * // 1. Create a fine-grained SIDE EFFECT for *this item*
628
+ * // This effect's lifetime is now tied to this specific item. created once on init of this index.
629
+ * const effectRef = nestedEffect(() => {
630
+ * // This only runs if *this* userSignal changes,
631
+ * // not if the whole list changes.
632
+ * console.log(`User ${index} updated:`, userSignal().name);
633
+ * });
634
+ *
635
+ * // 2. Return the data AND the cleanup logic
636
+ * return {
637
+ * // The mapped data
638
+ * label: computed(() => `User: ${userSignal().name}`),
639
+ *
640
+ * // The cleanup function
641
+ * destroyEffect: () => effectRef.destroy()
642
+ * };
643
+ * },
644
+ * {
645
+ * // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
646
+ * onDestroy: (mappedItem) => {
647
+ * mappedItem.destroyEffect();
648
+ * }
649
+ * }
650
+ * );
536
651
  * ```
537
652
  */
538
653
  function nestedEffect(effectFn, options) {
539
- const parent = current(options?.bindToFrame);
654
+ const bindToFrame = options?.bindToFrame ?? ((parent) => parent);
655
+ const parent = bindToFrame(current());
540
656
  const injector = options?.injector ?? parent?.injector ?? inject(Injector);
541
657
  const srcRef = untracked(() => {
542
658
  return effect((cleanup) => {
@@ -1440,8 +1556,6 @@ function sensors(track, opt) {
1440
1556
  }, {});
1441
1557
  }
1442
1558
 
1443
- // Credit to NGRX signal store, adaptation for purposes of supporting Writable/Mutable signals
1444
- // Link to source: https://github.com/ngrx/platform/blob/main/modules/signals/src/deep-signal.ts
1445
1559
  const TREAT_AS_VALUE = new Set([
1446
1560
  Date,
1447
1561
  Error,
@@ -1453,21 +1567,19 @@ const TREAT_AS_VALUE = new Set([
1453
1567
  WeakMap,
1454
1568
  WeakRef,
1455
1569
  Promise,
1570
+ Array,
1456
1571
  typeof Iterator !== 'undefined' ? Iterator : class {
1457
1572
  },
1458
- Array,
1459
1573
  ]);
1460
- function isIterable(value) {
1461
- return typeof value?.[Symbol.iterator] === 'function';
1462
- }
1463
1574
  function isRecord(value) {
1464
- if (value === null || typeof value !== 'object' || isIterable(value)) {
1575
+ if (value === null || typeof value !== 'object')
1576
+ return false;
1577
+ if (Array.isArray(value) ||
1578
+ typeof value[Symbol.iterator] === 'function')
1465
1579
  return false;
1466
- }
1467
1580
  let proto = Object.getPrototypeOf(value);
1468
- if (proto === Object.prototype) {
1581
+ if (proto === Object.prototype)
1469
1582
  return true;
1470
- }
1471
1583
  while (proto && proto !== Object.prototype) {
1472
1584
  if (TREAT_AS_VALUE.has(proto.constructor))
1473
1585
  return false;
@@ -1475,41 +1587,45 @@ function isRecord(value) {
1475
1587
  }
1476
1588
  return proto === Object.prototype;
1477
1589
  }
1478
- const STORE = Symbol(isDevMode() ? 'SIGNAL_STORE' : '');
1479
- const STORE_PROXY = Symbol(isDevMode() ? 'SIGNAL_STORE_PROXY' : '');
1590
+ const PROXY_CACHE = new WeakMap();
1480
1591
  function toStore(source, injector = inject(Injector)) {
1481
1592
  return new Proxy(source, {
1482
1593
  has(_, prop) {
1483
1594
  return Reflect.has(untracked(source), prop);
1484
1595
  },
1485
1596
  get(target, prop) {
1486
- const value = untracked(target);
1487
- if (!isRecord(value) || !(prop in value)) {
1488
- if (isSignal(target[prop]) && target[prop][STORE]) {
1489
- delete target[prop];
1490
- }
1491
- return target[prop];
1597
+ let storeCache = PROXY_CACHE.get(target);
1598
+ if (!storeCache) {
1599
+ storeCache = new Map();
1600
+ PROXY_CACHE.set(target, storeCache);
1492
1601
  }
1493
- if (target[prop][STORE_PROXY]) {
1494
- return target[prop][STORE_PROXY];
1602
+ const cachedRef = storeCache.get(prop);
1603
+ if (cachedRef) {
1604
+ const cached = cachedRef.deref();
1605
+ if (cached)
1606
+ return cached;
1607
+ storeCache.delete(prop); // Cleanup dead ref
1495
1608
  }
1496
- let computation;
1497
- if (!isSignal(target[prop])) {
1609
+ if (prop === 'set' ||
1610
+ prop === 'update' ||
1611
+ prop === 'asReadonly' ||
1612
+ typeof prop === 'symbol') {
1613
+ return target[prop];
1614
+ }
1615
+ const value = untracked(target);
1616
+ if (!isRecord(value))
1617
+ return target[prop];
1618
+ let computation = target[prop];
1619
+ if (!isSignal(computation)) {
1498
1620
  computation = derived(target, prop);
1499
1621
  Object.defineProperty(target, prop, {
1500
1622
  value: computation,
1501
1623
  configurable: true,
1624
+ writable: true,
1502
1625
  });
1503
- target[prop][STORE] = true;
1504
1626
  }
1505
- const proxy = toStore(target[prop], injector);
1506
- const cleanupRef = nestedEffect(() => {
1507
- if (Reflect.has(source(), prop))
1508
- return;
1509
- delete target[prop];
1510
- cleanupRef.destroy();
1511
- }, { injector });
1512
- target[prop][STORE_PROXY] = proxy;
1627
+ const proxy = toStore(computation, injector);
1628
+ storeCache.set(prop, new WeakRef(proxy));
1513
1629
  return proxy;
1514
1630
  },
1515
1631
  });
@@ -1995,5 +2111,5 @@ function withHistory(source, opt) {
1995
2111
  * Generated bundle index. Do not edit.
1996
2112
  */
1997
2113
 
1998
- export { combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, keyArray, map, mapArray, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2114
+ export { combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
1999
2115
  //# sourceMappingURL=mmstack-primitives.mjs.map