@mmstack/primitives 21.2.2 → 21.4.0

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.
@@ -628,6 +628,38 @@ function provideForwardingTransitionScope() {
628
628
  function getTransitionScope(injector) {
629
629
  return injector.get(TRANSITION_SCOPE, null);
630
630
  }
631
+ /**
632
+ * @internal Transaction-attributed pending for `startTransition`/`startTransaction`: like
633
+ * `scope.pending`, but loads already in flight when the tracker is created are NOT attributed —
634
+ * a pre-existing background load can neither settle the transaction early nor block its settle
635
+ * forever. A pre-existing flight is excluded only until it first settles; a later re-trigger of
636
+ * the same resource (e.g. the transaction's write changed its request) counts as the
637
+ * transaction's own work.
638
+ */
639
+ function createAttributedPending(scope) {
640
+ const isInFlight = (ref) => {
641
+ const s = untracked(ref.status);
642
+ return s === 'loading' || s === 'reloading';
643
+ };
644
+ const preexisting = new Set(untracked(scope.resources).filter(isInFlight));
645
+ return computed(() => {
646
+ let pending = false;
647
+ for (const ref of scope.resources()) {
648
+ const s = ref.status();
649
+ const loading = s === 'loading' || s === 'reloading';
650
+ if (preexisting.has(ref)) {
651
+ // deletes are monotonic, so this stays sound under re-computation
652
+ if (loading)
653
+ continue;
654
+ preexisting.delete(ref);
655
+ continue;
656
+ }
657
+ if (loading)
658
+ pending = true;
659
+ }
660
+ return pending;
661
+ });
662
+ }
631
663
  /**
632
664
  * Returns a register function bound to the nearest transition scope: it adds a resource
633
665
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
@@ -665,38 +697,43 @@ function registerResource(res, opt) {
665
697
  function injectStartTransition() {
666
698
  const scope = injectTransitionScope();
667
699
  const injector = inject(Injector);
700
+ const destroyRef = inject(DestroyRef);
668
701
  const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
669
702
  return (fn) => {
703
+ // attributed: loads already in flight when the transition starts are not ours —
704
+ // they can neither settle this transition early nor block it forever
705
+ const pending = createAttributedPending(scope);
670
706
  untracked(fn);
671
707
  let sawPending = false;
672
708
  const done = new Promise((resolve) => {
709
+ const settle = () => {
710
+ releaseDestroy();
711
+ watcher.destroy();
712
+ resolve();
713
+ };
673
714
  const watcher = effect(() => {
674
- const p = scope.pending();
715
+ const p = pending();
675
716
  if (p)
676
717
  sawPending = true;
677
718
  // settle: requests went in flight and then drained
678
- if (sawPending && !p) {
679
- watcher.destroy();
680
- resolve();
681
- }
719
+ if (sawPending && !p)
720
+ settle();
682
721
  }, { ...(ngDevMode ? { debugName: "watcher" } : /* istanbul ignore next */ {}), injector });
722
+ // a destroy mid-flight kills the watcher — resolve so awaiters never hang
723
+ const releaseDestroy = destroyRef.onDestroy(settle);
683
724
  if (onServer) {
684
- if (!untracked(scope.pending)) {
685
- watcher.destroy();
686
- resolve();
687
- }
725
+ if (!untracked(pending))
726
+ settle();
688
727
  return;
689
728
  }
690
729
  // no-async fallback: once the reactive system has processed the writes (afterNextRender),
691
730
  // if nothing ever went in flight, the transition is already complete.
692
731
  afterNextRender(() => {
693
- if (!sawPending && !untracked(scope.pending)) {
694
- watcher.destroy();
695
- resolve();
696
- }
732
+ if (!sawPending && !untracked(pending))
733
+ settle();
697
734
  }, { injector });
698
735
  });
699
- return { pending: scope.pending, done };
736
+ return { pending, done };
700
737
  };
701
738
  }
702
739
 
@@ -767,8 +804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
767
804
  }] });
768
805
  /**
769
806
  * Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
770
- * the resources to coordinate are registered *above* the boundary (e.g. an app-builder page whose
771
- * manifests/connectors register at a higher injector), so the boundary observes that outer scope
807
+ * the resources to coordinate are registered *above* the boundary so the boundary observes that outer scope
772
808
  * rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
773
809
  * ancestor.
774
810
  */
