@mmstack/primitives 19.3.5 → 19.3.6

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