@mmstack/primitives 21.0.7 → 21.0.9

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { computed, untracked, signal, inject, DestroyRef, isWritableSignal as isWritableSignal$1, isSignal, linkedSignal, isDevMode, Injector, effect, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
2
+ import { computed, untracked, signal, inject, DestroyRef, isDevMode, Injector, effect, runInInjectionContext, isWritableSignal as isWritableSignal$1, isSignal, linkedSignal, ElementRef, PLATFORM_ID, Injectable } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
4
  import { SIGNAL } from '@angular/core/primitives/signals';
5
5
 
@@ -289,6 +289,252 @@ function isDerivation(sig) {
289
289
  return 'from' in sig;
290
290
  }
291
291
 
292
+ const frameStack = [];
293
+ function currentFrame() {
294
+ return frameStack.at(-1) ?? null;
295
+ }
296
+ function clearFrame(frame, userCleanups) {
297
+ frame.parent = null;
298
+ for (const fn of userCleanups) {
299
+ try {
300
+ fn();
301
+ }
302
+ catch (e) {
303
+ if (isDevMode())
304
+ console.error('Error destroying nested effect:', e);
305
+ }
306
+ }
307
+ userCleanups.length = 0;
308
+ for (const child of frame.children) {
309
+ try {
310
+ child.destroy();
311
+ }
312
+ catch (e) {
313
+ if (isDevMode())
314
+ console.error('Error destroying nested effect:', e);
315
+ }
316
+ }
317
+ frame.children.clear();
318
+ }
319
+ function pushFrame(frame) {
320
+ return frameStack.push(frame);
321
+ }
322
+ function popFrame() {
323
+ return frameStack.pop();
324
+ }
325
+
326
+ /**
327
+ * Creates an effect that can be nested, similar to SolidJS's `createEffect`.
328
+ *
329
+ * This primitive enables true hierarchical reactivity. A `nestedEffect` created
330
+ * within another `nestedEffect` is automatically destroyed and recreated when
331
+ * the parent re-runs.
332
+ *
333
+ * It automatically handles injector propagation and lifetime management, allowing
334
+ * you to create fine-grained, conditional side-effects that only track
335
+ * dependencies when they are "live".
336
+ *
337
+ * @param effectFn The side-effect function, which receives a cleanup register function.
338
+ * @param options (Optional) Angular's `CreateEffectOptions`.
339
+ * @returns An `EffectRef` for the created effect.
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
344
+ * const coldGuard = signal(false);
345
+ * const hotSignal = signal(0);
346
+ *
347
+ * nestedEffect(() => {
348
+ * // This outer effect only tracks `coldGuard`.
349
+ * if (coldGuard()) {
350
+ *
351
+ * // This inner effect is CREATED when coldGuard is true
352
+ * // and DESTROYED when it becomes false.
353
+ * nestedEffect(() => {
354
+ * // It only tracks `hotSignal` while it exists.
355
+ * console.log('Hot signal is:', hotSignal());
356
+ * });
357
+ * }
358
+ * // If `coldGuard` is false, this outer effect does not track `hotSignal`.
359
+ * });
360
+ * ```
361
+ * @example
362
+ * ```ts
363
+ * const users = signal([
364
+ * { id: 1, name: 'Alice' },
365
+ * { id: 2, name: 'Bob' }
366
+ * ]);
367
+ *
368
+ * // The fine-grained mapped list
369
+ * const mappedUsers = mapArray(
370
+ * users,
371
+ * (userSignal, index) => {
372
+ * // 1. Create a fine-grained SIDE EFFECT for *this item*
373
+ * // This effect's lifetime is now tied to this specific item. created once on init of this index.
374
+ * const effectRef = nestedEffect(() => {
375
+ * // This only runs if *this* userSignal changes,
376
+ * // not if the whole list changes.
377
+ * console.log(`User ${index} updated:`, userSignal().name);
378
+ * });
379
+ *
380
+ * // 2. Return the data AND the cleanup logic
381
+ * return {
382
+ * // The mapped data
383
+ * label: computed(() => `User: ${userSignal().name}`),
384
+ *
385
+ * // The cleanup function
386
+ * destroyEffect: () => effectRef.destroy()
387
+ * };
388
+ * },
389
+ * {
390
+ * // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
391
+ * onDestroy: (mappedItem) => {
392
+ * mappedItem.destroyEffect();
393
+ * }
394
+ * }
395
+ * );
396
+ * ```
397
+ */
398
+ function nestedEffect(effectFn, options) {
399
+ const bindToFrame = options?.bindToFrame ?? ((parent) => parent);
400
+ const parent = bindToFrame(currentFrame());
401
+ const injector = options?.injector ?? parent?.injector ?? inject(Injector);
402
+ let isDestroyed = false;
403
+ const srcRef = untracked(() => {
404
+ return effect((cleanup) => {
405
+ if (isDestroyed)
406
+ return;
407
+ const frame = {
408
+ injector,
409
+ parent,
410
+ children: new Set(),
411
+ };
412
+ const userCleanups = [];
413
+ pushFrame(frame);
414
+ try {
415
+ effectFn((fn) => {
416
+ userCleanups.push(fn);
417
+ });
418
+ }
419
+ finally {
420
+ popFrame();
421
+ }
422
+ return cleanup(() => clearFrame(frame, userCleanups));
423
+ }, {
424
+ ...options,
425
+ injector,
426
+ manualCleanup: !!parent,
427
+ });
428
+ });
429
+ const ref = {
430
+ ...srcRef,
431
+ destroy: () => {
432
+ if (isDestroyed)
433
+ return;
434
+ isDestroyed = true;
435
+ parent?.children.delete(ref);
436
+ srcRef.destroy();
437
+ },
438
+ };
439
+ parent?.children.add(ref);
440
+ if (parent === null)
441
+ injector.get(DestroyRef).onDestroy(() => ref.destroy());
442
+ return ref;
443
+ }
444
+
445
+ let isBatching = false;
446
+ const pendingEffects = new Set();
447
+ function batch(fn) {
448
+ if (isBatching)
449
+ return fn();
450
+ isBatching = true;
451
+ try {
452
+ fn();
453
+ }
454
+ finally {
455
+ isBatching = false;
456
+ for (const run of pendingEffects) {
457
+ pendingEffects.delete(run);
458
+ run();
459
+ }
460
+ }
461
+ }
462
+ const ALWAYS_FALSE = () => false;
463
+ /**
464
+ * A synchronous version of Angular's effect, optimized for DOM-heavy renderers.
465
+ * Runs immediately on creation and on dependency changes, bypassing the microtask queue.
466
+ * * @example
467
+ * renderEffect((onCleanup) => {
468
+ * const el = document.createElement('div');
469
+ * el.textContent = count();
470
+ * container.appendChild(el);
471
+ * onCleanup(() => el.remove());
472
+ * });
473
+ */
474
+ function renderEffect(effectFn, options) {
475
+ const parent = currentFrame();
476
+ const injector = options?.injector ?? parent?.injector ?? inject(Injector);
477
+ let cleanupFn;
478
+ let isDestroyed = false;
479
+ const tracker = computed(() => {
480
+ if (cleanupFn) {
481
+ try {
482
+ cleanupFn();
483
+ }
484
+ catch {
485
+ // noop
486
+ }
487
+ cleanupFn = undefined;
488
+ }
489
+ const frame = {
490
+ injector,
491
+ parent,
492
+ children: new Set(),
493
+ };
494
+ const userCleanups = [];
495
+ pushFrame(frame);
496
+ try {
497
+ effectFn((fn) => userCleanups.push(fn));
498
+ }
499
+ finally {
500
+ popFrame();
501
+ cleanupFn = () => clearFrame(frame, userCleanups);
502
+ }
503
+ }, { ...(ngDevMode ? { debugName: "tracker" } : {}), equal: ALWAYS_FALSE });
504
+ const run = (isFromBridge = false) => {
505
+ if (isDestroyed)
506
+ return;
507
+ if (isBatching && !isFromBridge) {
508
+ pendingEffects.add(run);
509
+ return;
510
+ }
511
+ tracker();
512
+ };
513
+ let rootEffectRef = null;
514
+ const ref = {
515
+ run,
516
+ destroy: () => {
517
+ if (isDestroyed)
518
+ return;
519
+ isDestroyed = true;
520
+ rootEffectRef?.destroy();
521
+ parent?.children.delete(ref);
522
+ pendingEffects.delete(run);
523
+ if (cleanupFn)
524
+ cleanupFn();
525
+ },
526
+ };
527
+ parent?.children.add(ref);
528
+ if (parent === null) {
529
+ rootEffectRef = runInInjectionContext(injector, () => untracked(() => effect(() => run(true), { injector, manualCleanup: true })));
530
+ injector.get(DestroyRef).onDestroy(() => ref.destroy());
531
+ }
532
+ else {
533
+ untracked(() => run(true));
534
+ }
535
+ return ref;
536
+ }
537
+
292
538
  function isWritableSignal(value) {
293
539
  return isWritableSignal$1(value);
294
540
  }
@@ -429,7 +675,6 @@ function keyArray(source, mapFn, options = {}) {
429
675
  }
430
676
  return mapped;
431
677
  }
