@mmstack/primitives 21.1.1 → 21.2.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.
@@ -80,7 +80,7 @@ function popFrame() {
80
80
  * ]);
81
81
  *
82
82
  * // The fine-grained mapped list
83
- * const mappedUsers = mapArray(
83
+ * const mappedUsers = indexArray(
84
84
  * users,
85
85
  * (userSignal, index) => {
86
86
  * // 1. Create a fine-grained SIDE EFFECT for *this item*
@@ -101,7 +101,7 @@ function popFrame() {
101
101
  * };
102
102
  * },
103
103
  * {
104
- * // 3. Tell mapArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
104
+ * // 3. Tell indexArray HOW to clean up when an item is removed, this needs to be manual as it's not a nestedEffect itself
105
105
  * onDestroy: (mappedItem) => {
106
106
  * mappedItem.destroyEffect();
107
107
  * }
@@ -158,7 +158,7 @@ function nestedEffect(effectFn, options) {
158
158
  /**
159
159
  * Creates a new `Signal` that processes an array of items in time-sliced chunks. This is useful for handling large lists without blocking the main thread.
160
160
  *
161
- * The returned signal will initially contain the first `chunkSize` items from the source array. It will then schedule updates to include additional chunks of items based on the specified `duration`.
161
+ * The returned signal will initially contain the first `chunkSize` items from the source array. It will then schedule updates to include additional chunks of items based on the specified `delay`.
162
162
  *
163
163
  * @template T The type of items in the array.
164
164
  * @param source A `Signal` or a function that returns an array of items to be processed in chunks.
@@ -167,16 +167,23 @@ function nestedEffect(effectFn, options) {
167
167
  *
168
168
  * @example
169
169
  * const largeList = signal(Array.from({ length: 1000 }, (_, i) => i));
170
- * const chunkedList = chunked(largeList, { chunkSize: 100, duration: 100 });
170
+ * const chunkedList = chunked(largeList, { chunkSize: 100, delay: 100 });
171
171
  */
172
172
  function chunked(source, options) {
173
173
  const { chunkSize = 50, delay = 'frame', equal, injector } = options || {};
174
174
  let delayFn;
175
175
  if (delay === 'frame') {
176
- delayFn = (callback) => {
177
- const num = requestAnimationFrame(callback);
178
- return () => cancelAnimationFrame(num);
179
- };
176
+ delayFn =
177
+ typeof requestAnimationFrame === 'function'
178
+ ? (callback) => {
179
+ const num = requestAnimationFrame(callback);
180
+ return () => cancelAnimationFrame(num);
181
+ }
182
+ : // SSR: no requestAnimationFrame — approximate a frame with a timeout
183
+ (cb) => {
184
+ const num = setTimeout(cb, 16);
185
+ return () => clearTimeout(num);
186
+ };
180
187
  }
181
188
  else if (delay === 'microtask') {
182
189
  delayFn = (cb) => {
@@ -262,7 +269,9 @@ class MmActivity {
262
269
  });
263
270
  }
264
271
  for (const node of this.view.rootNodes) {
265
- if (node instanceof HTMLElement)
272
+ // covers HTML and SVG roots; text/comment roots can't be styled — their CD is still
273
+ // detached, but prefer an element root for true visual hiding
274
+ if (node instanceof HTMLElement || node instanceof SVGElement)
266
275
  node.style.display = visible ? '' : 'none';
267
276
  }
268
277
  if (visible)
@@ -341,14 +350,25 @@ function resolvePause(opt) {
341
350
  if (pause === false)
342
351
  return null;
343
352
  const run = (fn) => opt?.injector ? runInInjectionContext(opt.injector, fn) : fn();
353
+ // `inject` requires an injection context even with `optional: true`. A bare
354
+ // `pausableSignal(0)` (documented as "like `signal`") must degrade to the unwrapped
355
+ // primitive outside DI, not throw NG0203 — so injection failures fall back gracefully.
356
+ const tryRun = (fn, fallback) => {
357
+ try {
358
+ return run(fn);
359
+ }
360
+ catch {
361
+ return fallback;
362
+ }
363
+ };
344
364
  const onServer = () => typeof pause === 'function' && !opt?.injector
345
365
  ? typeof globalThis.window === 'undefined'
346
- : run(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'));
366
+ : tryRun(() => isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser'), typeof globalThis.window === 'undefined');
347
367
  if (typeof pause === 'function')
348
368
  return onServer() ? null : pause;
349
369
  if (onServer())
350
370
  return null;
351
- const paused = run(() => inject(PAUSED_CONTEXT, { optional: true }));
371
+ const paused = tryRun(() => inject(PAUSED_CONTEXT, { optional: true }), null);
352
372
  if (!paused) {
353
373
  if (explicit === true && isDevMode())
354
374
  console.warn('[pausable] `pause: true` but no PAUSED_CONTEXT in scope — not pausing. Provide one via an ' +
@@ -377,8 +397,9 @@ function pausableEffect(effectFn, options) {
377
397
  /**
378
398
  * Like `signal`, but pausable. While paused, READS hold the last value; writes still land on the
379
399
  * underlying signal and surface on resume. Built on the `keepPrevious`/`hold` shape — a
380
- * `linkedSignal` gated on the pause predicate, with `set`/`update`/`asReadonly` forwarded to the
381
- * source signal. With no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false`
400
+ * `linkedSignal` gated on the pause predicate, with `set`/`update` forwarded to the source signal.
401
+ * `asReadonly()` returns the held (gated) view, so both views of the signal agree while paused.
402
+ * With no `pause` option it defaults to the ambient `PAUSED_CONTEXT`; `pause: false`
382
403
  * makes it a plain `signal` — no `linkedSignal` is created.
383
404
  *
384
405
  * NOTE: while paused, `set(x)` followed by a read returns the *held* (pre-pause) value, not `x` — the
@@ -395,7 +416,8 @@ function pausableSignal(initialValue, options) {
395
416
  equal: options?.equal });
396
417
  read.set = src.set;
397
418
  read.update = src.update;
398
- read.asReadonly = src.asReadonly;
419
+ // NOTE: `asReadonly` deliberately stays the linkedSignal's own (the held view) — the
420
+ // source's readonly view would show live values while the signal itself shows held ones.
399
421
  return read;
400
422
  }
401
423
  /**
@@ -432,8 +454,12 @@ function mutable(initial, opt) {
432
454
  const internalUpdate = sig.update;
433
455
  sig.mutate = (updater) => {
434
456
  cnt++;
435
- internalUpdate(updater);
436
- cnt--;
457
+ try {
458
+ internalUpdate(updater);
459
+ }
460
+ finally {
461
+ cnt--;
462
+ }
437
463
  };
438
464
  sig.inline = (updater) => {
439
465
  sig.mutate((prev) => {
@@ -520,7 +546,7 @@ function createNoopScope() {
520
546
  hold: (value) => value,
521
547
  };
522
548
  }
523
- const TRANSITION_SCOPE = new InjectionToken('@mmstack/resource:transition-scope');
549
+ const TRANSITION_SCOPE = new InjectionToken('@mmstack/primitives:transition-scope');
524
550
  /** Provide a fresh transition scope at a boundary so its subtree's resources are tracked independently. */
525
551
  function provideTransitionScope() {
526
552
  return { provide: TRANSITION_SCOPE, useFactory: createTransitionScope };
@@ -529,12 +555,53 @@ function injectTransitionScope() {
529
555
  const scope = inject(TRANSITION_SCOPE, { optional: true });
530
556
  if (!scope) {
531
557
  if (isDevMode())
532
- console.warn('[mmstack/resource] No transition scope in context — registration/tracking here is a no-op. ' +
558
+ console.warn('[mmstack/primitives] No transition scope in context — registration/tracking here is a no-op. ' +
533
559
  'Use a <mm-suspense> boundary or provideTransitionScope() in an ancestor.');
534
560
  return createNoopScope();
535
561
  }
536
562
  return scope;
537
563
  }
564
+ function createForwardingScope() {
565
+ const own = createTransitionScope();
566
+ const target = signal(null, ...(ngDevMode ? [{ debugName: "target" }] : /* istanbul ignore next */ []));
567
+ const eff = () => target() ?? own;
568
+ const owners = new Map();
569
+ return {
570
+ setTarget: (t) => target.set(t),
571
+ resources: computed(() => eff().resources()),
572
+ pending: computed(() => eff().pending()),
573
+ suspended: (type) => eff().suspended(type),
574
+ add: (ref, opt) => {
575
+ const t = untracked(target) ?? own;
576
+ owners.set(ref, t);
577
+ t.add(ref, opt);
578
+ },
579
+ remove: (ref) => {
580
+ const t = owners.get(ref) ?? untracked(target) ?? own;
581
+ t.remove(ref);
582
+ owners.delete(ref);
583
+ },
584
+ commit: (value) => linkedSignal({
585
+ source: () => ({ v: value(), settled: !eff().pending() }),
586
+ computation: (curr, prev) => curr.settled || prev === undefined ? curr.v : prev.value,
587
+ }),
588
+ holding: computed(() => eff().holding()),
589
+ beginHold: () => (untracked(target) ?? own).beginHold(),
590
+ endHold: () => (untracked(target) ?? own).endHold(),
591
+ hold: (value) => linkedSignal({
592
+ source: () => ({ v: value(), held: eff().holding() }),
593
+ computation: (curr, prev) => prev !== undefined && curr.held ? prev.value : curr.v,
594
+ }),
595
+ };
596
+ }
597
+ /** Provide a forwarding transition scope at a boundary (used by the transition outlet). */
598
+ function provideForwardingTransitionScope() {
599
+ return { provide: TRANSITION_SCOPE, useFactory: createForwardingScope };
600
+ }
601
+ /** Read the transition scope reachable from `injector`, or null if none is provided there. */
602
+ function getTransitionScope(injector) {
603
+ return injector.get(TRANSITION_SCOPE, null);
604
+ }
538
605
  /**
539
606
  * Returns a register function bound to the nearest transition scope: it adds a resource
540
607
  * to the scope and removes it when the caller's injection context is destroyed. Pass any
@@ -563,6 +630,11 @@ function registerResource(res, opt) {
563
630
  *
564
631
  * Must be called in an injection context. This is the *async* generalization (Tier 2): it adds
565
632
  * no rendering cost and needs no fork — holding direct/sync readers is a separate, deferred tier.
633
+ *
634
+ * Caveat: work must go in flight by the first post-write render to be awaited. A loader that
635
+ * starts later (a debounced request signal, a chained/deferred resource) is not attributable to
636
+ * this transition — the no-async fallback will have already resolved `done`. Trigger such work
637
+ * eagerly inside `fn`, or coordinate it separately.
566
638
  */
567
639
  function injectStartTransition() {
568
640
  const scope = injectTransitionScope();
@@ -712,6 +784,11 @@ function runInTransaction(txn, fn) {
712
784
  * The writes land on LIVE state immediately (so derived variables and connector requests see the
713
785
  * new values and refetch); only the *display* is held, via `scope.hold`. Must run in an injection
714
786
  * context.
787
+ *
788
+ * Caveat: work must go in flight by the first post-write render to be part of the transaction. A
789
+ * loader that starts later (a debounced request signal, a chained/deferred resource) is not
790
+ * attributable to it — the no-async fallback will have already committed and released the hold,
791
+ * after which `abort()` is a no-op. Trigger such work eagerly inside `fn`.
715
792
  */
716
793
  function injectStartTransaction() {
717
794
  const scope = injectTransitionScope();
@@ -721,7 +798,15 @@ function injectStartTransaction() {
721
798
  // Hold BEFORE the writes, so the display freezes at pre-transaction values.
722
799
  scope.beginHold();
723
800
  let finished = false;
801
+ // eslint-disable-next-line prefer-const -- assigned in try/catch, but needs to be declared here for the `finally` block to see it
724
802
  let watcher;
803
+ let resolveDone;
804
+ const done = new Promise((resolve) => {
805
+ resolveDone = resolve;
806
+ });
807
+ // Every exit path funnels through here, so `done` always settles — including `abort()`
808
+ // and a throwing transaction body (which would otherwise leak the hold forever and
809
+ // freeze the boundary with no recovery).
725
810
  const finish = (restore) => {
726
811
  if (finished)
727
812
  return;
@@ -732,27 +817,28 @@ function injectStartTransaction() {
732
817
  else
733
818
  txn.clear();
734
819
  scope.endHold();
820
+ resolveDone();
735
821
  };
736
- runInTransaction(txn, fn);
822
+ try {
823
+ runInTransaction(txn, fn);
824
+ }
825
+ catch (e) {
826
+ finish(true);
827
+ throw e;
828
+ }
737
829
  let sawPending = false;
738
- const done = new Promise((resolve) => {
739
- watcher = effect(() => {
740
- const p = scope.pending();
741
- if (p)
742
- sawPending = true;
743
- if (sawPending && !p) {
744
- finish(false);
745
- resolve();
746
- }
747
- }, { injector });
748
- // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
749
- afterNextRender(() => {
750
- if (!sawPending && !untracked(scope.pending)) {
751
- finish(false);
752
- resolve();
753
- }
754
- }, { injector });
755
- });
830
+ watcher = effect(() => {
831
+ const p = scope.pending();
832
+ if (p)
833
+ sawPending = true;
834
+ if (sawPending && !p)
835
+ finish(false);
836
+ }, { injector });
837
+ // no-async fallback: if nothing ever went in flight, settle once the writes are processed.
838
+ afterNextRender(() => {
839
+ if (!sawPending && !untracked(scope.pending))
840
+ finish(false);
841
+ }, { injector });
756
842
  return {
757
843
  pending: scope.pending,
758
844
  done,
@@ -761,6 +847,17 @@ function injectStartTransaction() {
761
847
  };
762
848
  }
763
849
 
850
+ /**
851
+ * @internal
852
+ */
853
+ function getSignalEquality(sig) {
854
+ const internal = sig[SIGNAL];
855
+ if (internal && typeof internal.equal === 'function') {
856
+ return internal.equal;
857
+ }
858
+ return Object.is; // Default equality check
859
+ }
860
+
764
861
  /**
765
862
  * Converts a read-only `Signal` into a `WritableSignal` by providing custom `set` and, optionally, `update` functions.
766
863
  * This can be useful for creating controlled write access to a signal that is otherwise read-only.
@@ -859,6 +956,7 @@ function debounced(initial, opt) {
859
956
  * ```
860
957
  */
861
958
  function debounce(source, opt) {
959
+ const eq = opt?.equal ?? getSignalEquality(source);
862
960
  const ms = opt?.ms ?? 0;
863
961
  const trigger = signal(false, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
864
962
  let timeout;
@@ -873,25 +971,25 @@ function debounce(source, opt) {
873
971
  catch {
874
972
  // not in injection context & no destroyRef provided opting out of cleanup
875
973
  }
876
- const triggerFn = (next) => {
974
+ const set = (next) => {
975
+ const isEqual = eq(untracked(source), next);
976
+ if (!timeout && isEqual)
977
+ return; // nothing to do
877
978
  if (timeout)
878
- clearTimeout(timeout);
879
- source.set(next);
979
+ clearTimeout(timeout); // clear pending
980
+ if (!isEqual)
981
+ source.set(next);
880
982
  timeout = setTimeout(() => {
983
+ timeout = undefined;
881
984
  trigger.update((c) => !c);
882
985
  }, ms);
883
986
  };
884
- const set = (value) => {
885
- triggerFn(value);
886
- };
887
- const update = (fn) => {
888
- triggerFn(fn(untracked(source)));
889
- };
987
+ const update = (fn) => set(fn(untracked(source)));
890
988
  const writable = toWritable(computed(() => {
891
989
  trigger();
892
990
  return untracked(source);
893
991
  }, opt), set, update);
894
- writable.original = source;
992
+ writable.original = source.asReadonly();
895
993
  return writable;
896
994
  }
897
995
 
@@ -1062,8 +1160,18 @@ function derived(source, optOrKey, opt) {
1062
1160
  if (isMutable(source)) {
1063
1161
  sig.mutate = (updater) => {
1064
1162
  cnt++;
1065
- sig.update(updater);
1066
- cnt--;
1163
+ try {
1164
+ sig.update(updater);
1165
+ // The wrapped computed evaluates its `equal` lazily — at the next read, which would
1166
+ // normally happen after `cnt` has already dropped back to 0. For a reference-stable
1167
+ // mutation that read compares the same object to itself and the version never bumps,
1168
+ // so dependents are never notified. Reading here, while equality is still suppressed,
1169
+ // forces the recompute (and version bump) inside the mutate window.
1170
+ untracked(sig);
1171
+ }
1172
+ finally {
1173
+ cnt--;
1174
+ }
1067
1175
  };
1068
1176
  sig.inline = (updater) => {
1069
1177
  sig.mutate((prev) => {
@@ -1119,16 +1227,43 @@ function isDerivation(sig) {
1119
1227
  }
1120
1228
 
1121
1229
  function keepPrevious(src, opt) {
1230
+ const mutableSrc = isWritableSignal$2(src) && isMutable(src);
1231
+ // For a mutable source the linkedSignal's equality must be suppressible: a forwarded
1232
+ // `mutate` keeps the same reference, which default equality would otherwise swallow.
1233
+ let cnt = 0;
1234
+ const baseEqual = opt?.equal;
1235
+ const equal = mutableSrc
1236
+ ? (a, b) => cnt > 0 ? false : baseEqual ? baseEqual(a, b) : Object.is(a, b)
1237
+ : baseEqual;
1122
1238
  const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), ...opt,
1123
1239
  source: () => src(),
1124
- computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next });
1240
+ computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next,
1241
+ equal });
1125
1242
  if (isWritableSignal$2(src)) {
1126
1243
  persisted.set = src.set;
1127
1244
  persisted.update = src.update;
1128
- persisted.asReadonly = src.asReadonly;
1129
- if (isMutable(src)) {
1130
- persisted.mutate = src.mutate;
1131
- persisted.inline = src.inline;
1245
+ // NOTE: `asReadonly` deliberately stays the linkedSignal's own — returning the
1246
+ // source's readonly view would reintroduce the `undefined` flashes this wrapper exists
1247
+ // to prevent.
1248
+ if (mutableSrc) {
1249
+ persisted.mutate = (updater) => {
1250
+ cnt++;
1251
+ try {
1252
+ src.mutate(updater);
1253
+ // force the recompute while equality is suppressed, so the reference-stable
1254
+ // mutation bumps the wrapper's version (see derived.ts for the same pattern)
1255
+ untracked(persisted);
1256
+ }
1257
+ finally {
1258
+ cnt--;
1259
+ }
1260
+ };
1261
+ persisted.inline = (updater) => {
1262
+ persisted.mutate((prev) => {
1263
+ updater(prev);
1264
+ return prev;
1265
+ });
1266
+ };
1132
1267
  }
1133
1268
  if (isDerivation(src)) {
1134
1269
  persisted.from = src.from;
@@ -1185,13 +1320,18 @@ function indexArray(source, map, opt = {}) {
1185
1320
  : toWritable(data, () => {
1186
1321
  // noop
1187
1322
  });
1323
+ // copy before defaulting `equal` — assigning onto `opt` would mutate a caller-owned
1324
+ // (possibly shared/reused) options object
1188
1325
  if (isWritableSignal$1(data) && isMutable(data) && !opt.equal) {
1189
- opt.equal = (a, b) => {
1190
- if (typeof a !== typeof b)
1191
- return false;
1192
- if (typeof a === 'object' || typeof a === 'function')
1193
- return false;
1194
- return a === b;
1326
+ opt = {
1327
+ ...opt,
1328
+ equal: (a, b) => {
1329
+ if (typeof a !== typeof b)
1330
+ return false;
1331
+ if (typeof a === 'object' || typeof a === 'function')
1332
+ return false;
1333
+ return a === b;
1334
+ },
1195
1335
  };
1196
1336
  }
1197
1337
  return linkedSignal({
@@ -1385,8 +1525,17 @@ function pooledKeys(src) {
1385
1525
  for (const k in val)
1386
1526
  if (Object.prototype.hasOwnProperty.call(val, k))
1387
1527
  spare.add(k);
1388
- if (active.size === spare.size && active.isSubsetOf(spare))
1389
- return active;
1528
+ if (active.size === spare.size) {
1529
+ let subset = true;
1530
+ for (const k of active) {
1531
+ if (!spare.has(k)) {
1532
+ subset = false;
1533
+ break;
1534
+ }
1535
+ }
1536
+ if (subset)
1537
+ return active;
1538
+ }
1390
1539
  const temp = active;
1391
1540
  active = spare;
1392
1541
  spare = temp;
@@ -1486,7 +1635,7 @@ const filter = (predicate) => (src) => linkedSignal({
1486
1635
  computation: (next, prev) => {
1487
1636
  if (predicate(next))
1488
1637
  return next;
1489
- return prev?.source;
1638
+ return prev?.value;
1490
1639
  },
1491
1640
  });
1492
1641
  /**
@@ -1522,7 +1671,7 @@ const tap = (fn, injector) => (src) => {
1522
1671
  */
1523
1672
  const filterWith = (predicate, initial) => (src) => linkedSignal({
1524
1673
  source: src,
1525
- computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
1674
+ computation: (next, prev) => predicate(next) ? next : prev ? prev.value : initial,
1526
1675
  });
1527
1676
  /**
1528
1677
  * Emit `initial` on the first read, then mirror the source on every subsequent
@@ -1571,7 +1720,7 @@ const pairwise = () => (src) => linkedSignal({
1571
1720
  */
1572
1721
  const scan = (reducer, seed) => (src) => linkedSignal({
1573
1722
  source: src,
1574
- computation: (next, prev) => reducer(prev?.value ?? seed, next),
1723
+ computation: (next, prev) => reducer(prev ? prev.value : seed, next),
1575
1724
  });
1576
1725
 
1577
1726
  /**
@@ -1622,7 +1771,7 @@ function pipeable(signal) {
1622
1771
  return internal;
1623
1772
  }
1624
1773
  /**
1625
- * Create a new **writable** signal and return it as a `PipableSignal`.
1774
+ * Create a new **writable** signal and return it as a `PipeableSignal`.
1626
1775
  *
1627
1776
  * The returned value is a `WritableSignal<T>` with `.set`, `.update`, `.asReadonly`
1628
1777
  * still available (via intersection type), plus a chainable `.pipe(...)`.
@@ -1726,6 +1875,20 @@ function pooledMap(optOrComputation, signalOpt) {
1726
1875
  return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
1727
1876
  }
1728
1877
 
1878
+ /**
1879
+ * @internal Run a sensor factory inside `injector` when provided, else in the ambient
1880
+ * injection context. Keeps every sensor's escape hatch identical and in one place.
1881
+ */
1882
+ function runInSensorContext(injector, fn) {
1883
+ return injector ? runInInjectionContext(injector, fn) : fn();
1884
+ }
1885
+ /**
1886
+ * @internal Normalize the legacy positional `debugName: string` form into {@link SensorRunOptions}.
1887
+ */
1888
+ function coerceSensorOptions(opt) {
1889
+ return typeof opt === 'string' ? { debugName: opt } : (opt ?? {});
1890
+ }
1891
+
1729
1892
  const EVENTS = [
1730
1893
  'chargingchange',
1731
1894
  'levelchange',
@@ -1747,7 +1910,11 @@ const EVENTS = [
1747
1910
  * });
1748
1911
  * ```
1749
1912
  */
1750
- function batteryStatus(debugName = 'batteryStatus') {
1913
+ function batteryStatus(opt) {
1914
+ const { debugName = 'batteryStatus', injector } = coerceSensorOptions(opt);
1915
+ return runInSensorContext(injector, () => createBatteryStatus(debugName));
1916
+ }
1917
+ function createBatteryStatus(debugName) {
1751
1918
  if (isPlatformServer(inject(PLATFORM_ID)) ||
1752
1919
  typeof navigator === 'undefined' ||
1753
1920
  typeof navigator.getBattery !== 'function') {
@@ -1756,7 +1923,9 @@ function batteryStatus(debugName = 'batteryStatus') {
1756
1923
  const state = signal(null, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
1757
1924
  const abortController = new AbortController();
1758
1925
  inject(DestroyRef).onDestroy(() => abortController.abort());
1759
- navigator.getBattery().then((battery) => {
1926
+ navigator
1927
+ .getBattery()
1928
+ .then((battery) => {
1760
1929
  if (abortController.signal.aborted)
1761
1930
  return;
1762
1931
  const read = () => ({
@@ -1772,6 +1941,10 @@ function batteryStatus(debugName = 'batteryStatus') {
1772
1941
  signal: abortController.signal,
1773
1942
  });
1774
1943
  }
1944
+ })
1945
+ .catch(() => {
1946
+ // getBattery() rejects (NotAllowedError) when the `battery` permissions-policy is
1947
+ // disallowed, e.g. in cross-origin iframes — stay `null`, same as unsupported.
1775
1948
  });
1776
1949
  return state.asReadonly();
1777
1950
  }
@@ -1787,7 +1960,11 @@ function batteryStatus(debugName = 'batteryStatus') {
1787
1960
  * in browsers that gate it. Errors from `navigator.clipboard.readText` are
1788
1961
  * swallowed silently to keep the signal value stable.
1789
1962
  */
1790
- function clipboard(debugName = 'clipboard') {
1963
+ function clipboard(opt) {
1964
+ const { debugName = 'clipboard', injector } = coerceSensorOptions(opt);
1965
+ return runInSensorContext(injector, () => createClipboard(debugName));
1966
+ }
1967
+ function createClipboard(debugName) {
1791
1968
  if (isPlatformServer(inject(PLATFORM_ID)) ||
1792
1969
  typeof navigator === 'undefined' ||
1793
1970
  !navigator.clipboard) {
@@ -1837,7 +2014,13 @@ function observerSupported$1() {
1837
2014
  * });
1838
2015
  * ```
1839
2016
  */
1840
- function elementSize(target = inject(ElementRef), opt) {
2017
+ function elementSize(target, opt) {
2018
+ return runInSensorContext(opt?.injector, () =>
2019
+ // the host-element default must resolve INSIDE the sensor context, not as a
2020
+ // parameter default (which would run before the injector wrapper)
2021
+ createElementSize(target ?? inject(ElementRef), opt));
2022
+ }
2023
+ function createElementSize(target, opt) {
1841
2024
  const getElement = () => {
1842
2025
  if (isSignal(target)) {
1843
2026
  try {
@@ -1851,8 +2034,8 @@ function elementSize(target = inject(ElementRef), opt) {
1851
2034
  return target instanceof ElementRef ? target.nativeElement : target;
1852
2035
  };
1853
2036
  const resolveInitialValue = () => {
1854
- if (!observerSupported$1())
1855
- return undefined;
2037
+ // measuring needs only getBoundingClientRect — ResizeObserver support gates
2038
+ // live updates, not the initial read
1856
2039
  const el = getElement();
1857
2040
  if (el && el.getBoundingClientRect) {
1858
2041
  const rect = el.getBoundingClientRect();
@@ -1970,7 +2153,13 @@ function observerSupported() {
1970
2153
  * }
1971
2154
  * ```
1972
2155
  */
1973
- function elementVisibility(target = inject(ElementRef), opt) {
2156
+ function elementVisibility(target, opt) {
2157
+ return runInSensorContext(opt?.injector, () =>
2158
+ // the host-element default must resolve INSIDE the sensor context, not as a
2159
+ // parameter default (which would run before the injector wrapper)
2160
+ createElementVisibility(target ?? inject(ElementRef), opt));
2161
+ }
2162
+ function createElementVisibility(target, opt) {
1974
2163
  if (isPlatformServer(inject(PLATFORM_ID)) || !observerSupported()) {
1975
2164
  const base = computed(() => undefined, {
1976
2165
  debugName: opt?.debugName,
@@ -2038,11 +2227,18 @@ function unwrap$1(target) {
2038
2227
  * }
2039
2228
  * ```
2040
2229
  */
2041
- function focusWithin(target = inject(ElementRef)) {
2230
+ function focusWithin(target, opt) {
2231
+ return runInSensorContext(opt?.injector, () =>
2232
+ // the host-element default must resolve INSIDE the sensor context, not as a
2233
+ // parameter default (which would run before the injector wrapper)
2234
+ createFocusWithin(target ?? inject(ElementRef), opt));
2235
+ }
2236
+ function createFocusWithin(target, opt) {
2237
+ const debugName = opt?.debugName ?? 'focusWithin';
2042
2238
  if (isPlatformServer(inject(PLATFORM_ID))) {
2043
- return computed(() => false, { debugName: 'focusWithin' });
2239
+ return computed(() => false, { debugName });
2044
2240
  }
2045
- const state = signal(false, { debugName: 'focusWithin' });
2241
+ const state = signal(false, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
2046
2242
  const attach = (el) => {
2047
2243
  state.set(el.contains(document.activeElement));
2048
2244
  const abortController = new AbortController();
@@ -2090,6 +2286,9 @@ function focusWithin(target = inject(ElementRef)) {
2090
2286
  * ```
2091
2287
  */
2092
2288
  function geolocation(opt) {
2289
+ return runInSensorContext(opt?.injector, () => createGeolocation(opt));
2290
+ }
2291
+ function createGeolocation(opt) {
2093
2292
  if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
2094
2293
  const sig = computed(() => null, {
2095
2294
  debugName: opt?.debugName ?? 'geolocation',
@@ -2149,6 +2348,9 @@ const serverDate$1 = new Date();
2149
2348
  * ```
2150
2349
  */
2151
2350
  function idle(opt) {
2351
+ return runInSensorContext(opt?.injector, () => createIdle(opt));
2352
+ }
2353
+ function createIdle(opt) {
2152
2354
  if (isPlatformServer(inject(PLATFORM_ID))) {
2153
2355
  const sig = computed(() => false, {
2154
2356
  debugName: opt?.debugName ?? 'idle',
@@ -2238,7 +2440,11 @@ function idle(opt) {
2238
2440
  * }
2239
2441
  * ```
2240
2442
  */
2241
- function mediaQuery(query, debugName = 'mediaQuery') {
2443
+ function mediaQuery(query, opt) {
2444
+ const { debugName = 'mediaQuery', injector } = coerceSensorOptions(opt);
2445
+ return runInSensorContext(injector, () => createMediaQuery(query, debugName));
2446
+ }
2447
+ function createMediaQuery(query, debugName) {
2242
2448
  if (isPlatformServer(inject(PLATFORM_ID)) ||
2243
2449
  typeof window === 'undefined' ||
2244
2450
  typeof window.matchMedia !== 'function' // jsdom doesn't implement matchMedia
@@ -2276,8 +2482,8 @@ function mediaQuery(query, debugName = 'mediaQuery') {
2276
2482
  * });
2277
2483
  * ```
2278
2484
  */
2279
- function prefersDarkMode(debugName) {
2280
- return mediaQuery('(prefers-color-scheme: dark)', debugName);
2485
+ function prefersDarkMode(opt) {
2486
+ return mediaQuery('(prefers-color-scheme: dark)', opt);
2281
2487
  }
2282
2488
  /**
2283
2489
  * Creates a read-only signal that tracks the user's OS/browser preference
@@ -2304,8 +2510,8 @@ function prefersDarkMode(debugName) {
2304
2510
  * });
2305
2511
  * ```
2306
2512
  */
2307
- function prefersReducedMotion(debugName) {
2308
- return mediaQuery('(prefers-reduced-motion: reduce)', debugName);
2513
+ function prefersReducedMotion(opt) {
2514
+ return mediaQuery('(prefers-reduced-motion: reduce)', opt);
2309
2515
  }
2310
2516
 
2311
2517
  /**
@@ -2354,6 +2560,7 @@ function throttled(initial, opt) {
2354
2560
  * // after the 500ms cooldown.
2355
2561
  */
2356
2562
  function throttle(source, opt) {
2563
+ const eq = opt?.equal ?? getSignalEquality(source);
2357
2564
  const ms = opt?.ms ?? 0;
2358
2565
  const leading = opt?.leading ?? false;
2359
2566
  const trailing = opt?.trailing ?? true;
@@ -2379,31 +2586,32 @@ function throttle(source, opt) {
2379
2586
  fire();
2380
2587
  else
2381
2588
  pendingTrailing = trailing;
2382
- timeout = setTimeout(() => {
2589
+ const onWindowEnd = () => {
2383
2590
  timeout = undefined;
2384
2591
  if (trailing && pendingTrailing) {
2385
2592
  pendingTrailing = false;
2386
2593
  fire();
2594
+ timeout = setTimeout(onWindowEnd, ms);
2387
2595
  }
2388
- }, ms);
2596
+ };
2597
+ timeout = setTimeout(onWindowEnd, ms);
2389
2598
  return;
2390
2599
  }
2391
2600
  if (trailing)
2392
2601
  pendingTrailing = true;
2393
2602
  };
2394
- const set = (value) => {
2395
- source.set(value);
2396
- tick();
2397
- };
2398
- const update = (fn) => {
2399
- source.update(fn);
2603
+ const set = (next) => {
2604
+ if (eq(untracked(source), next))
2605
+ return;
2606
+ source.set(next);
2400
2607
  tick();
2401
2608
  };
2609
+ const update = (fn) => set(fn(untracked(source)));
2402
2610
  const writable = toWritable(computed(() => {
2403
2611
  trigger();
2404
2612
  return untracked(source);
2405
2613
  }, opt), set, update);
2406
- writable.original = source;
2614
+ writable.original = source.asReadonly();
2407
2615
  return writable;
2408
2616
  }
2409
2617
 
@@ -2440,6 +2648,9 @@ function throttle(source, opt) {
2440
2648
  * ```
2441
2649
  */
2442
2650
  function mousePosition(opt) {
2651
+ return runInSensorContext(opt?.injector, () => createMousePosition(opt));
2652
+ }
2653
+ function createMousePosition(opt) {
2443
2654
  if (isPlatformServer(inject(PLATFORM_ID))) {
2444
2655
  const base = computed(() => ({
2445
2656
  x: 0,
@@ -2451,8 +2662,12 @@ function mousePosition(opt) {
2451
2662
  return base;
2452
2663
  }
2453
2664
  const { target = window, coordinateSpace = 'client', touch = false, debugName = 'mousePosition', throttle = 100, } = opt ?? {};
2454
- const eventTarget = target instanceof ElementRef ? target.nativeElement : target;
2455
- if (!eventTarget) {
2665
+ const resolve = (t) => {
2666
+ if (!t)
2667
+ return null;
2668
+ return t instanceof ElementRef ? t.nativeElement : t;
2669
+ };
2670
+ if (!isSignal(target) && !resolve(target)) {
2456
2671
  if (isDevMode())
2457
2672
  console.warn('mousePosition: Target element not found.');
2458
2673
  const base = computed(() => ({
@@ -2475,7 +2690,7 @@ function mousePosition(opt) {
2475
2690
  x = coordinateSpace === 'page' ? event.pageX : event.clientX;
2476
2691
  y = coordinateSpace === 'page' ? event.pageY : event.clientY;
2477
2692
  }
2478
- else if (event.touches.length > 0) {
2693
+ else if (event.touches?.length > 0) {
2479
2694
  const firstTouch = event.touches[0];
2480
2695
  x = coordinateSpace === 'page' ? firstTouch.pageX : firstTouch.clientX;
2481
2696
  y = coordinateSpace === 'page' ? firstTouch.pageY : firstTouch.clientY;
@@ -2485,16 +2700,36 @@ function mousePosition(opt) {
2485
2700
  }
2486
2701
  pos.set({ x, y });
2487
2702
  };
2488
- eventTarget.addEventListener('mousemove', updatePosition);
2489
- if (touch) {
2490
- eventTarget.addEventListener('touchmove', updatePosition);
2491
- }
2492
- inject(DestroyRef).onDestroy(() => {
2493
- eventTarget.removeEventListener('mousemove', updatePosition);
2703
+ // passive: the handler never calls preventDefault, and a non-passive touchmove on
2704
+ // window forces the browser to wait on JS before scrolling (scroll jank on touch)
2705
+ const attach = (el) => {
2706
+ const controller = new AbortController();
2707
+ el.addEventListener('mousemove', updatePosition, {
2708
+ passive: true,
2709
+ signal: controller.signal,
2710
+ });
2494
2711
  if (touch) {
2495
- eventTarget.removeEventListener('touchmove', updatePosition);
2712
+ el.addEventListener('touchmove', updatePosition, {
2713
+ passive: true,
2714
+ signal: controller.signal,
2715
+ });
2496
2716
  }
2497
- });
2717
+ return () => controller.abort();
2718
+ };
2719
+ if (isSignal(target)) {
2720
+ // re-attach whenever the signal resolves to a (new) element — covers viewChild
2721
+ effect((cleanup) => {
2722
+ const el = resolve(target());
2723
+ if (!el)
2724
+ return;
2725
+ cleanup(attach(el));
2726
+ });
2727
+ }
2728
+ else {
2729
+ const el = resolve(target);
2730
+ if (el)
2731
+ inject(DestroyRef).onDestroy(attach(el));
2732
+ }
2498
2733
  const base = pos.asReadonly();
2499
2734
  base.unthrottled = pos.original;
2500
2735
  return base;
@@ -2508,7 +2743,8 @@ const serverDate = new Date();
2508
2743
  * An additional `since` signal is attached, tracking when the status last changed.
2509
2744
  * It's SSR-safe and automatically cleans up its event listeners.
2510
2745
  *
2511
- * @param debugName Optional debug name for the signal.
2746
+ * @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
2747
+ * (with an optional `injector` for creation outside an injection context).
2512
2748
  * @returns A `NetworkStatusSignal` instance.
2513
2749
  *
2514
2750
  * @example
@@ -2519,7 +2755,11 @@ const serverDate = new Date();
2519
2755
  * });
2520
2756
  * ```
2521
2757
  */
2522
- function networkStatus(debugName = 'networkStatus') {
2758
+ function networkStatus(opt) {
2759
+ const { debugName = 'networkStatus', injector } = coerceSensorOptions(opt);
2760
+ return runInSensorContext(injector, () => createNetworkStatus(debugName));
2761
+ }
2762
+ function createNetworkStatus(debugName) {
2523
2763
  if (isPlatformServer(inject(PLATFORM_ID))) {
2524
2764
  const sig = computed(() => true, {
2525
2765
  debugName,
@@ -2567,7 +2807,11 @@ const SSR_FALLBACK = {
2567
2807
  * });
2568
2808
  * ```
2569
2809
  */
2570
- function orientation(debugName = 'orientation') {
2810
+ function orientation(opt) {
2811
+ const { debugName = 'orientation', injector } = coerceSensorOptions(opt);
2812
+ return runInSensorContext(injector, () => createOrientation(debugName));
2813
+ }
2814
+ function createOrientation(debugName) {
2571
2815
  if (isPlatformServer(inject(PLATFORM_ID)) ||
2572
2816
  typeof screen === 'undefined' ||
2573
2817
  !screen.orientation) {
@@ -2594,7 +2838,8 @@ function orientation(debugName = 'orientation') {
2594
2838
  * The primitive is SSR-safe and automatically cleans up its event listeners
2595
2839
  * when the creating context is destroyed.
2596
2840
  *
2597
- * @param debugName Optional debug name for the signal.
2841
+ * @param opt Optional debug name for the signal, or a {@link SensorRunOptions} object
2842
+ * (with an optional `injector` for creation outside an injection context).
2598
2843
  * @returns A read-only `Signal<DocumentVisibilityState>`. On the server,
2599
2844
  * it returns a static signal with a value of `'visible'`.
2600
2845
  *
@@ -2622,7 +2867,11 @@ function orientation(debugName = 'orientation') {
2622
2867
  * }
2623
2868
  * ```
2624
2869
  */
2625
- function pageVisibility(debugName = 'pageVisibility') {
2870
+ function pageVisibility(opt) {
2871
+ const { debugName = 'pageVisibility', injector } = coerceSensorOptions(opt);
2872
+ return runInSensorContext(injector, () => createPageVisibility(debugName));
2873
+ }
2874
+ function createPageVisibility(debugName) {
2626
2875
  if (isPlatformServer(inject(PLATFORM_ID))) {
2627
2876
  return computed(() => 'visible', { debugName });
2628
2877
  }
@@ -2654,31 +2903,25 @@ function pageVisibility(debugName = 'pageVisibility') {
2654
2903
  * selector: 'app-scroll-tracker',
2655
2904
  * template: `
2656
2905
  * <p>Window Scroll: X: {{ windowScroll().x }}, Y: {{ windowScroll().y }}</p>
2657
- * <div #scrollableDiv style="height: 200px; width: 200px; overflow: auto; border: 1px solid black;">
2658
- * <div style="height: 400px; width: 400px;">Scroll me!</div>
2659
- * </div>
2660
- * @if (divScroll()) {
2661
- * <p>Div Scroll: X: {{ divScroll().x }}, Y: {{ divScroll().y }}</p>
2662
- * }
2906
+ * <p>Host Scroll: X: {{ hostScroll().x }}, Y: {{ hostScroll().y }}</p>
2663
2907
  * `
2664
2908
  * })
2665
2909
  * export class ScrollTrackerComponent {
2666
2910
  * readonly windowScroll = scrollPosition(); // Defaults to window
2911
+ * // Signal targets (e.g. viewChild) attach once the element exists:
2667
2912
  * readonly scrollableDiv = viewChild<ElementRef<HTMLDivElement>>('scrollableDiv');
2668
- * readonly divScroll = scrollPosition({ target: this.scrollableDiv() }); // Example with element target
2913
+ * readonly divScroll = scrollPosition({ target: this.scrollableDiv });
2669
2914
  *
2670
2915
  * constructor() {
2671
- * effect(() => {
2672
- * console.log('Window scrolled to:', this.windowScroll());
2673
- * if (this.divScroll()) {
2674
- * console.log('Div scrolled to:', this.divScroll());
2675
- * }
2676
- * });
2916
+ * effect(() => console.log('Window scrolled to:', this.windowScroll()));
2677
2917
  * }
2678
2918
  * }
2679
2919
  * ```
2680
2920
  */
2681
2921
  function scrollPosition(opt) {
2922
+ return runInSensorContext(opt?.injector, () => createScrollPosition(opt));
2923
+ }
2924
+ function createScrollPosition(opt) {
2682
2925
  if (isPlatformServer(inject(PLATFORM_ID))) {
2683
2926
  const base = computed(() => ({
2684
2927
  x: 0,
@@ -2690,43 +2933,44 @@ function scrollPosition(opt) {
2690
2933
  return base;
2691
2934
  }
2692
2935
  const { target = globalThis.window, throttle = 100, debugName = 'scrollPosition', } = opt || {};
2693
- let element;
2694
- let getScrollPosition;
2695
- if (target === globalThis.window || target.window === target) {
2696
- element = target;
2697
- getScrollPosition = () => {
2698
- return {
2699
- x: target.scrollX ?? target.pageXOffset ?? 0,
2700
- y: target.scrollY ?? target.pageYOffset ?? 0,
2701
- };
2702
- };
2703
- }
2704
- else if (target instanceof ElementRef) {
2705
- element = target.nativeElement;
2706
- getScrollPosition = () => {
2707
- return {
2708
- x: target.nativeElement.scrollLeft,
2709
- y: target.nativeElement.scrollTop,
2710
- };
2711
- };
2712
- }
2713
- else {
2714
- element = target;
2715
- getScrollPosition = () => {
2716
- return {
2717
- x: target.scrollLeft,
2718
- y: target.scrollTop,
2719
- };
2720
- };
2721
- }
2722
- const state = throttled(getScrollPosition(), {
2936
+ const resolve = (t) => {
2937
+ if (!t)
2938
+ return null;
2939
+ return t instanceof ElementRef ? t.nativeElement : t;
2940
+ };
2941
+ const isWindow = (el) => el.window === el;
2942
+ const readPosition = (el) => isWindow(el)
2943
+ ? {
2944
+ x: el.scrollX ?? el.pageXOffset ?? 0,
2945
+ y: el.scrollY ?? el.pageYOffset ?? 0,
2946
+ }
2947
+ : { x: el.scrollLeft, y: el.scrollTop };
2948
+ const initial = resolve(isSignal(target) ? untracked(target) : target);
2949
+ const state = throttled(initial ? readPosition(initial) : { x: 0, y: 0 }, {
2723
2950
  debugName,
2724
2951
  equal: (a, b) => a.x === b.x && a.y === b.y,
2725
2952
  ms: throttle,
2726
2953
  });
2727
- const onScroll = () => state.set(getScrollPosition());
2728
- element.addEventListener('scroll', onScroll, { passive: true });
2729
- inject(DestroyRef).onDestroy(() => element.removeEventListener('scroll', onScroll));
2954
+ if (isSignal(target)) {
2955
+ // re-attach whenever the signal resolves to a (new) element — covers viewChild
2956
+ effect((cleanup) => {
2957
+ const el = resolve(target());
2958
+ if (!el)
2959
+ return;
2960
+ state.set(readPosition(el)); // sync to the new element immediately
2961
+ const onScroll = () => state.set(readPosition(el));
2962
+ el.addEventListener('scroll', onScroll, { passive: true });
2963
+ cleanup(() => el.removeEventListener('scroll', onScroll));
2964
+ });
2965
+ }
2966
+ else {
2967
+ const el = resolve(target);
2968
+ if (el) {
2969
+ const onScroll = () => state.set(readPosition(el));
2970
+ el.addEventListener('scroll', onScroll, { passive: true });
2971
+ inject(DestroyRef).onDestroy(() => el.removeEventListener('scroll', onScroll));
2972
+ }
2973
+ }
2730
2974
  const base = state.asReadonly();
2731
2975
  base.unthrottled = state.original;
2732
2976
  return base;
@@ -2774,6 +3018,9 @@ function scrollPosition(opt) {
2774
3018
  * ```
2775
3019
  */
2776
3020
  function windowSize(opt) {
3021
+ return runInSensorContext(opt?.injector, () => createWindowSize(opt));
3022
+ }
3023
+ function createWindowSize(opt) {
2777
3024
  if (isPlatformServer(inject(PLATFORM_ID))) {
2778
3025
  const base = computed(() => ({
2779
3026
  width: 1024,
@@ -2810,17 +3057,19 @@ function sensor(type, options) {
2810
3057
  case 'mousePosition':
2811
3058
  return mousePosition(opts);
2812
3059
  case 'networkStatus':
2813
- return networkStatus(opts?.debugName);
3060
+ return networkStatus(opts);
2814
3061
  case 'pageVisibility':
2815
- return pageVisibility(opts?.debugName);
3062
+ return pageVisibility(opts);
2816
3063
  case 'darkMode':
2817
3064
  case 'dark-mode':
2818
- return prefersDarkMode(opts?.debugName);
3065
+ return prefersDarkMode(opts);
2819
3066
  case 'reducedMotion':
2820
3067
  case 'reduced-motion':
2821
- return prefersReducedMotion(opts?.debugName);
3068
+ return prefersReducedMotion(opts);
2822
3069
  case 'mediaQuery':
2823
- return mediaQuery(opts.query, opts.debugName);
3070
+ if (typeof opts?.query !== 'string')
3071
+ throw new Error(`sensor('mediaQuery') requires a 'query' option, e.g. sensor('mediaQuery', { query: '(min-width: 1024px)' })`);
3072
+ return mediaQuery(opts.query, opts);
2824
3073
  case 'windowSize':
2825
3074
  return windowSize(opts);
2826
3075
  case 'scrollPosition':
@@ -2832,15 +3081,15 @@ function sensor(type, options) {
2832
3081
  case 'geolocation':
2833
3082
  return geolocation(opts);
2834
3083
  case 'clipboard':
2835
- return clipboard(opts?.debugName);
3084
+ return clipboard(opts);
2836
3085
  case 'orientation':
2837
- return orientation(opts?.debugName);
3086
+ return orientation(opts);
2838
3087
  case 'batteryStatus':
2839
- return batteryStatus(opts?.debugName);
3088
+ return batteryStatus(opts);
2840
3089
  case 'idle':
2841
3090
  return idle(opts);
2842
3091
  case 'focusWithin':
2843
- return focusWithin(opts?.target);
3092
+ return focusWithin(opts?.target, opts);
2844
3093
  default:
2845
3094
  throw new Error(`Unknown sensor type: ${type}`);
2846
3095
  }
@@ -2894,16 +3143,24 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2894
3143
  else
2895
3144
  state.set(event);
2896
3145
  };
2897
- const { destroyRef: providedDestroyRef, ...listenerOpts } = opt ?? {};
3146
+ const { destroyRef: providedDestroyRef,
3147
+ // strip non-listener keys so they don't leak into addEventListener options
3148
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3149
+ injector: _injector,
3150
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
3151
+ debugName: _debugName, ...listenerOpts } = opt ?? {};
2898
3152
  if (isSignal(target)) {
2899
3153
  const targetSig = target;
2900
- effect((cleanup) => {
3154
+ const effectRef = effect((cleanup) => {
2901
3155
  const resolved = unwrap(targetSig());
2902
3156
  if (!resolved)
2903
3157
  return;
2904
3158
  resolved.addEventListener(eventName, handler, listenerOpts);
2905
3159
  cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
2906
- }, { injector });
3160
+ }, { ...(ngDevMode ? { debugName: "effectRef" } : /* istanbul ignore next */ {}), injector });
3161
+ // honor an explicit destroyRef for signal targets too — the effect would otherwise
3162
+ // only follow the injector's lifetime, contradicting the documented option
3163
+ providedDestroyRef?.onDestroy(() => effectRef.destroy());
2907
3164
  }
2908
3165
  else {
2909
3166
  const resolved = unwrap(target);
@@ -2992,7 +3249,8 @@ function alwaysFalse() {
2992
3249
  * @internal Attaches a lazy, memoized leaf probe to a store node. The probe (`() => boolean`)
2993
3250
  * closes over the node's value signal and its (stable) vivify setting, building the backing
2994
3251
  * `computed` on first call so leaf-ness tracks the live value reactively without taxing every
2995
- * node access. Idempotent.
3252
+ * node access. Under `noUnionLeaves` the caller promises shapes never flip, so the probe is
3253
+ * resolved once from the first sample and frozen as a constant. Idempotent.
2996
3254
  */
2997
3255
  function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
2998
3256
  if (typeof sig[LEAF] !== 'function') {
@@ -3000,13 +3258,11 @@ function markAsLeaf(sig, value, vivifyEnabled, noUnionLeaves) {
3000
3258
  const probe = () => {
3001
3259
  if (memo)
3002
3260
  return memo();
3003
- const v = untracked(value);
3004
- memo =
3005
- isOpaque(v) || (v == null && !vivifyEnabled) || noUnionLeaves
3006
- ? isLeafValue(v, vivifyEnabled)
3007
- ? alwaysTrue
3008
- : alwaysFalse
3009
- : computed(() => isLeafValue(value(), vivifyEnabled));
3261
+ memo = noUnionLeaves
3262
+ ? isLeafValue(untracked(value), vivifyEnabled)
3263
+ ? alwaysTrue
3264
+ : alwaysFalse
3265
+ : computed(() => isLeafValue(value(), vivifyEnabled));
3010
3266
  return memo();
3011
3267
  };
3012
3268
  Object.defineProperty(sig, LEAF, {
@@ -3094,6 +3350,40 @@ function resolveVivify(sample, option) {
3094
3350
  function hasOwnKey(value, key) {
3095
3351
  return value != null && Object.hasOwn(value, key);
3096
3352
  }
3353
+ /**
3354
+ * @internal
3355
+ * Builds the `onChange` for the fallback (non-record container) derivation branch. For an
3356
+ * immutable source the container is copied before the write — returning the same mutated
3357
+ * reference would let the source's equality cut propagation (leaving child signals permanently
3358
+ * stale) and alias the caller's original object, breaking the structural-sharing contract
3359
+ * `forkStore` relies on. For a mutable source the write goes through `mutate`, so the chain's
3360
+ * force-notify engages (plain `update` with the same reference would never notify).
3361
+ */
3362
+ function createFallbackOnChange(target, prop, vivifyFn, isMutableSource) {
3363
+ const write = (newValue) => (v) => {
3364
+ const container = vivifyFn(v, prop);
3365
+ if (container === null || container === undefined)
3366
+ return container;
3367
+ const next = isMutableSource
3368
+ ? container
3369
+ : Array.isArray(container)
3370
+ ? container.slice()
3371
+ : isRecord(container)
3372
+ ? { ...container }
3373
+ : container; // non-plain leaf (Date/class instance): legacy in-place attempt
3374
+ try {
3375
+ next[prop] = newValue;
3376
+ }
3377
+ catch (e) {
3378
+ if (isDevMode())
3379
+ console.error(`[store] Failed to set property "${String(prop)}"`, e);
3380
+ }
3381
+ return next;
3382
+ };
3383
+ return isMutableSource
3384
+ ? (newValue) => target.mutate(write(newValue))
3385
+ : (newValue) => target.update(write(newValue));
3386
+ }
3097
3387
  /**
3098
3388
  * @internal
3099
3389
  * Makes an array store
@@ -3116,7 +3406,9 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
3116
3406
  const idx = +prop;
3117
3407
  return idx >= 0 && idx < untracked(lengthSignal);
3118
3408
  }
3119
- return Reflect.has(untracked(source), prop);
3409
+ const v = untracked(source);
3410
+ // nullish node values are routinely descended with vivify on — `in` must not throw
3411
+ return v == null ? false : Reflect.has(v, prop);
3120
3412
  },
3121
3413
  ownKeys() {
3122
3414
  const v = untracked(source);
@@ -3153,7 +3445,9 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
3153
3445
  return lengthSignal;
3154
3446
  if (prop === Symbol.iterator) {
3155
3447
  return function* () {
3156
- for (let i = 0; i < untracked(lengthSignal); i++) {
3448
+ // read length reactively: a spread/for-of inside a computed/effect must re-run
3449
+ // when items are added or removed, not only when already-read elements change
3450
+ for (let i = 0; i < lengthSignal(); i++) {
3157
3451
  yield receiver[i];
3158
3452
  }
3159
3453
  };
@@ -3192,19 +3486,8 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
3192
3486
  })
3193
3487
  : derived(target, {
3194
3488
  from: (v) => v?.[idx],
3195
- onChange: (newValue) => target.update((v) => {
3196
- const container = vivifyFn(v, idx);
3197
- if (container === null || container === undefined)
3198
- return container;
3199
- try {
3200
- container[idx] = newValue;
3201
- }
3202
- catch (e) {
3203
- if (isDevMode())
3204
- console.error(`[store] Failed to set property "${String(idx)}"`, e);
3205
- }
3206
- return container;
3207
- }),
3489
+ onChange: createFallbackOnChange(target, idx, vivifyFn, isMutableSource),
3490
+ equal: equalFn,
3208
3491
  });
3209
3492
  const childSample = untracked(computation);
3210
3493
  const childVivify = resolveVivify(childSample, vivify);
@@ -3224,6 +3507,13 @@ function toArrayStore(source, injector, vivify, noUnionLeaves = false) {
3224
3507
  /**
3225
3508
  * Converts a Signal into a deep-observable Store.
3226
3509
  * Accessing nested properties returns a derived Signal of that path.
3510
+ *
3511
+ * @remarks
3512
+ * A child's *container kind* (array store vs object store) is resolved when the child is
3513
+ * first accessed and cached with the proxy. Leaf↔substore flips are tracked reactively, but a
3514
+ * union-typed node that later flips between an array and a record keeps its original trap set —
3515
+ * if you need that, re-model the union as `{ kind: ..., value: ... }` instead.
3516
+ *
3227
3517
  * @example
3228
3518
  * const state = store({ user: { name: 'John' } });
3229
3519
  * const nameSignal = state.user.name; // WritableSignal<string>
@@ -3306,19 +3596,8 @@ function toStore(source, injector, vivify = false, noUnionLeaves = false) {
3306
3596
  ? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
3307
3597
  : derived(target, {
3308
3598
  from: (v) => v?.[prop],
3309
- onChange: (newValue) => target.update((v) => {
3310
- const container = vivifyFn(v, prop);
3311
- if (container === null || container === undefined)
3312
- return container;
3313
- try {
3314
- container[prop] = newValue;
3315
- }
3316
- catch (e) {
3317
- if (isDevMode())
3318
- console.error(`[store] Failed to set property "${String(prop)}"`, e);
3319
- }
3320
- return container;
3321
- }),
3599
+ onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
3600
+ equal: equalFn,
3322
3601
  });
3323
3602
  const childSample = untracked(computation);
3324
3603
  const childVivify = resolveVivify(childSample, vivify);
@@ -3460,7 +3739,12 @@ function merge3(ancestor, mine, theirs) {
3460
3739
  if (isPlainRecord(mine) && isPlainRecord(theirs) && isPlainRecord(ancestor)) {
3461
3740
  const out = { ...theirs };
3462
3741
  for (const key of new Set([...Object.keys(mine), ...Object.keys(theirs)])) {
3463
- out[key] = merge3(ancestor[key], mine[key], theirs[key]);
3742
+ const merged = merge3(ancestor[key], mine[key], theirs[key]);
3743
+ // a key deleted on the fork must commit as ABSENT, not as an explicit `undefined`
3744
+ if (merged === undefined && !(key in mine))
3745
+ delete out[key];
3746
+ else
3747
+ out[key] = merged;
3464
3748
  }
3465
3749
  return out;
3466
3750
  }
@@ -3512,8 +3796,8 @@ const noopStore = {
3512
3796
  *
3513
3797
  * @template T The type of value held by the signal and stored (after serialization).
3514
3798
  * @param fallback The default value of type `T` to use when no value is found in storage
3515
- * or when deserialization fails. The signal's value will never be `null` or `undefined`
3516
- * publicly, it will always revert to this fallback.
3799
+ * or when deserialization fails. A stored value (including a legitimate `null` for a
3800
+ * nullable `T`) always round-trips; the fallback only surfaces when the entry is absent.
3517
3801
  * @param options Configuration options (`CreateStoredOptions<T>`). Requires at least the `key`.
3518
3802
  * @returns A `StoredSignal<T>` instance. This signal behaves like a standard `WritableSignal<T>`,
3519
3803
  * but its value is persisted. It includes a `.clear()` method to remove the item from storage
@@ -3526,7 +3810,8 @@ const noopStore = {
3526
3810
  * - **Error Handling:** Catches and logs errors during serialization/deserialization in dev mode.
3527
3811
  * - **Tab Sync:** If `syncTabs` is true, listens to `storage` events to keep the signal value
3528
3812
  * consistent across browser tabs using the same key. Cleanup is handled automatically
3529
- * using `DestroyRef`.
3813
+ * using `DestroyRef`. Web Storage only: the `storage` event never fires for custom `store`
3814
+ * adapters, so `syncTabs` has no effect with one.
3530
3815
  * - **Removal:** Use the `.clear()` method on the returned signal to remove the item from storage.
3531
3816
  * Setting the signal to the fallback value will store the fallback value, not remove the item.
3532
3817
  *
@@ -3561,25 +3846,28 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
3561
3846
  : isSignal(key)
3562
3847
  ? key
3563
3848
  : computed(key);
3849
+ // "no stored value" marker — distinct from `null`/`undefined`, so a nullable `T` can
3850
+ // round-trip a legitimate `null` through `set` instead of it acting like `clear()`
3851
+ const EMPTY = Symbol();
3564
3852
  const getValue = (key) => {
3565
3853
  const found = store.getItem(key);
3566
3854
  if (found === null)
3567
- return null;
3855
+ return EMPTY;
3568
3856
  try {
3569
3857
  const deserialized = deserialize(found);
3570
3858
  if (!validate(deserialized))
3571
- return null;
3859
+ return EMPTY;
3572
3860
  return deserialized;
3573
3861
  }
3574
3862
  catch (err) {
3575
3863
  if (isDevMode())
3576
3864
  console.error(`Failed to parse stored value for key "${key}":`, err);
3577
- return null;
3865
+ return EMPTY;
3578
3866
  }
3579
3867
  };
3580
3868
  const storeValue = (key, value) => {
3581
3869
  try {
3582
- if (value === null)
3870
+ if (value === EMPTY)
3583
3871
  return store.removeItem(key);
3584
3872
  const serialized = serialize(value);
3585
3873
  store.setItem(key, serialized);
@@ -3596,9 +3884,9 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
3596
3884
  const initialKey = untracked(keySig);
3597
3885
  const internal = signal(getValue(initialKey), { ...(ngDevMode ? { debugName: "internal" } : /* istanbul ignore next */ {}), ...opt,
3598
3886
  equal: (a, b) => {
3599
- if (a === null && b === null)
3887
+ if (a === EMPTY && b === EMPTY)
3600
3888
  return true;
3601
- if (a === null || b === null)
3889
+ if (a === EMPTY || b === EMPTY)
3602
3890
  return false;
3603
3891
  return equal(a, b);
3604
3892
  } });
@@ -3633,19 +3921,27 @@ function stored(fallback, { key, store: providedStore, serialize = JSON.stringif
3633
3921
  if (syncTabs && !isServer) {
3634
3922
  const destroyRef = inject(DestroyRef);
3635
3923
  const sync = (e) => {
3924
+ // `storage` events only describe Web Storage — ignore events for a different
3925
+ // storage area (or any event when a custom adapter is configured), otherwise an
3926
+ // unrelated localStorage write with the same key string corrupts our state
3927
+ if (e.storageArea !== store)
3928
+ return;
3636
3929
  if (e.key !== untracked(keySig))
3637
3930
  return;
3638
3931
  if (e.newValue === null)
3639
- internal.set(null);
3932
+ internal.set(EMPTY);
3640
3933
  else
3641
3934
  internal.set(getValue(e.key));
3642
3935
  };
3643
3936
  window.addEventListener('storage', sync);
3644
3937
  destroyRef.onDestroy(() => window.removeEventListener('storage', sync));
3645
3938
  }
3646
- const writable = toWritable(computed(() => internal() ?? fallback, opt), internal.set);
3939
+ const writable = toWritable(computed(() => {
3940
+ const v = internal();
3941
+ return v === EMPTY ? fallback : v;
3942
+ }, opt), internal.set);
3647
3943
  writable.clear = () => {
3648
- internal.set(null);
3944
+ internal.set(EMPTY);
3649
3945
  };
3650
3946
  writable.key = keySig;
3651
3947
  return writable;
@@ -3655,7 +3951,6 @@ class MessageBus {
3655
3951
  channel = new BroadcastChannel('mmstack-tab-sync-bus');
3656
3952
  listeners = new Map();
3657
3953
  subscribe(id, listener) {
3658
- this.unsubscribe(id); // Ensure no duplicate listeners
3659
3954
  const wrapped = (ev) => {
3660
3955
  try {
3661
3956
  if (ev.data?.id === id)
@@ -3666,18 +3961,28 @@ class MessageBus {
3666
3961
  }
3667
3962
  };
3668
3963
  this.channel.addEventListener('message', wrapped);
3669
- this.listeners.set(id, wrapped);
3964
+ let set = this.listeners.get(id);
3965
+ if (!set) {
3966
+ set = new Set();
3967
+ this.listeners.set(id, set);
3968
+ }
3969
+ set.add(wrapped);
3670
3970
  return {
3671
- unsub: (() => this.unsubscribe(id)).bind(this),
3672
- post: ((value) => this.channel.postMessage({ id, value })).bind(this),
3971
+ unsub: () => {
3972
+ this.channel.removeEventListener('message', wrapped);
3973
+ const cur = this.listeners.get(id);
3974
+ if (!cur)
3975
+ return;
3976
+ cur.delete(wrapped);
3977
+ if (cur.size === 0)
3978
+ this.listeners.delete(id);
3979
+ },
3980
+ post: (value) => this.channel.postMessage({ id, value }),
3673
3981
  };
3674
3982
  }
3675
- unsubscribe(id) {
3676
- const listener = this.listeners.get(id);
3677
- if (!listener)
3678
- return;
3679
- this.channel.removeEventListener('message', listener);
3680
- this.listeners.delete(id);
3983
+ ngOnDestroy() {
3984
+ this.channel.close();
3985
+ this.listeners.clear();
3681
3986
  }
3682
3987
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3683
3988
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: MessageBus, providedIn: 'root' });
@@ -3688,6 +3993,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
3688
3993
  providedIn: 'root',
3689
3994
  }]
3690
3995
  }] });
3996
+ /**
3997
+ * @deprecated The generated id hashes the call-site stack line, which collides when a shared
3998
+ * helper calls {@link tabSync} for multiple signals and diverges across minified builds during
3999
+ * a rolling deploy. Pass an explicit `{ id }` instead.
4000
+ */
3691
4001
  function generateDeterministicID() {
3692
4002
  const stack = new Error().stack;
3693
4003
  if (stack) {
@@ -3725,10 +4035,8 @@ function generateDeterministicID() {
3725
4035
  *
3726
4036
  * @example
3727
4037
  * ```typescript
3728
- * // Basic usage - auto-generates channel ID from call site
3729
- * const theme = tabSync(signal('dark'));
3730
- *
3731
- * // With explicit ID (recommended for production)
4038
+ * // With explicit ID (recommended)
4039
+ * const theme = tabSync(signal('dark'), { id: 'theme' });
3732
4040
  * const userPrefs = tabSync(signal({ lang: 'en' }), { id: 'user-preferences' });
3733
4041
  *
3734
4042
  * // Changes in one tab will sync to all other tabs
@@ -3740,6 +4048,7 @@ function generateDeterministicID() {
3740
4048
  * - Uses a single BroadcastChannel for all synchronized signals
3741
4049
  * - Automatically cleans up listeners when the injection context is destroyed
3742
4050
  * - Initial signal value after sync setup is not broadcasted to prevent loops
4051
+ * - Received values are not re-broadcast, so tabs never echo each other's updates
3743
4052
  *
3744
4053
  */
3745
4054
  function tabSync(sig, opt) {
@@ -3747,7 +4056,20 @@ function tabSync(sig, opt) {
3747
4056
  return sig;
3748
4057
  const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
3749
4058
  const bus = inject(MessageBus);
3750
- const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
4059
+ // The last value applied from a remote tab. The outbound effect skips (exactly) the run
4060
+ // caused by that write — without this, an inbound object (a fresh structured clone, so
4061
+ // never reference-equal) would be re-posted, and two tabs would ping-pong forever.
4062
+ const NONE = Symbol();
4063
+ let received = NONE;
4064
+ const { unsub, post } = bus.subscribe(id, (next) => {
4065
+ const before = untracked(sig);
4066
+ received = next;
4067
+ sig.set(next);
4068
+ // Equality-suppressed write (e.g. an identical primitive): no effect run will follow,
4069
+ // so clear the marker — it must not swallow a later, genuinely local change.
4070
+ if (untracked(sig) === before)
4071
+ received = NONE;
4072
+ });
3751
4073
  let first = false;
3752
4074
  const effectRef = effect(() => {
3753
4075
  const val = sig();
@@ -3755,6 +4077,11 @@ function tabSync(sig, opt) {
3755
4077
  first = true;
3756
4078
  return;
3757
4079
  }
4080
+ if (val === received) {
4081
+ received = NONE;
4082
+ return;
4083
+ }
4084
+ received = NONE;
3758
4085
  post(val);
3759
4086
  }, ...(ngDevMode ? [{ debugName: "effectRef" }] : /* istanbul ignore next */ []));
3760
4087
  inject(DestroyRef).onDestroy(() => {
@@ -3765,7 +4092,6 @@ function tabSync(sig, opt) {
3765
4092
  }
3766
4093
 
3767
4094
  function until(sourceSignal, predicate, options = {}) {
3768
- const injector = options.injector ?? inject(Injector);
3769
4095
  return new Promise((resolve, reject) => {
3770
4096
  let effectRef;
3771
4097
  let timeoutId;
@@ -3802,6 +4128,14 @@ function until(sourceSignal, predicate, options = {}) {
3802
4128
  cleanupAndResolve(initialValue);
3803
4129
  return;
3804
4130
  }
4131
+ let injector;
4132
+ try {
4133
+ injector = options.injector ?? inject(Injector);
4134
+ }
4135
+ catch {
4136
+ cleanupAndReject('until: No injector available — provide options.injector when calling outside an injection context.');
4137
+ return;
4138
+ }
3805
4139
  if (options?.timeout !== undefined) {
3806
4140
  timeoutId = setTimeout(() => cleanupAndReject(`until: Timeout after ${options.timeout}ms.`), options.timeout);
3807
4141
  }
@@ -3819,17 +4153,6 @@ function until(sourceSignal, predicate, options = {}) {
3819
4153
  });
3820
4154
  }
3821
4155
 
3822
- /**
3823
- * @interal
3824
- */
3825
- function getSignalEquality(sig) {
3826
- const internal = sig[SIGNAL];
3827
- if (internal && typeof internal.equal === 'function') {
3828
- return internal.equal;
3829
- }
3830
- return Object.is; // Default equality check
3831
- }
3832
-
3833
4156
  /**
3834
4157
  * Enhances an existing `WritableSignal` by adding a complete undo/redo history
3835
4158
  * stack and an API to control it.
@@ -3878,9 +4201,10 @@ function getSignalEquality(sig) {
3878
4201
  * ```
3879
4202
  */
3880
4203
  function withHistory(sourceOrValue, opt) {
3881
- const equal = (opt?.equal ?? isSignal(sourceOrValue))
3882
- ? getSignalEquality(sourceOrValue)
3883
- : Object.is;
4204
+ const equal = opt?.equal ??
4205
+ (isSignal(sourceOrValue)
4206
+ ? getSignalEquality(sourceOrValue)
4207
+ : Object.is);
3884
4208
  const source = isSignal(sourceOrValue)
3885
4209
  ? sourceOrValue
3886
4210
  : signal(sourceOrValue);
@@ -3925,9 +4249,8 @@ function withHistory(sourceOrValue, opt) {
3925
4249
  if (historyStack.length === 0)
3926
4250
  return;
3927
4251
  const valueForRedo = untracked(source);
3928
- const valueToRestore = historyStack.at(-1);
3929
- if (valueToRestore === undefined)
3930
- return;
4252
+ // length checked above — a legitimately `undefined` entry must still restore
4253
+ const valueToRestore = historyStack[historyStack.length - 1];
3931
4254
  originalSet.call(source, valueToRestore);
3932
4255
  history.inline((h) => h.pop());
3933
4256
  redoArray.mutate((r) => {
@@ -3941,9 +4264,8 @@ function withHistory(sourceOrValue, opt) {
3941
4264
  if (redoStack.length === 0)
3942
4265
  return;
3943
4266
  const valueForUndo = untracked(source);
3944
- const valueToRestore = redoStack.at(-1);
3945
- if (valueToRestore === undefined)
3946
- return;
4267
+ // length checked above — a legitimately `undefined` entry must still restore
4268
+ const valueToRestore = redoStack[redoStack.length - 1];
3947
4269
  originalSet.call(source, valueToRestore);
3948
4270
  redoArray.inline((r) => r.pop());
3949
4271
  history.mutate((h) => {
@@ -3966,5 +4288,5 @@ function withHistory(sourceOrValue, opt) {
3966
4288
  * Generated bundle index. Do not edit.
3967
4289
  */
3968
4290
 
3969
- export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, forkStore, geolocation, 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, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
4291
+ export { MmActivity, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, 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, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
3970
4292
  //# sourceMappingURL=mmstack-primitives.mjs.map