@mmstack/primitives 21.0.8 → 21.0.10

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. */
@@ -2140,5 +2242,5 @@ function withHistory(source, opt) {
2140
2242
  * Generated bundle index. Do not edit.
2141
2243
  */
2142
2244
 
2143
- 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 { batch, 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 };
2144
2246
  //# sourceMappingURL=mmstack-primitives.mjs.map