@mmstack/primitives 19.5.2 → 19.7.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.
@@ -634,6 +634,38 @@ function provideForwardingTransitionScope() {
634
634
  function getTransitionScope(injector) {
635
635
  return injector.get(TRANSITION_SCOPE, null);
636
636
  }
637
+ /**
638
+ * @internal Transaction-attributed pending for `startTransition`/`startTransaction`: like
639
+ * `scope.pending`, but loads already in flight when the tracker is created are NOT attributed —
640
+ * a pre-existing background load can neither settle the transaction early nor block its settle
641
+ * forever. A pre-existing flight is excluded only until it first settles; a later re-trigger of
642
+ * the same resource (e.g. the transaction's write changed its request) counts as the
643
+ * transaction's own work.
644
+ */
645
+ function createAttributedPending(scope) {
646
+ const isInFlight = (ref) => {
647
+ const s = untracked(ref.status);
648
+ return s === ResourceStatus.Loading || s === ResourceStatus.Reloading;
649
+ };
650
+ const preexisting = new Set(untracked(scope.resources).filter(isInFlight));
651
+ return computed(() => {
652
+ let pending = false;
653
+ for (const ref of scope.resources()) {
654
+ const s = ref.status();
655
+ const loading = s === ResourceStatus.Loading || s === ResourceStatus.Reloading;
656
+ if (preexisting.has(ref)) {
657
+ // deletes are monotonic, so this stays sound under re-computation
658
+ if (loading)
659
+ continue;
660
+ preexisting.delete(ref);
661
+ continue;
662
+ }
663
+ if (loading)
664
+ pending = true;
665
+ }
666
+ return pending;
667
+ });
668
+ }
637
669
  /**
638
670
  * Returns a register function bound to the nearest transition scope: it adds a resource
639
671
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
@@ -671,38 +703,43 @@ function registerResource(res, opt) {
671
703
  function injectStartTransition() {
672
704
  const scope = injectTransitionScope();
673
705
  const injector = inject(Injector);
706
+ const destroyRef = inject(DestroyRef);
674
707
  const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
675
708
  return (fn) => {
709
+ // attributed: loads already in flight when the transition starts are not ours —
710
+ // they can neither settle this transition early nor block it forever
711
+ const pending = createAttributedPending(scope);
676
712
  untracked(fn);
677
713
  let sawPending = false;
678
714
  const done = new Promise((resolve) => {
715
+ const settle = () => {
716
+ releaseDestroy();
717
+ watcher.destroy();
718
+ resolve();
719
+ };
679
720
  const watcher = effect(() => {
680
- const p = scope.pending();
721
+ const p = pending();
681
722
  if (p)
682
723
  sawPending = true;
683
724
  // settle: requests went in flight and then drained
684
- if (sawPending && !p) {
685
- watcher.destroy();
686
- resolve();
687
- }
725
+ if (sawPending && !p)
726
+ settle();
688
727
  }, { injector });
728
+ // a destroy mid-flight kills the watcher — resolve so awaiters never hang
729
+ const releaseDestroy = destroyRef.onDestroy(settle);
689
730
  if (onServer) {
690
- if (!untracked(scope.pending)) {
691
- watcher.destroy();
692
- resolve();
693
- }
731
+ if (!untracked(pending))
732
+ settle();
694
733
  return;
695
734
  }
696
735
  // no-async fallback: once the reactive system has processed the writes (afterNextRender),
697
736
  // if nothing ever went in flight, the transition is already complete.
698
737
  afterNextRender(() => {
699
- if (!sawPending && !untracked(scope.pending)) {
700
- watcher.destroy();
701
- resolve();
702
- }
738
+ if (!sawPending && !untracked(pending))
739
+ settle();
703
740
  }, { injector });
704
741
  });
705
- return { pending: scope.pending, done };
742
+ return { pending, done };
706
743
  };
707
744
  }
708
745
 
@@ -773,8 +810,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.19", ngImpo
773
810
  }] });
774
811
  /**
775
812
  * Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
776
- * the resources to coordinate are registered *above* the boundary (e.g. an app-builder page whose
777
- * manifests/connectors register at a higher injector), so the boundary observes that outer scope
813
+ * the resources to coordinate are registered *above* the boundary so the boundary observes that outer scope
778
814
  * rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
779
815
  * ancestor.
780
816
  */
@@ -839,9 +875,13 @@ function runInTransaction(txn, fn) {
839
875
  function injectStartTransaction() {
840
876
  const scope = injectTransitionScope();
841
877
  const injector = inject(Injector);
878
+ const destroyRef = inject(DestroyRef);
842
879
  const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
843
880
  return (fn) => {
844
881
  const txn = createTransaction();
882
+ // attributed: loads already in flight when the transaction starts are not ours —
883
+ // they can neither commit this transaction early nor block its settle forever
884
+ const pending = createAttributedPending(scope);
845
885
  // Hold BEFORE the writes, so the display freezes at pre-transaction values.
846
886
  scope.beginHold();
847
887
  let finished = false;
@@ -858,6 +898,7 @@ function injectStartTransaction() {
858
898
  if (finished)
859
899
  return;
860
900
  finished = true;
901
+ releaseDestroy();
861
902
  watcher?.destroy();
862
903
  if (restore)
863
904
  txn.restore();
@@ -866,6 +907,10 @@ function injectStartTransaction() {
866
907
  scope.endHold();
867
908
  resolveDone();
868
909
  };
910
+ // The scope may outlive the calling context (a component transacting on an ancestor
911
+ // boundary): a destroy mid-flight kills the settle watcher, so without this the hold
912
+ // would leak and freeze the surviving scope forever. Keep the writes — they landed live.
913
+ const releaseDestroy = destroyRef.onDestroy(() => finish(false));
869
914
  try {
870
915
  runInTransaction(txn, fn);
871
916
  }
@@ -875,25 +920,25 @@ function injectStartTransaction() {
875
920
  }
876
921
  let sawPending = false;
877
922
  watcher = effect(() => {
878
- const p = scope.pending();
923
+ const p = pending();
879
924
  if (p)
880
925
  sawPending = true;
881
926
  if (sawPending && !p)
882
927
  finish(false);
883
928
  }, { injector });
884
929
  if (onServer) {
885
- if (!untracked(scope.pending))
930
+ if (!untracked(pending))
886
931
  finish(false);
887
932
  }
888
933
  else {
889
934
  // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
890
935
  afterNextRender(() => {
891
- if (!sawPending && !untracked(scope.pending))
936
+ if (!sawPending && !untracked(pending))
892
937
  finish(false);
893
938
  }, { injector });
894
939
  }
895
940
  return {
896
- pending: scope.pending,
941
+ pending,
897
942
  done,
898
943
  abort: () => finish(true),
899
944
  };
@@ -2905,13 +2950,21 @@ const IDLE = {
2905
2950
  pointerId: null,
2906
2951
  modifiers: { shift: false, alt: false, ctrl: false, meta: false },
2907
2952
  button: -1,
2953
+ pointerType: '',
2954
+ origin: null,
2955
+ cancelled: false,
2908
2956
  };
2957
+ /** Terminal state of an aborted gesture — same idle shape, `cancelled: true`. */
2958
+ const CANCELLED = { ...IDLE, cancelled: true };
2909
2959
  function stateEqual(a, b) {
2910
2960
  return (a.active === b.active &&
2961
+ a.cancelled === b.cancelled &&
2911
2962
  a.pointerId === b.pointerId &&
2912
2963
  a.current.x === b.current.x &&
2913
2964
  a.current.y === b.current.y &&
2914
2965
  a.button === b.button &&
2966
+ a.pointerType === b.pointerType &&
2967
+ a.origin === b.origin &&
2915
2968
  a.modifiers.shift === b.modifiers.shift &&
2916
2969
  a.modifiers.alt === b.modifiers.alt &&
2917
2970
  a.modifiers.ctrl === b.modifiers.ctrl &&
@@ -2945,7 +2998,7 @@ function createPointerDrag(opt) {
2945
2998
  return base;
2946
2999
  }
2947
3000
  const hostRef = inject((ElementRef), { optional: true });
2948
- const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], debugName = 'pointerDrag', } = opt ?? {};
3001
+ const { target = hostRef?.nativeElement, coordinateSpace = 'client', activationThreshold = 3, throttle = 16, handleSelector, buttons = [0], stopPropagation = false, debugName = 'pointerDrag', } = opt ?? {};
2949
3002
  const resolve = (t) => {
2950
3003
  if (!t)
2951
3004
  return null;
@@ -2966,9 +3019,12 @@ function createPointerDrag(opt) {
2966
3019
  equal: stateEqual,
2967
3020
  debugName,
2968
3021
  });
3022
+ const threshold2 = activationThreshold * activationThreshold;
2969
3023
  let startPoint = { x: 0, y: 0 };
2970
3024
  let activePointerId = null;
2971
3025
  let activeButton = -1;
3026
+ let activePointerType = '';
3027
+ let activeOrigin = null;
2972
3028
  let activated = false;
2973
3029
  let gesture = null;
2974
3030
  const coord = (e) => coordinateSpace === 'page'
@@ -2980,22 +3036,24 @@ function createPointerDrag(opt) {
2980
3036
  ctrl: e.ctrlKey,
2981
3037
  meta: e.metaKey,
2982
3038
  });
2983
- const end = () => {
3039
+ const end = (cancelled = false) => {
2984
3040
  gesture?.abort();
2985
3041
  gesture = null;
2986
3042
  activePointerId = null;
2987
3043
  activeButton = -1;
3044
+ activePointerType = '';
3045
+ activeOrigin = null;
2988
3046
  activated = false;
2989
- state.set(IDLE);
2990
- state.flush(); // terminal transition: reflect IDLE now, not on the trailing edge
3047
+ state.set(cancelled ? CANCELLED : IDLE);
3048
+ state.flush(); // terminal transition: reflect idle now, not on the trailing edge
2991
3049
  };
2992
3050
  const onMove = (e) => {
2993
3051
  if (e.pointerId !== activePointerId)
2994
3052
  return;
2995
3053
  const current = coord(e);
2996
3054
  const delta = { x: current.x - startPoint.x, y: current.y - startPoint.y };
2997
- if (!activated && Math.hypot(delta.x, delta.y) >= activationThreshold) {
2998
- activated = true;
3055
+ if (!activated && delta.x * delta.x + delta.y * delta.y >= threshold2) {
3056
+ activated = true; // squared compare — no sqrt on the pre-activation path
2999
3057
  }
3000
3058
  state.set({
3001
3059
  active: activated,
@@ -3005,6 +3063,9 @@ function createPointerDrag(opt) {
3005
3063
  pointerId: activePointerId,
3006
3064
  modifiers: mods(e),
3007
3065
  button: activeButton, // pointermove button is -1; keep the down-button
3066
+ pointerType: activePointerType,
3067
+ origin: activeOrigin,
3068
+ cancelled: false,
3008
3069
  });
3009
3070
  };
3010
3071
  const onUp = (e) => {
@@ -3013,22 +3074,28 @@ function createPointerDrag(opt) {
3013
3074
  };
3014
3075
  const onCancel = (e) => {
3015
3076
  if (e.pointerId === activePointerId)
3016
- end();
3077
+ end(true);
3017
3078
  };
3018
3079
  const onKey = (e) => {
3019
3080
  if (e.key === 'Escape' && activePointerId !== null)
3020
- end();
3081
+ end(true);
3021
3082
  };
3022
3083
  const onDown = (el) => (e) => {
3023
3084
  if (activePointerId !== null)
3024
3085
  return;
3025
3086
  if (!buttons.includes(e.button))
3026
3087
  return;
3027
- if (handleSelector && !e.target?.closest?.(handleSelector)) {
3028
- return;
3029
- }
3088
+ const matched = handleSelector
3089
+ ? e.target?.closest?.(handleSelector)
3090
+ : el;
3091
+ if (!matched)
3092
+ return; // handleSelector set but pointerdown landed outside a handle
3093
+ if (stopPropagation)
3094
+ e.stopPropagation(); // claim it: an outer sensor won't also start
3030
3095
  activePointerId = e.pointerId;
3031
3096
  activeButton = e.button;
3097
+ activePointerType = e.pointerType;
3098
+ activeOrigin = matched;
3032
3099
  activated = false;
3033
3100
  startPoint = coord(e);
3034
3101
  try {
@@ -3054,6 +3121,9 @@ function createPointerDrag(opt) {
3054
3121
  pointerId: e.pointerId,
3055
3122
  modifiers: mods(e),
3056
3123
  button: e.button,
3124
+ pointerType: activePointerType,
3125
+ origin: activeOrigin,
3126
+ cancelled: false,
3057
3127
  });
3058
3128
  };
3059
3129
  const attach = (el) => {
@@ -3063,7 +3133,7 @@ function createPointerDrag(opt) {
3063
3133
  });
3064
3134
  return () => {
3065
3135
  controller.abort();
3066
- end();
3136
+ end(true); // teardown mid-gesture is an abort, not a drop
3067
3137
  };
3068
3138
  };
3069
3139
  if (isSignal(target)) {
@@ -3081,7 +3151,7 @@ function createPointerDrag(opt) {
3081
3151
  }
3082
3152
  const base = state.asReadonly();
3083
3153
  base.unthrottled = state.original;
3084
- base.cancel = end;
3154
+ base.cancel = () => end(true);
3085
3155
  return base;
3086
3156
  }
3087
3157
 
@@ -3432,17 +3502,14 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
3432
3502
 
3433
3503
  const IS_STORE = Symbol('@mmstack/primitives::store/IS_STORE');
3434
3504
  const SCOPE_PARENT = Symbol('@mmstack/primitives::store/SCOPE_PARENT');
3505
+ const STORE_SHARED_GLOBALS = Symbol('@mmstack/primitives::store/STORE_SHARED_GLOBALS');
3506
+ const STORE_SHARED_OPTIONS = Symbol('@mmstack/primitives::store/STORE_SHARED_OPTIONS');
3435
3507
  /**
3436
3508
  * @internal Brand carrying a store's writability ('mutable' | 'writable' | 'readonly'), stamped
3437
3509
  * on every store proxy. Read by `extendStore` instead of re-deriving via `isWritableSignal`,
3438
3510
  * which would mis-classify a readonly scoped store (its backing `toWritable` still has a `set`).
3439
3511
  */
3440
3512
  const STORE_KIND = Symbol('@mmstack/primitives::store/STORE_KIND');
3441
- /**
3442
- * @internal Brand exposing the injector a store was built with, so `extendStore` inherits it the
3443
- * same way `store.extend(...)` does (via closure) — no injection context needed at the call site.
3444
- */
3445
- const STORE_INJECTOR = Symbol('@mmstack/primitives::store/STORE_INJECTOR');
3446
3513
  const SIGNAL_FN_PROP = new Set([
3447
3514
  'set',
3448
3515
  'update',
@@ -3450,21 +3517,20 @@ const SIGNAL_FN_PROP = new Set([
3450
3517
  'inline',
3451
3518
  'asReadonly',
3452
3519
  ]);
3453
- /**
3454
- * @internal
3455
- * Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
3456
- * Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
3457
- */
3458
- const PROXY_CACHE = new WeakMap();
3459
- /**
3460
- * @internal
3461
- * Test-only handle on the finalization registry (deliberately NOT re-exported from the public
3462
- * barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
3463
- */
3464
- const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
3465
- const storeCache = PROXY_CACHE.get(target);
3466
- if (storeCache)
3467
- storeCache.delete(prop);
3520
+ const PROXY_CACHE_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cache', {
3521
+ providedIn: 'root',
3522
+ factory: () => new WeakMap(),
3523
+ });
3524
+ const PROXY_CLEANUP_TOKEN = new InjectionToken('@mmstack/primitives:store-proxy-cleanup', {
3525
+ providedIn: 'root',
3526
+ factory: () => {
3527
+ const cache = inject(PROXY_CACHE_TOKEN);
3528
+ return new FinalizationRegistry(({ target, prop }) => {
3529
+ const store = cache.get(target);
3530
+ if (store)
3531
+ store.delete(prop);
3532
+ });
3533
+ },
3468
3534
  });
3469
3535
  /**
3470
3536
  * @internal
@@ -3658,11 +3724,11 @@ function isLeaf(value) {
3658
3724
  * holding it via a `WeakRef` and registering it for finalizer-driven cache pruning. The cache
3659
3725
  * is keyed per backing signal, so child identity is stable across repeat reads.
3660
3726
  */
3661
- function getCachedChild(target, prop, build) {
3662
- let storeCache = PROXY_CACHE.get(target);
3727
+ function getCachedChild(target, prop, build, cache, cleanupRegistry) {
3728
+ let storeCache = cache.get(target);
3663
3729
  if (!storeCache) {
3664
3730
  storeCache = new Map();
3665
- PROXY_CACHE.set(target, storeCache);
3731
+ cache.set(target, storeCache);
3666
3732
  }
3667
3733
  const cachedRef = storeCache.get(prop);
3668
3734
  if (cachedRef) {
@@ -3670,42 +3736,47 @@ function getCachedChild(target, prop, build) {
3670
3736
  if (cached)
3671
3737
  return cached;
3672
3738
  storeCache.delete(prop);
3673
- PROXY_CLEANUP.unregister(cachedRef);
3739
+ cleanupRegistry.unregister(cachedRef);
3674
3740
  }
3675
3741
  const proxy = build();
3676
3742
  const ref = new WeakRef(proxy);
3677
3743
  storeCache.set(prop, ref);
3678
- PROXY_CLEANUP.register(proxy, { target, prop }, ref);
3744
+ cleanupRegistry.register(proxy, { target, prop }, ref);
3679
3745
  return proxy;
3680
3746
  }
3747
+ /**
3748
+ * @internal Whether a mutable parent's child value must always re-notify: in-place mutation
3749
+ * keeps an object child's reference stable, so `Object.is` would swallow the change. Decided
3750
+ * per-VALUE (not snapshotted at build) so a union child that becomes an object later still
3751
+ * propagates parent-level mutations.
3752
+ */
3753
+ function mutableChildEqual(a, b) {
3754
+ if (typeof a === 'object' && a !== null)
3755
+ return false;
3756
+ return Object.is(a, b);
3757
+ }
3681
3758
  /**
3682
3759
  * @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
3683
- * A record parent reads the key directly; any other container goes through the fallback `from`/
3684
- * `onChange` path. Shared verbatim by the array and object proxies the only place a child node
3685
- * is constructed.
3760
+ * Both the read (`v?.[prop]`) and the write (`createFallbackOnChange` copies by the container's
3761
+ * LIVE shape) are shape-adaptive, so a child cached before an array↔record↔null union flip stays
3762
+ * correct after it. The only place a child node is constructed — shared by every container kind.
3686
3763
  */
3687
- function buildChildNode(target, prop, isMutableSource, injector, vivify, noUnionLeaves) {
3764
+ function buildChildNode(target, prop, isMutableSource, options) {
3688
3765
  const value = untracked(target);
3689
- const valueIsRecord = isRecord(value);
3690
- const valueIsArray = Array.isArray(value);
3691
- const nodeVivify = resolveVivify(value, vivify);
3766
+ const nodeVivify = resolveVivify(value, options.vivify);
3692
3767
  const vivifyFn = createVivify(nodeVivify);
3693
- const equalFn = (valueIsRecord || valueIsArray) &&
3694
- isMutableSource &&
3695
- typeof value[prop] === 'object'
3696
- ? () => false
3768
+ const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
3769
+ ? mutableChildEqual
3697
3770
  : undefined;
3698
- const computation = valueIsRecord
3699
- ? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
3700
- : derived(target, {
3701
- from: (v) => v?.[prop],
3702
- onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
3703
- equal: equalFn,
3704
- });
3771
+ const computation = derived(target, {
3772
+ from: (v) => v?.[prop],
3773
+ onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
3774
+ equal: equalFn,
3775
+ });
3705
3776
  const childSample = untracked(computation);
3706
- const childVivify = resolveVivify(childSample, vivify);
3707
- const proxy = toStore(computation, injector, childVivify, noUnionLeaves);
3708
- markAsLeaf(proxy, computation, childVivify !== false, noUnionLeaves);
3777
+ const childVivify = resolveVivify(childSample, options.vivify);
3778
+ const proxy = toStore(computation, options);
3779
+ markAsLeaf(proxy, computation, childVivify !== false, options.noUnionLeaves);
3709
3780
  return proxy;
3710
3781
  }
3711
3782
  /**
@@ -3723,7 +3794,7 @@ function buildChildNode(target, prop, isMutableSource, injector, vivify, noUnion
3723
3794
  * const state = store({ user: { name: 'John' } });
3724
3795
  * const nameSignal = state.user.name; // WritableSignal<string>
3725
3796
  */
3726
- function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3797
+ function toStore(source, { injector, vivify = false, noUnionLeaves = false, ...rest } = {}) {
3727
3798
  if (isStore(source))
3728
3799
  return source;
3729
3800
  if (!injector)
@@ -3743,6 +3814,16 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3743
3814
  return 'record';
3744
3815
  return 'primitive';
3745
3816
  });
3817
+ const STORE_OPTIONS = {
3818
+ injector,
3819
+ vivify,
3820
+ noUnionLeaves,
3821
+ [STORE_SHARED_GLOBALS]: {
3822
+ cache: rest[STORE_SHARED_GLOBALS]?.cache ?? injector.get(PROXY_CACHE_TOKEN),
3823
+ registry: rest[STORE_SHARED_GLOBALS]?.registry ??
3824
+ injector.get(PROXY_CLEANUP_TOKEN),
3825
+ },
3826
+ };
3746
3827
  // built lazily so non-array nodes never allocate it
3747
3828
  let length;
3748
3829
  const arrayLength = () => (length ??= computed(() => {
@@ -3796,21 +3877,23 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3796
3877
  return { enumerable: true, configurable: true };
3797
3878
  },
3798
3879
  get(target, prop, receiver) {
3799
- if (prop === IS_STORE)
3800
- return true;
3801
- if (prop === STORE_KIND)
3802
- return isMutableSource
3803
- ? 'mutable'
3804
- : isWritableSource
3805
- ? 'writable'
3806
- : 'readonly';
3807
- if (prop === STORE_INJECTOR)
3808
- return injector;
3880
+ if (typeof prop === 'symbol') {
3881
+ if (prop === IS_STORE)
3882
+ return true;
3883
+ if (prop === STORE_KIND)
3884
+ return isMutableSource
3885
+ ? 'mutable'
3886
+ : isWritableSource
3887
+ ? 'writable'
3888
+ : 'readonly';
3889
+ if (prop === STORE_SHARED_OPTIONS)
3890
+ return STORE_OPTIONS;
3891
+ }
3809
3892
  if (prop === 'asReadonlyStore')
3810
3893
  return () => {
3811
3894
  if (!isWritableSource)
3812
3895
  return s;
3813
- return untracked(() => toStore(source.asReadonly(), injector, vivify, noUnionLeaves));
3896
+ return untracked(() => toStore(source.asReadonly(), { injector, vivify, noUnionLeaves }));
3814
3897
  };
3815
3898
  const k = untracked(kind);
3816
3899
  if (prop === 'extend' && k !== 'array')
@@ -3818,7 +3901,7 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3818
3901
  ? 'mutable'
3819
3902
  : isWritableSource
3820
3903
  ? 'writable'
3821
- : 'readonly', injector);
3904
+ : 'readonly', STORE_OPTIONS);
3822
3905
  if (k === 'array') {
3823
3906
  if (prop === 'length')
3824
3907
  return arrayLength();
@@ -3835,7 +3918,7 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3835
3918
  return target[prop];
3836
3919
  if (k === 'array' && !isIndexProp(prop))
3837
3920
  return Reflect.get(target, prop, receiver);
3838
- return getCachedChild(target, prop, () => buildChildNode(target, k === 'array' ? +prop : prop, isMutableSource, injector, vivify, noUnionLeaves));
3921
+ 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);
3839
3922
  },
3840
3923
  });
3841
3924
  return s;
@@ -3849,14 +3932,14 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3849
3932
  * identity + two-way), while local keys never propagate upward. A merged `computed` is derived
3850
3933
  * only for whole-object reads / `has` / iteration — never for routing.
3851
3934
  */
3852
- function scopedStore(parent, seed, kind, injector) {
3935
+ function scopedStore(parent, seed, kind, options) {
3853
3936
  const local = isSignal(seed)
3854
- ? toStore(seed, injector)
3937
+ ? toStore(seed, options)
3855
3938
  : kind === 'mutable'
3856
- ? mutableStore(seed, { injector })
3939
+ ? mutableStore(seed, options)
3857
3940
  : kind === 'readonly'
3858
- ? store(seed, { injector }).asReadonlyStore()
3859
- : store(seed, { injector });
3941
+ ? store(seed, options).asReadonlyStore()
3942
+ : store(seed, options);
3860
3943
  const localValue = () => untracked(local);
3861
3944
  const parentValue = () => untracked(parent);
3862
3945
  const view = computed(() => ({
@@ -3887,18 +3970,20 @@ function scopedStore(parent, seed, kind, injector) {
3887
3970
  }
3888
3971
  const scope = new Proxy(base, {
3889
3972
  get(target, prop) {
3890
- if (prop === IS_STORE)
3891
- return true;
3892
- if (prop === STORE_KIND)
3893
- return kind;
3894
- if (prop === STORE_INJECTOR)
3895
- return injector;
3896
- if (prop === SCOPE_PARENT)
3897
- return parent;
3973
+ if (typeof prop === 'symbol') {
3974
+ if (prop === IS_STORE)
3975
+ return true;
3976
+ if (prop === STORE_KIND)
3977
+ return kind;
3978
+ if (prop === SCOPE_PARENT)
3979
+ return parent;
3980
+ if (prop === STORE_SHARED_OPTIONS)
3981
+ return options;
3982
+ }
3898
3983
  if (prop === 'extend')
3899
- return (childSeed) => scopedStore(scope, childSeed, kind, injector);
3984
+ return (childSeed) => scopedStore(scope, childSeed, kind, options);
3900
3985
  if (prop === 'asReadonlyStore')
3901
- return () => toStore(computed(() => ({ ...parent(), ...local() })), injector);
3986
+ return () => toStore(computed(() => ({ ...parent(), ...local() })), options);
3902
3987
  if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
3903
3988
  return target[prop];
3904
3989
  // Route by consulting both signals: local first, then parent, else local (new → local).
@@ -3944,22 +4029,34 @@ function storeKind(s) {
3944
4029
  * scoped.count.set(1); // writes through to base
3945
4030
  * scoped.label.set('x'); // stays local
3946
4031
  */
3947
- function extendStore(store, source, injector) {
3948
- return scopedStore(store, source, storeKind(store), injector ?? store[STORE_INJECTOR] ?? inject(Injector));
4032
+ function extendStore(store, source, options) {
4033
+ const opt = {
4034
+ ...store[STORE_SHARED_OPTIONS],
4035
+ ...options,
4036
+ };
4037
+ return scopedStore(store, source, storeKind(store), opt);
3949
4038
  }
3950
4039
  /**
3951
4040
  * Creates a WritableSignalStore from a value.
3952
4041
  * @see {@link toStore}
3953
4042
  */
3954
4043
  function store(value, opt) {
3955
- return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
4044
+ return toStore(signal(value, opt), {
4045
+ vivify: false,
4046
+ noUnionLeaves: false,
4047
+ ...opt,
4048
+ });
3956
4049
  }
3957
4050
  /**
3958
4051
  * Creates a MutableSignalStore from a value.
3959
4052
  * @see {@link toStore}
3960
4053
  */
3961
4054
  function mutableStore(value, opt) {
3962
- return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false, opt?.noUnionLeaves ?? false);
4055
+ return toStore(mutable(value, opt), {
4056
+ vivify: false,
4057
+ noUnionLeaves: false,
4058
+ ...opt,
4059
+ });
3963
4060
  }
3964
4061
 
3965
4062
  function isPlainRecord(value) {
@@ -4023,7 +4120,13 @@ function forkStore(base, opt) {
4023
4120
  source: () => base(),
4024
4121
  computation: (theirs, prev) => prev === undefined ? theirs : merge(prev.source, prev.value, theirs),
4025
4122
  });
4026
- const store = toStore(staged, opt?.injector, opt?.vivify, opt?.noUnionLeaves);
4123
+ // Inherit the base's shared options (injector, vivify, noUnionLeaves + the
4124
+ // proxy cache/registry), same as extendStore — a fork should vivify like its
4125
+ // base and share its injector-scoped cache. `opt` overrides (advanced use).
4126
+ const store = toStore(staged, {
4127
+ ...base[STORE_SHARED_OPTIONS],
4128
+ ...opt,
4129
+ });
4027
4130
  return {
4028
4131
  store,
4029
4132
  commit: () => base.set(untracked(staged)),
@@ -4558,5 +4661,5 @@ function withHistory(sourceOrValue, opt) {
4558
4661
  * Generated bundle index. Do not edit.
4559
4662
  */
4560
4663
 
4561
- 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 };
4664
+ 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 };
4562
4665
  //# sourceMappingURL=mmstack-primitives.mjs.map