@mmstack/primitives 21.0.5 → 21.0.7

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,70 +372,183 @@ 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 = [];
@@ -501,38 +616,38 @@ 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) {
@@ -1441,76 +1556,98 @@ function sensors(track, opt) {
1441
1556
  }, {});
1442
1557
  }
1443
1558
 
1444
- // Credit to NGRX signal store, adaptation for purposes of supporting Writable/Mutable signals
1445
- // Link to source: https://github.com/ngrx/platform/blob/main/modules/signals/src/deep-signal.ts
1446
- const TREAT_AS_VALUE = new Set([
1447
- Date,
1448
- Error,
1449
- RegExp,
1450
- ArrayBuffer,
1451
- DataView,
1452
- Function,
1453
- WeakSet,
1454
- WeakMap,
1455
- WeakRef,
1456
- Promise,
1457
- typeof Iterator !== 'undefined' ? Iterator : class {
1458
- },
1459
- Array,
1460
- ]);
1461
- function isIterable(value) {
1462
- return typeof value?.[Symbol.iterator] === 'function';
1463
- }
1464
1559
  function isRecord(value) {
1465
- if (value === null || typeof value !== 'object' || isIterable(value)) {
1560
+ if (value === null || typeof value !== 'object')
1466
1561
  return false;
1467
- }
1468
- let proto = Object.getPrototypeOf(value);
1469
- if (proto === Object.prototype) {
1470
- return true;
1471
- }
1472
- while (proto && proto !== Object.prototype) {
1473
- if (TREAT_AS_VALUE.has(proto.constructor))
1474
- return false;
1475
- proto = Object.getPrototypeOf(proto);
1476
- }
1477
- return proto === Object.prototype;
1562
+ const proto = Object.getPrototypeOf(value);
1563
+ return proto === Object.prototype || proto === null;
1478
1564
  }
1479
- const STORE = Symbol(isDevMode() ? 'SIGNAL_STORE' : '');
1480
- const STORE_PROXY = Symbol(isDevMode() ? 'SIGNAL_STORE_PROXY' : '');
1481
- function toStore(source, injector = inject(Injector)) {
1482
- return new Proxy(source, {
1565
+ const IS_STORE = Symbol('MMSTACK::IS_STORE');
1566
+ const PROXY_CACHE = new WeakMap();
1567
+ const SIGNAL_FN_PROP = new Set([
1568
+ 'set',
1569
+ 'update',
1570
+ 'mutate',
1571
+ 'inline',
1572
+ 'asReadonly',
1573
+ ]);
1574
+ /**
1575
+ * @experimental This API is experimental and may change or be removed in future releases.
1576
+ * Converts a Signal into a deep-observable Store.
1577
+ * Accessing nested properties returns a derived Signal of that path.
1578
+ * @example
1579
+ * const state = store({ user: { name: 'John' } });
1580
+ * const nameSignal = state.user.name; // WritableSignal<string>
1581
+ */
1582
+ function toStore(source, injector) {
1583
+ if (source[IS_STORE])
1584
+ return source;
1585
+ if (!injector)
1586
+ injector = inject(Injector);
1587
+ const writableSource = isWritableSignal(source)
1588
+ ? source
1589
+ : toWritable(source, () => {
1590
+ // noop
1591
+ });
1592
+ const isMutableSource = isMutable(writableSource);
1593
+ return new Proxy(writableSource, {
1483
1594
  has(_, prop) {
1484
1595
  return Reflect.has(untracked(source), prop);
1485
1596
  },
1597
+ ownKeys() {
1598
+ const v = untracked(source);
1599
+ return isRecord(v) ? Reflect.ownKeys(v) : [];
1600
+ },
1601
+ getOwnPropertyDescriptor(_, prop) {
1602
+ const value = untracked(source);
1603
+ if (!isRecord(value) || !(prop in value))
1604
+ return;
1605
+ return {
1606
+ enumerable: true,
1607
+ configurable: true,
1608
+ };
1609
+ },
1486
1610
  get(target, prop) {
1487
- const value = untracked(target);
1488
- if (!isRecord(value) || !(prop in value)) {
1489
- if (isSignal(target[prop]) && target[prop][STORE]) {
1490
- delete target[prop];
1491
- }
1611
+ if (prop === IS_STORE)
1612
+ return true;
1613
+ if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
1492
1614
  return target[prop];
1615
+ let storeCache = PROXY_CACHE.get(target);
1616
+ if (!storeCache) {
1617
+ storeCache = new Map();
1618
+ PROXY_CACHE.set(target, storeCache);
1493
1619
  }
1494
- if (target[prop][STORE_PROXY]) {
1495
- return target[prop][STORE_PROXY];
1620
+ const cachedRef = storeCache.get(prop);
1621
+ if (cachedRef) {
1622
+ const cached = cachedRef.deref();
1623
+ if (cached)
1624
+ return cached;
1625
+ storeCache.delete(prop);
1496
1626
  }
1497
- let computation;
1498
- if (!isSignal(target[prop])) {
1499
- computation = derived(target, prop);
1500
- Object.defineProperty(target, prop, {
1501
- value: computation,
1502
- configurable: true,
1627
+ const value = untracked(target);
1628
+ const valueIsRecord = isRecord(value);
1629
+ const equalFn = valueIsRecord && isMutableSource && typeof value[prop] === 'object'
1630
+ ? () => false
1631
+ : undefined;
1632
+ const computation = valueIsRecord
1633
+ ? derived(target, prop, { equal: equalFn })
1634
+ : derived(target, {
1635
+ from: (v) => v?.[prop],
1636
+ onChange: (newValue) => target.update((v) => {
1637
+ if (v === null || v === undefined)
1638
+ return v;
1639
+ try {
1640
+ v[prop] = newValue;
1641
+ }
1642
+ catch (e) {
1643
+ if (isDevMode())
1644
+ console.error(`[store] Failed to set property "${String(prop)}"`, e);
1645
+ }
1646
+ return v;
1647
+ }),
1503
1648
  });
1504
- target[prop][STORE] = true;
1505
- }
1506
- const proxy = toStore(target[prop], injector);
1507
- const cleanupRef = nestedEffect(() => {
1508
- if (Reflect.has(source(), prop))
1509
- return;
1510
- delete target[prop];
1511
- cleanupRef.destroy();
1512
- }, { injector });
1513
- target[prop][STORE_PROXY] = proxy;
1649
+ const proxy = toStore(computation, injector);
1650
+ storeCache.set(prop, new WeakRef(proxy));
1514
1651
  return proxy;
1515
1652
  },
1516
1653
  });
@@ -1996,5 +2133,5 @@ function withHistory(source, opt) {
1996
2133
  * Generated bundle index. Do not edit.
1997
2134
  */
1998
2135
 
1999
- 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 };
2136
+ 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 };
2000
2137
  //# sourceMappingURL=mmstack-primitives.mjs.map