432
- // Fast path for new create (init)
433
678
  if (len === 0) {
434
679
  for (j = 0; j < newLen; j++) {
435
680
  item = newItems[j];
@@ -444,19 +689,16 @@ function keyArray(source, mapFn, options = {}) {
444
689
  temp.length = 0;
445
690
  tempIndexes.length = 0;
446
691
  newIndicesNext.length = 0;
447
- // Skip common prefix
448
692
  for (start = 0, end = Math.min(len, newLen); start < end && getKey(items[start]) === getKey(newItems[start]); start++) {
449
693
  newMapped[start] = mapped[start];
450
694
  newIndexes[start] = indexes[start];
451
695
  }
452
- // Common suffix
453
696
  for (end = len - 1, newEnd = newLen - 1; end >= start &&
454
697
  newEnd >= start &&
455
698
  getKey(items[end]) === getKey(newItems[newEnd]); end--, newEnd--) {
456
699
  temp[newEnd] = mapped[end];
457
700
  tempIndexes[newEnd] = indexes[end];
458
701
  }
459
- // 0) Prepare a map of all indices in newItems, scanning backwards
460
702
  for (j = newEnd; j >= start; j--) {
461
703
  item = newItems[j];
462
704
  key = getKey(item);
@@ -464,7 +706,6 @@ function keyArray(source, mapFn, options = {}) {
464
706
  newIndicesNext[j] = i === undefined ? -1 : i;
465
707
  newIndices.set(key, j);
466
708
  }
467
- // 1) Step through old items: check if they are in new set
468
709
  for (i = start; i <= end; i++) {
469
710
  item = items[i];
470
711
  key = getKey(item);
@@ -482,10 +723,10 @@ function keyArray(source, mapFn, options = {}) {
482
723
  }
483
724
  // 2) Set all new values
484
725
  for (j = start; j < newLen; j++) {
485
- if (j in temp) {
726
+ if (temp[j] !== undefined) {
486
727
  newMapped[j] = temp[j];
487
728
  newIndexes[j] = tempIndexes[j];
488
- untracked(() => newIndexes[j].set(j)); // Update index signal
729
+ newIndexes[j].set(j);
489
730
  }
490
731
  else {
491
732
  const indexSignal = signal(j, ...(ngDevMode ? [{ debugName: "indexSignal" }] : []));
@@ -493,7 +734,6 @@ function keyArray(source, mapFn, options = {}) {
493
734
  newMapped[j] = mapFn(newItems[j], indexSignal);
494
735
  }
495
736
  }
496
- // 4) Save items for next update
497
737
  items.length = newLen;
498
738
  for (let k = 0; k < newLen; k++)
499
739
  items[k] = newItems[k];
@@ -551,144 +791,6 @@ function mapObject(source, mapFn, options = {}) {
551
791
  }).asReadonly();
552
792
  }
553
793
 
554
- const frameStack = [];
555
- function current() {
556
- return frameStack.at(-1) ?? null;
557
- }
558
- function clearFrame(frame, userCleanups) {
559
- frame.parent = null;
560
- for (const child of frame.children) {
561
- try {
562
- child.destroy();
563
- }
564
- catch (e) {
565
- if (isDevMode())
566
- console.error('Error destroying nested effect:', e);
567
- }
568
- }
569
- frame.children.clear();
570
- for (const fn of userCleanups) {
571
- try {
572
- fn();
573
- }
574
- catch (e) {
575
- if (isDevMode())
576
- console.error('Error destroying nested effect:', e);
577
- }
578
- }
579
- userCleanups.length = 0;
580
- }
581
- /**
582
- * Creates an effect that can be nested, similar to SolidJS's `createEffect`.
583
- *
584
- * This primitive enables true hierarchical reactivity. A `nestedEffect` created
585
- * within another `nestedEffect` is automatically destroyed and recreated when
586
- * the parent re-runs.
587
- *
588
- * It automatically handles injector propagation and lifetime management, allowing
589
- * you to create fine-grained, conditional side-effects that only track
590
- * dependencies when they are "live".
591
- *
592
- * @param effectFn The side-effect function, which receives a cleanup register function.
593
- * @param options (Optional) Angular's `CreateEffectOptions`.
594
- * @returns An `EffectRef` for the created effect.
595
- *
596
- * @example
597
- * ```ts
598
- * // Assume `coldGuard` changes rarely, but `hotSignal` changes often.
599
- * const coldGuard = signal(false);
600
- * const hotSignal = signal(0);
601
- *
602
- * nestedEffect(() => {
603
- * // This outer effect only tracks `coldGuard`.
604
- * if (coldGuard()) {
605
- *
606
- * // This inner effect is CREATED when coldGuard is true
607
- * // and DESTROYED when it becomes false.
608
- * nestedEffect(() => {
609
- * // It only tracks `hotSignal` while it exists.
610
- * console.log('Hot signal is:', hotSignal());
611
- * });
612
- * }
613
- * // If `coldGuard` is false, this outer effect does not track `hotSignal`.
614
- * });
615
- * ```
616
- * @example
617
- * ```ts
618
- * const users = signal([
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
- * );
651
- * ```
652
- */
653
- function nestedEffect(effectFn, options) {
654
- const bindToFrame = options?.bindToFrame ?? ((parent) => parent);
655
- const parent = bindToFrame(current());
656
- const injector = options?.injector ?? parent?.injector ?? inject(Injector);
657
- const srcRef = untracked(() => {
658
- return effect((cleanup) => {
659
- const frame = {
660
- injector,
661
- parent,
662
- children: new Set(),
663
- };
664
- const userCleanups = [];
665
- frameStack.push(frame);
666
- try {
667
- effectFn((fn) => {
668
- userCleanups.push(fn);
669
- });
670
- }
671
- finally {
672
- frameStack.pop();
673
- }
674
- return cleanup(() => clearFrame(frame, userCleanups));
675
- }, {
676
- ...options,
677
- injector,
678
- manualCleanup: !!parent,
679
- });
680
- });
681
- const ref = {
682
- ...srcRef,
683
- destroy: () => {
684
- parent?.children.delete(ref);
685
- srcRef.destroy();
686
- },
687
- };
688
- parent?.children.add(ref);
689
- return ref;
690
- }
691
-
692
794
  /** Project with optional equality. Pure & sync. */
693
795
  const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
694
796
  /** Combine with another signal using a projector. */
@@ -1590,7 +1692,7 @@ function toStore(source, injector) {
1590
1692
  // noop
1591
1693
  });
1592
1694
  const isMutableSource = isMutable(writableSource);
1593
- return new Proxy(writableSource, {
1695
+ const s = new Proxy(writableSource, {
1594
1696
  has(_, prop) {
1595
1697
  return Reflect.has(untracked(source), prop);
1596
1698
  },
@@ -1610,6 +1712,12 @@ function toStore(source, injector) {
1610
1712
  get(target, prop) {
1611
1713
  if (prop === IS_STORE)
1612
1714
  return true;
1715
+ if (prop === 'asReadonlyStore')
1716
+ return () => {
1717
+ if (!isWritableSignal(source))
1718
+ return s;
1719
+ return untracked(() => toStore(source.asReadonly(), injector));
1720
+ };
1613
1721
  if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
1614
1722
  return target[prop];
1615
1723
  let storeCache = PROXY_CACHE.get(target);
@@ -1651,6 +1759,7 @@ function toStore(source, injector) {
1651
1759
  return proxy;
1652
1760
  },
1653
1761
  });
1762
+ return s;
1654
1763
  }
1655
1764
  function store(value, opt) {
1656
1765
  return toStore(signal(value, opt));
@@ -2133,5 +2242,5 @@ function withHistory(source, opt) {
2133
2242
  * Generated bundle index. Do not edit.
2134
2243
  */
2135
2244
 
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 };
2245
+ 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, renderEffect, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2137
2246
  //# sourceMappingURL=mmstack-primitives.mjs.map