@@ -833,9 +869,13 @@ function runInTransaction(txn, fn) {
833
869
  function injectStartTransaction() {
834
870
  const scope = injectTransitionScope();
835
871
  const injector = inject(Injector);
872
+ const destroyRef = inject(DestroyRef);
836
873
  const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
837
874
  return (fn) => {
838
875
  const txn = createTransaction();
876
+ // attributed: loads already in flight when the transaction starts are not ours —
877
+ // they can neither commit this transaction early nor block its settle forever
878
+ const pending = createAttributedPending(scope);
839
879
  // Hold BEFORE the writes, so the display freezes at pre-transaction values.
840
880
  scope.beginHold();
841
881
  let finished = false;
@@ -852,6 +892,7 @@ function injectStartTransaction() {
852
892
  if (finished)
853
893
  return;
854
894
  finished = true;
895
+ releaseDestroy();
855
896
  watcher?.destroy();
856
897
  if (restore)
857
898
  txn.restore();
@@ -860,6 +901,10 @@ function injectStartTransaction() {
860
901
  scope.endHold();
861
902
  resolveDone();
862
903
  };
904
+ // The scope may outlive the calling context (a component transacting on an ancestor
905
+ // boundary): a destroy mid-flight kills the settle watcher, so without this the hold
906
+ // would leak and freeze the surviving scope forever. Keep the writes — they landed live.
907
+ const releaseDestroy = destroyRef.onDestroy(() => finish(false));
863
908
  try {
864
909
  runInTransaction(txn, fn);
865
910
  }
@@ -869,25 +914,25 @@ function injectStartTransaction() {
869
914
  }
870
915
  let sawPending = false;
871
916
  watcher = effect(() => {
872
- const p = scope.pending();
917
+ const p = pending();
873
918
  if (p)
874
919
  sawPending = true;
875
920
  if (sawPending && !p)
876
921
  finish(false);
877
922
  }, { injector });
878
923
  if (onServer) {
879
- if (!untracked(scope.pending))
924
+ if (!untracked(pending))
880
925
  finish(false);
881
926
  }
882
927
  else {
883
928
  // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
884
929
  afterNextRender(() => {
885
- if (!sawPending && !untracked(scope.pending))
930
+ if (!sawPending && !untracked(pending))
886
931
  finish(false);
887
932
  }, { injector });
888
933
  }
889
934
  return {
890
- pending: scope.pending,
935
+ pending,
891
936
  done,
892
937
  abort: () => finish(true),
893
938
  };
@@ -2893,13 +2938,21 @@ const IDLE = {
2893
2938
  pointerId: null,
2894
2939
  modifiers: { shift: false, alt: false, ctrl: false, meta: false },
2895
2940
  button: -1,
2941
+ pointerType: '',
2942
+ origin: null,
2943
+ cancelled: false,
2896
2944
  };
2945
+ /** Terminal state of an aborted gesture — same idle shape, `cancelled: true`. */
2946
+ const CANCELLED = { ...IDLE, cancelled: true };
2897
2947
  function stateEqual(a, b) {
2898
2948
  return (a.active === b.active &&
2949
+ a.cancelled === b.cancelled &&
2899
2950
  a.pointerId === b.pointerId &&
2900
2951
  a.current.x === b.current.x &&
2901
2952
  a.current.y === b.current.y &&
2902
2953
  a.button === b.button &&
2954
+ a.pointerType === b.pointerType &&
2955
+ a.origin === b.origin &&
2903
2956
  a.modifiers.shift === b.modifiers.shift &&
2904
2957
  a.modifiers.alt === b.modifiers.alt &&
2905
2958
  a.modifiers.ctrl === b.modifiers.ctrl &&
@@ -2933,7 +2986,7 @@ function createPointerDrag(opt) {
2933
2986
  return base;
2934
2987
  }
2935
2988
  const hostRef = inject((ElementRef), { optional: true });
2936
- const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], debugName = 'pointerDrag', } = opt ?? {};
2989
+ const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
2937
2990
  const resolve = (t) => {
2938
2991
  if (!t)
2939
2992
  return null;
@@ -2954,9 +3007,12 @@ function createPointerDrag(opt) {
2954
3007
  equal: stateEqual,
2955
3008
  debugName,
2956
3009
  });
3010
+ const threshold2 = activationThreshold * activationThreshold;
2957
3011
  let startPoint = { x: 0, y: 0 };
2958
3012
  let activePointerId = null;
2959
3013
  let activeButton = -1;
3014
+ let activePointerType = '';
3015
+ let activeOrigin = null;
2960
3016
  let activated = false;
2961
3017
  let gesture = null;
2962
3018
  const coord = (e) => coordinateSpace === 'page'
@@ -2968,22 +3024,24 @@ function createPointerDrag(opt) {
2968
3024
  ctrl: e.ctrlKey,
2969
3025
  meta: e.metaKey,
2970
3026
  });
2971
- const end = () => {
3027
+ const end = (cancelled = false) => {
2972
3028
  gesture?.abort();
2973
3029
  gesture = null;
2974
3030
  activePointerId = null;
2975
3031
  activeButton = -1;
3032
+ activePointerType = '';
3033
+ activeOrigin = null;
2976
3034
  activated = false;
2977
- state.set(IDLE);
2978
- state.flush(); // terminal transition: reflect IDLE now, not on the trailing edge
3035
+ state.set(cancelled ? CANCELLED : IDLE);
3036
+ state.flush(); // terminal transition: reflect idle now, not on the trailing edge
2979
3037
  };
2980
3038
  const onMove = (e) => {
2981
3039
  if (e.pointerId !== activePointerId)
2982
3040
  return;
2983
3041
  const current = coord(e);
2984
3042
  const delta = { x: current.x - startPoint.x, y: current.y - startPoint.y };
2985
- if (!activated && Math.hypot(delta.x, delta.y) >= activationThreshold) {
2986
- activated = true;
3043
+ if (!activated && delta.x * delta.x + delta.y * delta.y >= threshold2) {
3044
+ activated = true; // squared compare — no sqrt on the pre-activation path
2987
3045
  }
2988
3046
  state.set({
2989
3047
  active: activated,
@@ -2993,6 +3051,9 @@ function createPointerDrag(opt) {
2993
3051
  pointerId: activePointerId,
2994
3052
  modifiers: mods(e),
2995
3053
  button: activeButton, // pointermove button is -1; keep the down-button
3054
+ pointerType: activePointerType,
3055
+ origin: activeOrigin,
3056
+ cancelled: false,
2996
3057
  });
2997
3058
  };
2998
3059
  const onUp = (e) => {
@@ -3001,22 +3062,28 @@ function createPointerDrag(opt) {
3001
3062
  };
3002
3063
  const onCancel = (e) => {
3003
3064
  if (e.pointerId === activePointerId)
3004
- end();
3065
+ end(true);
3005
3066
  };
3006
3067
  const onKey = (e) => {
3007
3068
  if (e.key === 'Escape' && activePointerId !== null)
3008
- end();
3069
+ end(true);
3009
3070
  };
3010
3071
  const onDown = (el) => (e) => {
3011
3072
  if (activePointerId !== null)
3012
3073
  return;
3013
3074
  if (!buttons.includes(e.button))
3014
3075
  return;
3015
- if (handleSelector && !e.target?.closest?.(handleSelector)) {
3016
- return;
3017
- }
3076
+ const matched = handleSelector
3077
+ ? e.target?.closest?.(handleSelector)
3078
+ : el;
3079
+ if (!matched)
3080
+ return; // handleSelector set but pointerdown landed outside a handle
3081
+ if (stopPropagation)
3082
+ e.stopPropagation(); // claim it: an outer sensor won't also start
3018
3083
  activePointerId = e.pointerId;
3019
3084
  activeButton = e.button;
3085
+ activePointerType = e.pointerType;
3086
+ activeOrigin = matched;
3020
3087
  activated = false;
3021
3088
  startPoint = coord(e);
3022
3089
  try {
@@ -3042,6 +3109,9 @@ function createPointerDrag(opt) {
3042
3109
  pointerId: e.pointerId,
3043
3110
  modifiers: mods(e),
3044
3111
  button: e.button,
3112
+ pointerType: activePointerType,
3113
+ origin: activeOrigin,
3114
+ cancelled: false,
3045
3115
  });
3046
3116
  };
3047
3117
  const attach = (el) => {
@@ -3051,7 +3121,7 @@ function createPointerDrag(opt) {
3051
3121
  });
3052
3122
  return () => {
3053
3123
  controller.abort();
3054
- end();
3124
+ end(true); // teardown mid-gesture is an abort, not a drop
3055
3125
  };
3056
3126
  };
3057
3127
  if (isSignal(target)) {
@@ -3069,7 +3139,7 @@ function createPointerDrag(opt) {
3069
3139
  }
3070
3140
  const base = state.asReadonly();
3071
3141
  base.unthrottled = state.original;
3072
- base.cancel = end;
3142
+ base.cancel = () => end(true);
3073
3143
  return base;
3074
3144
  }
3075
3145
 
@@ -3600,17 +3670,14 @@ function isLeaf(value) {
3600
3670
 
3601
3671
  const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
3602
3672
  const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
3673
+ const STORE_SHARED_GLOBALS = Symbol('@mmstack/primitives::store/STORE_SHARED_GLOBALS');
3674
+ const STORE_SHARED_OPTIONS = Symbol('@mmstack/primitives::store/STORE_SHARED_OPTIONS');
3603
3675
  /**
3604
3676
  * @internal Brand carrying a store's writability ('mutable' | 'writable' | 'readonly'), stamped
3605
3677
  * on every store proxy. Read by `extendStore` instead of re-deriving via `isWritableSignal`,
3606
3678
  * which would mis-classify a readonly scoped store (its backing `toWritable` still has a `set`).
3607
3679
  */
3608
3680
  const STORE_KIND = Symbol('@mmstack/primitives::store/STORE_KIND');
3609
- /**
3610
- * @internal Brand exposing the injector a store was built with, so `extendStore` inherits it the
3611
- * same way `store.extend(...)` does (via closure) — no injection context needed at the call site.
3612
- */
3613
- const STORE_INJECTOR = Symbol('@mmstack/primitives::store/STORE_INJECTOR');
3614
3681
  const SIGNAL_FN_PROP = new Set([
3615
3682
  'set',
3616
3683
  'update',
@@ -3618,21 +3685,20 @@ const SIGNAL_FN_PROP = new Set([
3618
3685
  'inline',
3619
3686
  'asReadonly',
3620
3687
  ]);
3621
- /**
3622
- * @internal
3623
- * Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
3624
- * Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
3625
- */
3626
- const PROXY_CACHE = new WeakMap();
3627
- /**
3628
- * @internal
3629
- * Test-only handle on the finalization registry (deliberately NOT re-exported from the public
3630
- * barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
3631
- */
3632
- const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
3633
- const storeCache = PROXY_CACHE.get(target);
3634
- if (storeCache)
3635
- storeCache.delete(prop);
3688
+ const PROXY_CACHE_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cache', {
3689
+ providedIn: 'root',
3690
+ factory: () => new WeakMap(),
3691
+ });
3692
+ const PROXY_CLEANUP_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cleanup', {
3693
+ providedIn: 'root',
3694
+ factory: () => {
3695
+ const cache = inject(PROXY_CACHE_TOKEN);
3696
+ return new FinalizationRegistry(({ target, prop }) => {
3697
+ const store = cache.get(target);
3698
+ if (store)
3699
+ store.delete(prop);
3700
+ });
3701
+ },
3636
3702
  });
3637
3703
  /**
3638
3704
  * @internal
@@ -3649,11 +3715,11 @@ function isStore(value) {
3649
3715
  * holding it via a `WeakRef` and registering it for finalizer-driven cache pruning. The cache
3650
3716
  * is keyed per backing signal, so child identity is stable across repeat reads.
3651
3717
  */
3652
- function getCachedChild(target, prop, build) {
3653
- let storeCache = PROXY_CACHE.get(target);
3718
+ function getCachedChild(target, prop, build, cache, cleanupRegistry) {
3719
+ let storeCache = cache.get(target);
3654
3720
  if (!storeCache) {
3655
3721
  storeCache = new Map();
3656
- PROXY_CACHE.set(target, storeCache);
3722
+ cache.set(target, storeCache);
3657
3723
  }
3658
3724
  const cachedRef = storeCache.get(prop);
3659
3725
  if (cachedRef) {
@@ -3661,42 +3727,47 @@ function getCachedChild(target, prop, build) {
3661
3727
  if (cached)
3662
3728
  return cached;
3663
3729
  storeCache.delete(prop);
3664
- PROXY_CLEANUP.unregister(cachedRef);
3730
+ cleanupRegistry.unregister(cachedRef);
3665
3731
  }
3666
3732
  const proxy = build();
3667
3733
  const ref = new WeakRef(proxy);
3668
3734
  storeCache.set(prop, ref);
3669
- PROXY_CLEANUP.register(proxy, { target, prop }, ref);
3735
+ cleanupRegistry.register(proxy, { target, prop }, ref);
3670
3736
  return proxy;
3671
3737
  }
3738
+ /**
3739
+ * @internal Whether a mutable parent's child value must always re-notify: in-place mutation
3740
+ * keeps an object child's reference stable, so `Object.is` would swallow the change. Decided
3741
+ * per-VALUE (not snapshotted at build) so a union child that becomes an object later still
3742
+ * propagates parent-level mutations.
3743
+ */
3744
+ function mutableChildEqual(a, b) {
3745
+ if (typeof a === 'object' && a !== null)
3746
+ return false;
3747
+ return Object.is(a, b);
3748
+ }
3672
3749
  /**
3673
3750
  * @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
3674
- * A record parent reads the key directly; any other container goes through the fallback `from`/
3675
- * `onChange` path. Shared verbatim by the array and object proxies the only place a child node
3676
- * is constructed.
3751
+ * Both the read (`v?.[prop]`) and the write (`createFallbackOnChange` copies by the container's
3752
+ * LIVE shape) are shape-adaptive, so a child cached before an array↔record↔null union flip stays
3753
+ * correct after it. The only place a child node is constructed — shared by every container kind.
3677
3754
  */
3678
- function buildChildNode(target, prop, isMutableSource, injector, vivify, noUnionLeaves) {
3755
+ function buildChildNode(target, prop, isMutableSource, options) {
3679
3756
  const value = untracked(target);
3680
- const valueIsRecord = isRecord(value);
3681
- const valueIsArray = Array.isArray(value);
3682
- const nodeVivify = resolveVivify(value, vivify);
3757
+ const nodeVivify = resolveVivify(value, options.vivify);
3683
3758
  const vivifyFn = createVivify(nodeVivify);
3684
- const equalFn = (valueIsRecord || valueIsArray) &&
3685
- isMutableSource &&
3686
- typeof value[prop] === 'object'
3687
- ? () => false
3759
+ const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
3760
+ ? mutableChildEqual
3688
3761
  : undefined;
3689
- const computation = valueIsRecord
3690
- ? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
3691
- : derived(target, {
3692
- from: (v) => v?.[prop],
3693
- onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
3694
- equal: equalFn,
3695
- });
3762
+ const computation = derived(target, {
3763
+ from: (v) => v?.[prop],
3764
+ onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
3765
+ equal: equalFn,
3766
+ });
3696
3767
  const childSample = untracked(computation);
3697
- const childVivify = resolveVivify(childSample, vivify);
3698
- const proxy = toStore(computation, injector, childVivify, noUnionLeaves);
3699
- markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
3768
+ const childVivify = resolveVivify(childSample, options.vivify);
3769
+ const proxy = toStore(computation, options);
3770
+ markAsLeaf(proxy, computation, childVivify !== false, options.noUnionLeaves);
3700
3771
  return proxy;
3701
3772
  }
3702
3773
  /**
@@ -3714,7 +3785,7 @@ function buildChildNode(target, prop, isMutableSource, injector, vivify, noUnion
3714
3785
  * const state = store({ user: { name: 'John' } });
3715
3786
  * const nameSignal = state.user.name; // WritableSignal<string>
3716
3787
  */
3717
- function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3788
+ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...rest } = {}) {
3718
3789
  if (isStore(source))
3719
3790
  return source;
3720
3791
  if (!injector)
@@ -3734,6 +3805,16 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3734
3805
  return 'record';
3735
3806
  return 'primitive';
3736
3807
  }, ...(ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []));
3808
+ const STORE_OPTIONS = {
3809
+ injector,
3810
+ vivify,
3811
+ noUnionLeaves,
3812
+ [STORE_SHARED_GLOBALS]: {
3813
+ cache: rest[STORE_SHARED_GLOBALS]?.cache ?? injector.get(PROXY_CACHE_TOKEN),
3814
+ registry: rest[STORE_SHARED_GLOBALS]?.registry ??
3815
+ injector.get(PROXY_CLEANUP_TOKEN),
3816
+ },
3817
+ };
3737
3818
  // built lazily so non-array nodes never allocate it
3738
3819
  let length;
3739
3820
  const arrayLength = () => (length ??= computed(() => {
@@ -3787,21 +3868,23 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3787
3868
  return { enumerable: true, configurable: true };
3788
3869
  },
3789
3870
  get(target, prop, receiver) {
3790
- if (prop === IS_STORE)
3791
- return true;
3792
- if (prop === STORE_KIND)
3793
- return isMutableSource
3794
- ? 'mutable'
3795
- : isWritableSource
3796
- ? 'writable'
3797
- : 'readonly';
3798
- if (prop === STORE_INJECTOR)
3799
- return injector;
3871
+ if (typeof prop === 'symbol') {
3872
+ if (prop === IS_STORE)
3873
+ return true;
3874
+ if (prop === STORE_KIND)
3875
+ return isMutableSource
3876
+ ? 'mutable'
3877
+ : isWritableSource
3878
+ ? 'writable'
3879
+ : 'readonly';
3880
+ if (prop === STORE_SHARED_OPTIONS)
3881
+ return STORE_OPTIONS;
3882
+ }
3800
3883
  if (prop === 'asReadonlyStore')
3801
3884
  return () => {
3802
3885
  if (!isWritableSource)
3803
3886
  return s;
3804
- return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
3887
+ return untracked(() => toStore(source.asReadonly(), { injector, vivify, noUnionLeaves }));
3805
3888
  };
3806
3889
  const k = untracked(kind);
3807
3890
  if (prop === 'extend' && k !== 'array')
@@ -3809,7 +3892,7 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3809
3892
  ? 'mutable'
3810
3893
  : isWritableSource
3811
3894
  ? 'writable'
3812
- : 'readonly', injector);
3895
+ : 'readonly', STORE_OPTIONS);
3813
3896
  if (k === 'array') {
3814
3897
  if (prop === 'length')
3815
3898
  return arrayLength();
@@ -3826,7 +3909,7 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3826
3909
  return target[prop];
3827
3910
  if (k === 'array' && !isIndexProp(prop))
3828
3911
  return Reflect.get(target, prop, receiver);
3829
- return getCachedChild(target, prop, () => buildChildNode(target, k === 'array' ? +prop : prop, isMutableSource, injector, vivify, noUnionLeaves));
3912
+ return getCachedChild(target, prop, () => buildChildNode(target, k === 'array' ? +prop : prop, isMutableSource, STORE_OPTIONS), STORE_OPTIONS[STORE_SHARED_GLOBALS].cache, STORE_OPTIONS[STORE_SHARED_GLOBALS].registry);
3830
3913
  },
3831
3914
  });
3832
3915
  return s;
@@ -3840,14 +3923,14 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3840
3923
  * identity + two-way), while local keys never propagate upward. A merged `computed` is derived
3841
3924
  * only for whole-object reads / `has` / iteration — never for routing.
3842
3925
  */
3843
- function scopedStore(parent, seed, kind, injector) {
3926
+ function scopedStore(parent, seed, kind, options) {
3844
3927
  const local = isSignal(seed)
3845
- ? toStore(seed, injector)
3928
+ ? toStore(seed, options)
3846
3929
  : kind === 'mutable'
3847
- ? mutableStore(seed, { injector })
3930
+ ? mutableStore(seed, options)
3848
3931
  : kind === 'readonly'
3849
- ? store(seed, { injector }).asReadonlyStore()
3850
- : store(seed, { injector });
3932
+ ? store(seed, options).asReadonlyStore()
3933
+ : store(seed, options);
3851
3934
  const localValue = () => untracked(local);
3852
3935
  const parentValue = () => untracked(parent);
3853
3936
  const view = computed(() => ({
@@ -3878,18 +3961,20 @@ function scopedStore(parent, seed, kind, injector) {
3878
3961
  }
3879
3962
  const scope = new Proxy(base, {
3880
3963
  get(target, prop) {
3881
- if (prop === IS_STORE)
3882
- return true;
3883
- if (prop === STORE_KIND)
3884
- return kind;
3885
- if (prop === STORE_INJECTOR)
3886
- return injector;
3887
- if (prop === SCOPE_PARENT)
3888
- return parent;
3964
+ if (typeof prop === 'symbol') {
3965
+ if (prop === IS_STORE)
3966
+ return true;
3967
+ if (prop === STORE_KIND)
3968
+ return kind;
3969
+ if (prop === SCOPE_PARENT)
3970
+ return parent;
3971
+ if (prop === STORE_SHARED_OPTIONS)
3972
+ return options;
3973
+ }
3889
3974
  if (prop === 'extend')
3890
- return (childSeed) => scopedStore(scope, childSeed, kind, injector);
3975
+ return (childSeed) => scopedStore(scope, childSeed, kind, options);
3891
3976
  if (prop === 'asReadonlyStore')
3892
- return () => toStore(computed(() => ({ ...parent(), ...local() })), injector);
3977
+ return () => toStore(computed(() => ({ ...parent(), ...local() })), options);
3893
3978
  if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
3894
3979
  return target[prop];
3895
3980
  // Route by consulting both signals: local first, then parent, else local (new → local).
@@ -3935,22 +4020,34 @@ function storeKind(s) {
3935
4020
  * scoped.count.set(1); // writes through to base
3936
4021
  * scoped.label.set('x'); // stays local
3937
4022
  */
3938
- function extendStore(store, source, injector) {
3939
- return scopedStore(store, source, storeKind(store), injector ?? store[STORE_INJECTOR] ?? inject(Injector));
4023
+ function extendStore(store, source, options) {
4024
+ const opt = {
4025
+ ...store[STORE_SHARED_OPTIONS],
4026
+ ...options,
4027
+ };
4028
+ return scopedStore(store, source, storeKind(store), opt);
3940
4029
  }
3941
4030
  /**
3942
4031
  * Creates a WritableSignalStore from a value.
3943
4032
  * @see {@link toStore}
3944
4033
  */
3945
4034
  function store(value, opt) {
3946
- return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
4035
+ return toStore(signal(value, opt), {
4036
+ vivify: false,
4037
+ noUnionLeaves: false,
4038
+ ...opt,
4039
+ });
3947
4040
  }
3948
4041
  /**
3949
4042
  * Creates a MutableSignalStore from a value.
3950
4043
  * @see {@link toStore}
3951
4044
  */
3952
4045
  function mutableStore(value, opt) {
3953
- return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
4046
+ return toStore(mutable(value, opt), {
4047
+ vivify: false,
4048
+ noUnionLeaves: false,
4049
+ ...opt,
4050
+ });
3954
4051
  }
3955
4052
 
3956
4053
  function isPlainRecord(value) {
@@ -4012,7 +4109,13 @@ function forkStore(base, opt) {
4012
4109
  const merge = reconcile;
4013
4110
  const staged = linkedSignal({ ...(ngDevMode ? { debugName: "staged" } : /* istanbul ignore next */ {}), source: () => base(),
4014
4111
  computation: (theirs, prev) => prev === undefined ? theirs : merge(prev.source, prev.value, theirs) });
4015
- const store = toStore(staged, opt?.injector, opt?.vivify, opt?.noUnionLeaves);
4112
+ // Inherit the base's shared options (injector, vivify, noUnionLeaves + the
4113
+ // proxy cache/registry), same as extendStore — a fork should vivify like its
4114
+ // base and share its injector-scoped cache. `opt` overrides (advanced use).
4115
+ const store = toStore(staged, {
4116
+ ...base[STORE_SHARED_OPTIONS],
4117
+ ...opt,
4118
+ });
4016
4119
  return {
4017
4120
  store,
4018
4121
  commit: () => base.set(untracked(staged)),
@@ -4545,5 +4648,5 @@ function withHistory(sourceOrValue, opt) {
4545
4648
  * Generated bundle index. Do not edit.
4546
4649
  */
4547
4650
 
4548
- export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
4651
+ export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
4549
4652
  //# sourceMappingURL=mmstack-primitives.mjs.map