@mmstack/primitives 21.0.22 → 21.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isWritableSignal as isWritableSignal$1, isSignal, ElementRef, PLATFORM_ID, Injectable, runInInjectionContext } from '@angular/core';
2
+ import { isDevMode, inject, Injector, untracked, effect, DestroyRef, linkedSignal, computed, signal, isWritableSignal as isWritableSignal$1, isSignal, PLATFORM_ID, ElementRef, Injectable, runInInjectionContext } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
4
  import { SIGNAL } from '@angular/core/primitives/signals';
5
5
 
@@ -349,9 +349,9 @@ function debounce(source, opt) {
349
349
  const { is } = Object;
350
350
  function mutable(initial, opt) {
351
351
  const baseEqual = opt?.equal ?? is;
352
- let trigger = false;
352
+ let cnt = 0;
353
353
  const equal = (a, b) => {
354
- if (trigger)
354
+ if (cnt > 0)
355
355
  return false;
356
356
  return baseEqual(a, b);
357
357
  };
@@ -361,9 +361,9 @@ function mutable(initial, opt) {
361
361
  });
362
362
  const internalUpdate = sig.update;
363
363
  sig.mutate = (updater) => {
364
- trigger = true;
364
+ cnt++;
365
365
  internalUpdate(updater);
366
- trigger = false;
366
+ cnt--;
367
367
  };
368
368
  sig.inline = (updater) => {
369
369
  sig.mutate((prev) => {
@@ -430,10 +430,10 @@ function derived(source, optOrKey, opt) {
430
430
  };
431
431
  const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
432
432
  const baseEqual = rest?.equal ?? Object.is;
433
- let trigger = false;
433
+ let cnt = 0;
434
434
  const equal = isMutable(source)
435
435
  ? (a, b) => {
436
- if (trigger)
436
+ if (cnt > 0)
437
437
  return false;
438
438
  return baseEqual(a, b);
439
439
  }
@@ -442,9 +442,9 @@ function derived(source, optOrKey, opt) {
442
442
  sig.from = from;
443
443
  if (isMutable(source)) {
444
444
  sig.mutate = (updater) => {
445
- trigger = true;
445
+ cnt++;
446
446
  sig.update(updater);
447
- trigger = false;
447
+ cnt--;
448
448
  };
449
449
  sig.inline = (updater) => {
450
450
  sig.mutate((prev) => {
@@ -549,9 +549,11 @@ function indexArray(source, map, opt = {}) {
549
549
  });
550
550
  if (isWritableSignal(data) && isMutable(data) && !opt.equal) {
551
551
  opt.equal = (a, b) => {
552
- if (a !== b)
553
- return false; // actually check primitives and references
554
- return false; // opt out for same refs
552
+ if (typeof a !== typeof b)
553
+ return false;
554
+ if (typeof a === 'object' || typeof a === 'function')
555
+ return false;
556
+ return a === b;
555
557
  };
556
558
  }
557
559
  return linkedSignal({
@@ -773,10 +775,35 @@ const filter = (predicate) => (src) => linkedSignal({
773
775
  },
774
776
  });
775
777
  /** tap into the value */
776
- const tap = (fn) => (src) => {
777
- effect(() => fn(src()));
778
+ const tap = (fn, injector) => (src) => {
779
+ effect(() => fn(src()), {
780
+ injector,
781
+ });
778
782
  return src;
779
783
  };
784
+ /**
785
+ * Like {@link filter}, but emits `initial` until a value passes the predicate
786
+ * for the first time. Avoids the `T | undefined` return type.
787
+ */
788
+ const filterWith = (predicate, initial) => (src) => linkedSignal({
789
+ source: src,
790
+ computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
791
+ });
792
+ /** Emits `initial` first, then mirrors source. */
793
+ const startWith = (initial) => (src) => linkedSignal({
794
+ source: src,
795
+ computation: (next, prev) => (prev === undefined ? initial : next),
796
+ });
797
+ /** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
798
+ const pairwise = () => (src) => linkedSignal({
799
+ source: src,
800
+ computation: (next, prev) => [prev?.source, next],
801
+ });
802
+ /** Reduce-like accumulator across emissions. */
803
+ const scan = (reducer, seed) => (src) => linkedSignal({
804
+ source: src,
805
+ computation: (next, prev) => reducer(prev?.value ?? seed, next),
806
+ });
780
807
 
781
808
  /**
782
809
  * Decorate any `Signal<T>` with a chainable `.pipe(...)` method.
@@ -880,12 +907,10 @@ function pooled({ create, reset, computation, ...opt }) {
880
907
  const next = other ?? untracked(() => create());
881
908
  if (current !== undefined) {
882
909
  if (currentFresh) {
883
- // never-mutated buffer leaving the active slot; nothing to clean
884
910
  other = current;
885
911
  }
886
912
  else {
887
- // reset on release: clean the dirty buffer as it goes back into the pool
888
- // (also threads the swap-return correctly into the pool's spare slot)
913
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- guarded by the `current !== undefined` check above
889
914
  other = untracked(() => reset(current)) ?? current;
890
915
  }
891
916
  }
@@ -932,6 +957,87 @@ function pooledMap(optOrComputation, signalOpt) {
932
957
  return pooled(toPooledOptions(optOrComputation, createEmptyMap, resetClearable, signalOpt));
933
958
  }
934
959
 
960
+ const EVENTS = [
961
+ 'chargingchange',
962
+ 'levelchange',
963
+ 'chargingtimechange',
964
+ 'dischargingtimechange',
965
+ ];
966
+ /**
967
+ * Creates a read-only signal that tracks the system battery status using the
968
+ * Battery Status API. Returns `null` until the underlying `getBattery()`
969
+ * promise resolves, or permanently when the API is unsupported (Firefox /
970
+ * Safari at the time of writing). SSR-safe.
971
+ */
972
+ function batteryStatus(debugName = 'batteryStatus') {
973
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
974
+ typeof navigator === 'undefined' ||
975
+ typeof navigator.getBattery !== 'function') {
976
+ return computed(() => null, { debugName });
977
+ }
978
+ const state = signal(null, { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
979
+ const abortController = new AbortController();
980
+ inject(DestroyRef).onDestroy(() => abortController.abort());
981
+ navigator.getBattery().then((battery) => {
982
+ if (abortController.signal.aborted)
983
+ return;
984
+ const read = () => ({
985
+ level: battery.level,
986
+ charging: battery.charging,
987
+ chargingTime: battery.chargingTime,
988
+ dischargingTime: battery.dischargingTime,
989
+ });
990
+ const onChange = () => state.set(read());
991
+ state.set(read());
992
+ for (const ev of EVENTS) {
993
+ battery.addEventListener(ev, onChange, {
994
+ signal: abortController.signal,
995
+ });
996
+ }
997
+ });
998
+ return state.asReadonly();
999
+ }
1000
+
1001
+ /**
1002
+ * Creates a read-only signal mirroring the system clipboard contents.
1003
+ *
1004
+ * The signal value starts empty and updates whenever a `copy` event fires on
1005
+ * the document (or {@link ClipboardSignal.copy} is invoked from this app).
1006
+ * SSR-safe — returns `''` and `isSupported: false` on the server.
1007
+ *
1008
+ * Note: read access requires the Clipboard API and an active permission grant
1009
+ * in browsers that gate it. Errors from `navigator.clipboard.readText` are
1010
+ * swallowed silently to keep the signal value stable.
1011
+ */
1012
+ function clipboard(debugName = 'clipboard') {
1013
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
1014
+ typeof navigator === 'undefined' ||
1015
+ !navigator.clipboard) {
1016
+ const sig = computed(() => '', { debugName });
1017
+ sig.copy = () => Promise.resolve();
1018
+ sig.isSupported = computed(() => false, ...(ngDevMode ? [{ debugName: "isSupported" }] : /* istanbul ignore next */ []));
1019
+ return sig;
1020
+ }
1021
+ const state = signal('', { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName });
1022
+ const refresh = () => {
1023
+ navigator.clipboard.readText().then((value) => state.set(value), () => {
1024
+ // permission denied / focus required — ignore
1025
+ });
1026
+ };
1027
+ const abortController = new AbortController();
1028
+ const onCopy = () => refresh();
1029
+ document.addEventListener('copy', onCopy, { signal: abortController.signal });
1030
+ document.addEventListener('cut', onCopy, { signal: abortController.signal });
1031
+ inject(DestroyRef).onDestroy(() => abortController.abort());
1032
+ const sig = state.asReadonly();
1033
+ sig.copy = async (value) => {
1034
+ await navigator.clipboard.writeText(value);
1035
+ state.set(value);
1036
+ };
1037
+ sig.isSupported = computed(() => true, ...(ngDevMode ? [{ debugName: "isSupported" }] : /* istanbul ignore next */ []));
1038
+ return sig;
1039
+ }
1040
+
935
1041
  function observerSupported$1() {
936
1042
  return typeof ResizeObserver !== 'undefined';
937
1043
  }
@@ -1132,6 +1238,167 @@ function elementVisibility(target = inject(ElementRef), opt) {
1132
1238
  return base;
1133
1239
  }
1134
1240
 
1241
+ function unwrap$1(target) {
1242
+ if (!target)
1243
+ return null;
1244
+ return target instanceof ElementRef ? target.nativeElement : target;
1245
+ }
1246
+ /**
1247
+ * Creates a read-only signal that tracks whether the focused element is the
1248
+ * target or a descendant of it. Mirrors the CSS `:focus-within` pseudo-class.
1249
+ *
1250
+ * Defaults `target` to the current `ElementRef` so it can be used inline in a
1251
+ * component's `class` field. SSR-safe — returns a constant `false` signal on
1252
+ * the server.
1253
+ */
1254
+ function focusWithin(target = inject(ElementRef)) {
1255
+ if (isPlatformServer(inject(PLATFORM_ID))) {
1256
+ return computed(() => false, { debugName: 'focusWithin' });
1257
+ }
1258
+ const state = signal(false, { debugName: 'focusWithin' });
1259
+ const attach = (el) => {
1260
+ state.set(el.contains(document.activeElement));
1261
+ const abortController = new AbortController();
1262
+ el.addEventListener('focusin', () => state.set(true), {
1263
+ signal: abortController.signal,
1264
+ });
1265
+ el.addEventListener('focusout', () => {
1266
+ // Defer so `document.activeElement` reflects the focus move.
1267
+ queueMicrotask(() => state.set(el.contains(document.activeElement)));
1268
+ }, { signal: abortController.signal });
1269
+ return () => abortController.abort();
1270
+ };
1271
+ if (isSignal(target)) {
1272
+ const targetSig = target;
1273
+ effect((cleanup) => {
1274
+ const el = unwrap$1(targetSig());
1275
+ if (!el) {
1276
+ state.set(false);
1277
+ return;
1278
+ }
1279
+ cleanup(attach(el));
1280
+ });
1281
+ }
1282
+ else {
1283
+ const el = unwrap$1(target);
1284
+ if (el) {
1285
+ const detach = attach(el);
1286
+ inject(DestroyRef).onDestroy(detach);
1287
+ }
1288
+ }
1289
+ return state.asReadonly();
1290
+ }
1291
+
1292
+ /**
1293
+ * Creates a read-only signal that exposes the current geolocation position.
1294
+ *
1295
+ * The returned signal carries `error` and `loading` sub-signals for permission
1296
+ * failures and the in-flight initial fetch respectively. SSR-safe — on the
1297
+ * server the position is `null`, loading is `false`, and no API calls are made.
1298
+ *
1299
+ * @example
1300
+ * ```ts
1301
+ * const where = geolocation({ watch: true, enableHighAccuracy: true });
1302
+ * effect(() => console.log(where()?.coords, where.error()));
1303
+ * ```
1304
+ */
1305
+ function geolocation(opt) {
1306
+ if (isPlatformServer(inject(PLATFORM_ID)) || typeof navigator === 'undefined' || !navigator.geolocation) {
1307
+ const sig = computed(() => null, {
1308
+ debugName: opt?.debugName ?? 'geolocation',
1309
+ });
1310
+ sig.error = computed(() => null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1311
+ sig.loading = computed(() => false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1312
+ return sig;
1313
+ }
1314
+ const position = signal(null, {
1315
+ debugName: opt?.debugName ?? 'geolocation',
1316
+ });
1317
+ const error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
1318
+ const loading = signal(true, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
1319
+ const onSuccess = (p) => {
1320
+ position.set(p);
1321
+ error.set(null);
1322
+ loading.set(false);
1323
+ };
1324
+ const onError = (e) => {
1325
+ error.set(e);
1326
+ loading.set(false);
1327
+ };
1328
+ if (opt?.watch) {
1329
+ const watchId = navigator.geolocation.watchPosition(onSuccess, onError, opt);
1330
+ inject(DestroyRef).onDestroy(() => navigator.geolocation.clearWatch(watchId));
1331
+ }
1332
+ else {
1333
+ navigator.geolocation.getCurrentPosition(onSuccess, onError, opt);
1334
+ }
1335
+ const sig = position.asReadonly();
1336
+ sig.error = error.asReadonly();
1337
+ sig.loading = loading.asReadonly();
1338
+ return sig;
1339
+ }
1340
+
1341
+ const DEFAULT_EVENTS = [
1342
+ 'mousemove',
1343
+ 'keydown',
1344
+ 'touchstart',
1345
+ 'scroll',
1346
+ 'visibilitychange',
1347
+ ];
1348
+ const serverDate$1 = new Date();
1349
+ /**
1350
+ * Creates a read-only signal that flips to `true` after a window of user
1351
+ * inactivity. Any of the configured `events` (default: pointer/keyboard/scroll
1352
+ * activity) resets the timer and flips the signal back to `false`.
1353
+ *
1354
+ * SSR-safe — always `false` with a frozen `since` date on the server.
1355
+ */
1356
+ function idle(opt) {
1357
+ if (isPlatformServer(inject(PLATFORM_ID))) {
1358
+ const sig = computed(() => false, {
1359
+ debugName: opt?.debugName ?? 'idle',
1360
+ });
1361
+ sig.since = computed(() => serverDate$1, ...(ngDevMode ? [{ debugName: "since" }] : /* istanbul ignore next */ []));
1362
+ return sig;
1363
+ }
1364
+ const ms = opt?.ms ?? 60_000;
1365
+ const events = opt?.events ?? DEFAULT_EVENTS;
1366
+ const state = signal(false, { debugName: opt?.debugName ?? 'idle' });
1367
+ const since = signal(new Date(), ...(ngDevMode ? [{ debugName: "since" }] : /* istanbul ignore next */ []));
1368
+ let timer;
1369
+ const goIdle = () => {
1370
+ if (state())
1371
+ return;
1372
+ state.set(true);
1373
+ since.set(new Date());
1374
+ };
1375
+ const reset = () => {
1376
+ if (timer)
1377
+ clearTimeout(timer);
1378
+ if (state()) {
1379
+ state.set(false);
1380
+ since.set(new Date());
1381
+ }
1382
+ timer = setTimeout(goIdle, ms);
1383
+ };
1384
+ const abortController = new AbortController();
1385
+ for (const ev of events) {
1386
+ window.addEventListener(ev, reset, {
1387
+ passive: true,
1388
+ signal: abortController.signal,
1389
+ });
1390
+ }
1391
+ timer = setTimeout(goIdle, ms);
1392
+ inject(DestroyRef).onDestroy(() => {
1393
+ if (timer)
1394
+ clearTimeout(timer);
1395
+ abortController.abort();
1396
+ });
1397
+ const sig = state.asReadonly();
1398
+ sig.since = since.asReadonly();
1399
+ return sig;
1400
+ }
1401
+
1135
1402
  /**
1136
1403
  * Creates a read-only signal that reactively tracks whether a CSS media query
1137
1404
  * string currently matches.
@@ -1293,26 +1560,41 @@ function throttled(initial, opt) {
1293
1560
  */
1294
1561
  function throttle(source, opt) {
1295
1562
  const ms = opt?.ms ?? 0;
1563
+ const leading = opt?.leading ?? false;
1564
+ const trailing = opt?.trailing ?? true;
1296
1565
  const trigger = signal(false, ...(ngDevMode ? [{ debugName: "trigger" }] : /* istanbul ignore next */ []));
1566
+ const fire = () => trigger.update((c) => !c);
1297
1567
  let timeout;
1568
+ let pendingTrailing = false;
1298
1569
  try {
1299
1570
  const destroyRef = opt?.destroyRef ?? inject(DestroyRef, { optional: true });
1300
1571
  destroyRef?.onDestroy(() => {
1301
1572
  if (timeout)
1302
1573
  clearTimeout(timeout);
1303
1574
  timeout = undefined;
1575
+ pendingTrailing = false;
1304
1576
  });
1305
1577
  }
1306
1578
  catch {
1307
1579
  // not in injection context & no destroyRef provided opting out of cleanup
1308
1580
  }
1309
1581
  const tick = () => {
1310
- if (timeout)
1582
+ if (!timeout) {
1583
+ if (leading)
1584
+ fire();
1585
+ else
1586
+ pendingTrailing = trailing;
1587
+ timeout = setTimeout(() => {
1588
+ timeout = undefined;
1589
+ if (trailing && pendingTrailing) {
1590
+ pendingTrailing = false;
1591
+ fire();
1592
+ }
1593
+ }, ms);
1311
1594
  return;
1312
- timeout = setTimeout(() => {
1313
- trigger.update((c) => !c);
1314
- timeout = undefined;
1315
- }, ms);
1595
+ }
1596
+ if (trailing)
1597
+ pendingTrailing = true;
1316
1598
  };
1317
1599
  const set = (value) => {
1318
1600
  source.set(value);
@@ -1463,6 +1745,35 @@ function networkStatus(debugName = 'networkStatus') {
1463
1745
  return sig;
1464
1746
  }
1465
1747
 
1748
+ const SSR_FALLBACK = {
1749
+ angle: 0,
1750
+ type: 'portrait-primary',
1751
+ };
1752
+ /**
1753
+ * Creates a read-only signal that tracks `screen.orientation`.
1754
+ *
1755
+ * SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
1756
+ * and in environments without `screen.orientation` support.
1757
+ */
1758
+ function orientation(debugName = 'orientation') {
1759
+ if (isPlatformServer(inject(PLATFORM_ID)) ||
1760
+ typeof screen === 'undefined' ||
1761
+ !screen.orientation) {
1762
+ return computed(() => SSR_FALLBACK, { debugName });
1763
+ }
1764
+ const so = screen.orientation;
1765
+ const read = () => ({
1766
+ angle: so.angle,
1767
+ type: so.type,
1768
+ });
1769
+ const state = signal(read(), { ...(ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {}), debugName,
1770
+ equal: (a, b) => a.angle === b.angle && a.type === b.type });
1771
+ const onChange = () => state.set(read());
1772
+ so.addEventListener('change', onChange);
1773
+ inject(DestroyRef).onDestroy(() => so.removeEventListener('change', onChange));
1774
+ return state.asReadonly();
1775
+ }
1776
+
1466
1777
  /**
1467
1778
  * Creates a read-only signal that tracks the page's visibility state.
1468
1779
  *
@@ -1682,35 +1993,42 @@ function windowSize(opt) {
1682
1993
  * @internal
1683
1994
  */
1684
1995
  function sensor(type, options) {
1996
+ const opts = options;
1685
1997
  switch (type) {
1686
1998
  case 'mousePosition':
1687
- return mousePosition(options);
1999
+ return mousePosition(opts);
1688
2000
  case 'networkStatus':
1689
- return networkStatus(options?.debugName);
2001
+ return networkStatus(opts?.debugName);
1690
2002
  case 'pageVisibility':
1691
- return pageVisibility(options?.debugName);
2003
+ return pageVisibility(opts?.debugName);
1692
2004
  case 'darkMode':
1693
2005
  case 'dark-mode':
1694
- return prefersDarkMode(options?.debugName);
2006
+ return prefersDarkMode(opts?.debugName);
1695
2007
  case 'reducedMotion':
1696
2008
  case 'reduced-motion':
1697
- return prefersReducedMotion(options?.debugName);
1698
- case 'mediaQuery': {
1699
- const opt = options;
1700
- return mediaQuery(opt.query, opt.debugName);
1701
- }
2009
+ return prefersReducedMotion(opts?.debugName);
2010
+ case 'mediaQuery':
2011
+ return mediaQuery(opts.query, opts.debugName);
1702
2012
  case 'windowSize':
1703
- return windowSize(options);
2013
+ return windowSize(opts);
1704
2014
  case 'scrollPosition':
1705
- return scrollPosition(options);
1706
- case 'elementVisibility': {
1707
- const opt = options;
1708
- return elementVisibility(opt.target, opt);
1709
- }
1710
- case 'elementSize': {
1711
- const opt = options;
1712
- return elementSize(opt.target, opt);
1713
- }
2015
+ return scrollPosition(opts);
2016
+ case 'elementVisibility':
2017
+ return elementVisibility(opts?.target, opts);
2018
+ case 'elementSize':
2019
+ return elementSize(opts?.target, opts);
2020
+ case 'geolocation':
2021
+ return geolocation(opts);
2022
+ case 'clipboard':
2023
+ return clipboard(opts?.debugName);
2024
+ case 'orientation':
2025
+ return orientation(opts?.debugName);
2026
+ case 'batteryStatus':
2027
+ return batteryStatus(opts?.debugName);
2028
+ case 'idle':
2029
+ return idle(opts);
2030
+ case 'focusWithin':
2031
+ return focusWithin(opts?.target);
1714
2032
  default:
1715
2033
  throw new Error(`Unknown sensor type: ${type}`);
1716
2034
  }
@@ -1722,6 +2040,49 @@ function sensors(track, opt) {
1722
2040
  }, {});
1723
2041
  }
1724
2042
 
2043
+ function unwrap(t) {
2044
+ if (!t)
2045
+ return null;
2046
+ return t instanceof ElementRef ? t.nativeElement : t;
2047
+ }
2048
+ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2049
+ const project = typeof projectOrOpt === 'function' ? projectOrOpt : undefined;
2050
+ const opt = typeof projectOrOpt === 'function' ? maybeOpt : projectOrOpt;
2051
+ const injector = opt?.injector ?? inject(Injector);
2052
+ if (isPlatformServer(injector.get(PLATFORM_ID))) {
2053
+ return computed(() => initial, { debugName: opt?.debugName });
2054
+ }
2055
+ const state = signal(initial, {
2056
+ debugName: opt?.debugName,
2057
+ });
2058
+ const handler = (event) => {
2059
+ if (project)
2060
+ state.set(project(event));
2061
+ else
2062
+ state.set(event);
2063
+ };
2064
+ const { destroyRef: providedDestroyRef, ...listenerOpts } = opt ?? {};
2065
+ if (isSignal(target)) {
2066
+ const targetSig = target;
2067
+ effect((cleanup) => {
2068
+ const resolved = unwrap(targetSig());
2069
+ if (!resolved)
2070
+ return;
2071
+ resolved.addEventListener(eventName, handler, listenerOpts);
2072
+ cleanup(() => resolved.removeEventListener(eventName, handler, listenerOpts));
2073
+ }, { injector });
2074
+ }
2075
+ else {
2076
+ const resolved = unwrap(target);
2077
+ if (resolved) {
2078
+ resolved.addEventListener(eventName, handler, listenerOpts);
2079
+ const destroyRef = providedDestroyRef ?? injector.get(DestroyRef);
2080
+ destroyRef.onDestroy(() => resolved.removeEventListener(eventName, handler, listenerOpts));
2081
+ }
2082
+ }
2083
+ return untracked(() => state.asReadonly());
2084
+ }
2085
+
1725
2086
  const IS_STORE = Symbol('MMSTACK::IS_STORE');
1726
2087
  const PROXY_CACHE = new WeakMap();
1727
2088
  const SIGNAL_FN_PROP = new Set([
@@ -2212,14 +2573,11 @@ function generateDeterministicID() {
2212
2573
  *
2213
2574
  * @template T - The type of the WritableSignal
2214
2575
  * @param sig - The WritableSignal to synchronize across tabs
2215
- * @param opt - Optional configuration object
2216
- * @param opt.id - Explicit channel ID for synchronization. If not provided,
2217
- * a deterministic ID is generated based on the call site.
2218
- * Use explicit IDs in production for reliability.
2576
+ * @param opt - configuration object
2577
+ * @param opt.id - Explicit channel ID for synchronization.
2219
2578
  *
2220
2579
  * @returns The same WritableSignal instance, now synchronized across tabs
2221
2580
  *
2222
- * @throws {Error} When deterministic ID generation fails and no explicit ID is provided
2223
2581
  *
2224
2582
  * @example
2225
2583
  * ```typescript
@@ -2243,7 +2601,7 @@ function generateDeterministicID() {
2243
2601
  function tabSync(sig, opt) {
2244
2602
  if (isPlatformServer(inject(PLATFORM_ID)))
2245
2603
  return sig;
2246
- const id = opt?.id || generateDeterministicID();
2604
+ const id = typeof opt === 'string' ? opt : (opt?.id ?? generateDeterministicID());
2247
2605
  const bus = inject(MessageBus);
2248
2606
  const { unsub, post } = bus.subscribe(id, (next) => sig.set(next));
2249
2607
  let first = false;
@@ -2375,8 +2733,13 @@ function getSignalEquality(sig) {
2375
2733
  * console.log('Can undo:', name.canUndo()); // false
2376
2734
  * ```
2377
2735
  */
2378
- function withHistory(source, opt) {
2379
- const equal = opt?.equal ?? getSignalEquality(source);
2736
+ function withHistory(sourceOrValue, opt) {
2737
+ const equal = (opt?.equal ?? isSignal(sourceOrValue))
2738
+ ? getSignalEquality(sourceOrValue)
2739
+ : Object.is;
2740
+ const source = isSignal(sourceOrValue)
2741
+ ? sourceOrValue
2742
+ : signal(sourceOrValue);
2380
2743
  const maxSize = opt?.maxSize ?? Infinity;
2381
2744
  const history = mutable([], {
2382
2745
  ...opt,
@@ -2384,20 +2747,22 @@ function withHistory(source, opt) {
2384
2747
  });
2385
2748
  const redoArray = mutable([]);
2386
2749
  const originalSet = source.set;
2750
+ const trim = (arr) => {
2751
+ if (arr.length < maxSize)
2752
+ return arr;
2753
+ if (opt?.cleanupStrategy === 'shift') {
2754
+ arr.shift();
2755
+ return arr;
2756
+ }
2757
+ return arr.slice(Math.floor(maxSize / 2));
2758
+ };
2387
2759
  const set = (value) => {
2388
2760
  const current = untracked(source);
2389
2761
  if (equal(value, current))
2390
2762
  return;
2391
2763
  source.set(value);
2392
2764
  history.mutate((c) => {
2393
- if (c.length >= maxSize) {
2394
- if (opt?.cleanupStrategy === 'shift') {
2395
- c.shift();
2396
- }
2397
- else {
2398
- c = c.slice(Math.floor(maxSize / 2));
2399
- }
2400
- }
2765
+ c = trim(c);
2401
2766
  c.push(current);
2402
2767
  return c;
2403
2768
  });
@@ -2421,7 +2786,11 @@ function withHistory(source, opt) {
2421
2786
  return;
2422
2787
  originalSet.call(source, valueToRestore);
2423
2788
  history.inline((h) => h.pop());
2424
- redoArray.inline((r) => r.push(valueForRedo));
2789
+ redoArray.mutate((r) => {
2790
+ r = trim(r);
2791
+ r.push(valueForRedo);
2792
+ return r;
2793
+ });
2425
2794
  };
2426
2795
  internal.redo = () => {
2427
2796
  const redoStack = untracked(redoArray);
@@ -2434,14 +2803,7 @@ function withHistory(source, opt) {
2434
2803
  originalSet.call(source, valueToRestore);
2435
2804
  redoArray.inline((r) => r.pop());
2436
2805
  history.mutate((h) => {
2437
- if (h.length >= maxSize) {
2438
- if (opt?.cleanupStrategy === 'shift') {
2439
- h.shift();
2440
- }
2441
- else {
2442
- h = h.slice(Math.floor(maxSize / 2));
2443
- }
2444
- }
2806
+ h = trim(h);
2445
2807
  h.push(valueForUndo);
2446
2808
  return h;
2447
2809
  });
@@ -2460,5 +2822,5 @@ function withHistory(source, opt) {
2460
2822
  * Generated bundle index. Do not edit.
2461
2823
  */
2462
2824
 
2463
- export { chunked, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, pageVisibility, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scrollPosition, select, sensor, sensors, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2825
+ export { batteryStatus, chunked, clipboard, combineWith, debounce, debounced, derived, distinct, elementSize, elementVisibility, filter, filterWith, focusWithin, geolocation, idle, indexArray, isDerivation, isMutable, isStore, keyArray, map, mapArray, mapObject, mediaQuery, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, orientation, pageVisibility, pairwise, pipeable, piped, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
2464
2826
  //# sourceMappingURL=mmstack-primitives.mjs.map