@mmstack/primitives 21.0.23 → 21.0.25

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.
@@ -397,37 +397,158 @@ function isMutable(value) {
397
397
  return 'mutate' in value && typeof value.mutate === 'function';
398
398
  }
399
399
 
400
+ /**
401
+ * @internal
402
+ * Type guard for an array-index-like property key: a non-empty string that parses to a finite
403
+ * number (e.g. `'0'`, `'42'`). Used to choose array-vs-object shape during autovivification and
404
+ * deep store proxying.
405
+ */
406
+ function isIndexProp(prop) {
407
+ return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
408
+ }
409
+
410
+ // Container resolvers used by createVivify: each returns the current value when present and
411
+ // only creates a new container when it is null/undefined.
412
+ function identity(x) {
413
+ return x;
414
+ }
415
+ function createArray(cur) {
416
+ if (cur === null || cur === undefined)
417
+ return [];
418
+ return cur;
419
+ }
420
+ function createObject(cur) {
421
+ if (cur === null || cur === undefined)
422
+ return {};
423
+ return cur;
424
+ }
425
+ function createAuto(cur, key) {
426
+ if (cur === null || cur === undefined) {
427
+ return typeof key === 'number' || isIndexProp(key)
428
+ ? []
429
+ : {};
430
+ }
431
+ return cur;
432
+ }
433
+ /**
434
+ * @internal
435
+ * Resolves a {@link Vivify} option into a {@link VivifyFn}. The returned function leaves a
436
+ * present value untouched and only creates a new container — object, array, or factory result —
437
+ * when the current value is `null`/`undefined`.
438
+ */
439
+ function createVivify(option) {
440
+ switch (option) {
441
+ case false:
442
+ return identity;
443
+ case 'array':
444
+ return createArray;
445
+ case 'object':
446
+ return createObject;
447
+ case 'auto':
448
+ case true:
449
+ return createAuto;
450
+ default:
451
+ return typeof option === 'function'
452
+ ? (cur) => cur === null || cur === undefined ? option() : cur
453
+ : identity;
454
+ }
455
+ }
456
+
457
+ function createMutableArrayUpdater(source, index, vivifyFn) {
458
+ return (next) => source.mutate((cur) => {
459
+ const vivified = vivifyFn(cur, index);
460
+ if (vivified === null || vivified === undefined)
461
+ return vivified;
462
+ vivified[index] = next;
463
+ return vivified;
464
+ });
465
+ }
466
+ function createImmutableArrayUpdater(source, index, vivifyFn) {
467
+ return (next) => source.update((cur) => {
468
+ const vivified = vivifyFn(cur, index)?.slice();
469
+ if (vivified === null || vivified === undefined)
470
+ return vivified;
471
+ vivified[index] = next;
472
+ return vivified;
473
+ });
474
+ }
475
+ function createMutableObjectUpdater(source, key, vivifyFn) {
476
+ return (next) => source.mutate((cur) => {
477
+ const vivified = vivifyFn(cur, key);
478
+ if (vivified === null || vivified === undefined)
479
+ return vivified;
480
+ vivified[key] = next;
481
+ return vivified;
482
+ });
483
+ }
484
+ function createImmutableObjectUpdater(source, key, vivifyFn) {
485
+ return (next) => source.update((cur) => {
486
+ const vivified = vivifyFn(cur, key);
487
+ if (vivified === null || vivified === undefined)
488
+ return vivified;
489
+ return { ...vivified, [key]: next };
490
+ });
491
+ }
492
+ function createUpdater(source, key, vivify) {
493
+ const sample = untracked(source);
494
+ // fast path for when vivification is off
495
+ if (!vivify) {
496
+ if (Array.isArray(sample) && typeof key === 'number') {
497
+ const idx = key;
498
+ return isMutable(source)
499
+ ? (next) => source.mutate((cur) => {
500
+ cur[idx] = next;
501
+ return cur;
502
+ })
503
+ : (next) => source.update((cur) => {
504
+ const copy = cur.slice();
505
+ copy[idx] = next;
506
+ return copy;
507
+ });
508
+ }
509
+ return isMutable(source)
510
+ ? (next) => source.mutate((cur) => {
511
+ cur[key] = next;
512
+ return cur;
513
+ })
514
+ : (next) => source.update((cur) => ({
515
+ ...cur,
516
+ [key]: next,
517
+ }));
518
+ }
519
+ const present = sample !== null && sample !== undefined;
520
+ const keyIsIndex = typeof key === 'number' || isIndexProp(key);
521
+ let vivifyOpt = vivify;
522
+ if (vivifyOpt === 'auto' || vivifyOpt === true) {
523
+ vivifyOpt = ((present ? Array.isArray(sample) : keyIsIndex) ? 'array' : 'object');
524
+ }
525
+ const vivifyFn = createVivify(vivifyOpt);
526
+ // Route to the array updater whenever the container is (or will be vivified as) an
527
+ // array, so the updater and the created container agree on shape for a nullish source.
528
+ const isArray = vivifyOpt === 'array'
529
+ ? keyIsIndex
530
+ : vivifyOpt === 'object'
531
+ ? false
532
+ : Array.isArray(sample) && typeof key === 'number';
533
+ if (isArray)
534
+ return isMutable(source)
535
+ ? createMutableArrayUpdater(source, key, vivifyFn)
536
+ : createImmutableArrayUpdater(source, key, vivifyFn);
537
+ return isMutable(source)
538
+ ? createMutableObjectUpdater(source, key, vivifyFn)
539
+ : createImmutableObjectUpdater(source, key, vivifyFn);
540
+ }
400
541
  function derived(source, optOrKey, opt) {
401
- const isArray = Array.isArray(untracked(source)) && typeof optOrKey === 'number';
402
- const from = typeof optOrKey === 'object' ? optOrKey.from : (v) => v[optOrKey];
542
+ const vivify = typeof optOrKey === 'object' ? false : (opt?.vivify ?? false);
543
+ // With vivification the source may legitimately be null/undefined
544
+ const from = typeof optOrKey === 'object'
545
+ ? optOrKey.from
546
+ : vivify
547
+ ? (v) => v?.[optOrKey]
548
+ : (v) => v[optOrKey];
403
549
  const onChange = typeof optOrKey === 'object'
404
550
  ? optOrKey.onChange
405
- : isArray
406
- ? isMutable(source)
407
- ? (next) => {
408
- source.mutate((cur) => {
409
- cur[optOrKey] = next;
410
- return cur;
411
- });
412
- }
413
- : (next) => {
414
- source.update((cur) => {
415
- const newArray = [...cur];
416
- newArray[optOrKey] = next;
417
- return newArray;
418
- });
419
- }
420
- : isMutable(source)
421
- ? (next) => {
422
- source.mutate((cur) => {
423
- cur[optOrKey] =
424
- next;
425
- return cur;
426
- });
427
- }
428
- : (next) => {
429
- source.update((cur) => ({ ...cur, [optOrKey]: next }));
430
- };
551
+ : createUpdater(source, optOrKey, vivify);
431
552
  const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
432
553
  const baseEqual = rest?.equal ?? Object.is;
433
554
  let cnt = 0;
@@ -600,7 +721,30 @@ const mapArray = indexArray;
600
721
  * @param mapFn The mapping function. Receives the item and its index as a Signal.
601
722
  * @param options Optional configuration:
602
723
  * - `onDestroy`: A callback invoked when a mapped item is removed from the array.
724
+ * - `key`: A custom key extractor for identity matching (e.g. `(item) => item.id`)
725
+ * when item references change but conceptual identity is preserved.
603
726
  * @returns A `Signal<U[]>` containing the mapped array.
727
+ *
728
+ * @example
729
+ * ```ts
730
+ * const users = signal([
731
+ * { id: 1, name: 'Alice' },
732
+ * { id: 2, name: 'Bob' },
733
+ * ]);
734
+ *
735
+ * const rows = keyArray(
736
+ * users,
737
+ * (user, index) => ({
738
+ * label: computed(() => `#${index()} ${user.name}`),
739
+ * id: user.id,
740
+ * }),
741
+ * { key: (u) => u.id },
742
+ * );
743
+ *
744
+ * // Reordering users() rebuilds index signals only — `rows` entries
745
+ * // are matched by id and reused, not re-created.
746
+ * users.set([users()[1], users()[0]]);
747
+ * ```
604
748
  */
605
749
  function keyArray(source, mapFn, options = {}) {
606
750
  const sourceSignal = isSignal(source) ? source : computed(source);
@@ -757,15 +901,69 @@ function mapObject(source, mapFn, options = {}) {
757
901
  }).asReadonly();
758
902
  }
759
903
 
760
- /** Project with optional equality. Pure & sync. */
904
+ /**
905
+ * Synchronous projection of a signal value with optional `CreateSignalOptions`
906
+ * (custom `equal`, `debugName`, etc.). Equivalent to `map` plus the ability to
907
+ * pass signal options through to the underlying `computed()`.
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * const user = piped({ id: 1, name: 'Alice' });
912
+ * const name = user.pipe(select((u) => u.name));
913
+ * name(); // 'Alice'
914
+ * ```
915
+ */
761
916
  const select = (projector, opt) => (src) => computed(() => projector(src()), opt);
762
- /** Combine with another signal using a projector. */
917
+ /**
918
+ * Combine the piped signal with another `Signal` using a projector. The result
919
+ * recomputes whenever either source changes.
920
+ *
921
+ * @example
922
+ * ```ts
923
+ * const price = piped(10);
924
+ * const quantity = signal(3);
925
+ * const total = price.pipe(combineWith(quantity, (p, q) => p * q));
926
+ * total(); // 30
927
+ * ```
928
+ */
763
929
  const combineWith = (other, project, opt) => (src) => computed(() => project(src(), other()), opt);
764
- /** Only re-emit when equal(prev, next) is false. */
930
+ /**
931
+ * Suppress emissions while consecutive values are considered equal. The
932
+ * comparator defaults to `Object.is`; pass a custom one for structural or
933
+ * key-based equality (e.g. compare by `id` only).
934
+ *
935
+ * @example
936
+ * ```ts
937
+ * const user = piped({ id: 1, lastSeen: Date.now() });
938
+ * const byId = user.pipe(distinct((a, b) => a.id === b.id));
939
+ * // byId only re-emits when `id` changes, not on every `lastSeen` update
940
+ * ```
941
+ */
765
942
  const distinct = (equal = Object.is) => (src) => computed(() => src(), { equal });
766
- /** map to new value */
943
+ /**
944
+ * Pure synchronous transform from input to output. Equivalent to a `computed()`
945
+ * that reads the source and returns `fn(value)`.
946
+ *
947
+ * @example
948
+ * ```ts
949
+ * const count = piped(2);
950
+ * const doubled = count.pipe(map((n) => n * 2));
951
+ * doubled(); // 4
952
+ * ```
953
+ */
767
954
  const map = (fn) => (src) => computed(() => fn(src()));
768
- /** filter values, keeping the last value if it was ever available, if first value is filtered will return undefined */
955
+ /**
956
+ * Keep only values that pass the predicate. The result holds the last passing
957
+ * value across emissions; before any value passes, the result is `undefined` —
958
+ * see {@link filterWith} when you need a non-`undefined` seed.
959
+ *
960
+ * @example
961
+ * ```ts
962
+ * const event = piped<MouseEvent | null>(null);
963
+ * const clicks = event.pipe(filter((e): e is MouseEvent => e?.type === 'click'));
964
+ * clicks(); // undefined until a click happens, then the last MouseEvent
965
+ * ```
966
+ */
769
967
  const filter = (predicate) => (src) => linkedSignal({
770
968
  source: src,
771
969
  computation: (next, prev) => {
@@ -774,7 +972,19 @@ const filter = (predicate) => (src) => linkedSignal({
774
972
  return prev?.source;
775
973
  },
776
974
  });
777
- /** tap into the value */
975
+ /**
976
+ * Run a side effect on every emission without altering the signal value. Wraps
977
+ * Angular's `effect()`, so it must run in an injection context or receive an
978
+ * explicit `injector`. Use for logging / analytics — not for setting other
979
+ * signals (that's what regular `effect()` is for).
980
+ *
981
+ * @example
982
+ * ```ts
983
+ * const count = piped(0);
984
+ * count.pipe(tap((n) => console.log('count:', n)));
985
+ * count.set(1); // logs 'count: 1'
986
+ * ```
987
+ */
778
988
  const tap = (fn, injector) => (src) => {
779
989
  effect(() => fn(src()), {
780
990
  injector,
@@ -782,24 +992,66 @@ const tap = (fn, injector) => (src) => {
782
992
  return src;
783
993
  };
784
994
  /**
785
- * Like {@link filter}, but emits `initial` until a value passes the predicate
786
- * for the first time. Avoids the `T | undefined` return type.
995
+ * Like {@link filter}, but emits `initial` until a value first passes the
996
+ * predicate. Eliminates the `T | undefined` return type at the cost of an
997
+ * explicit seed value.
998
+ *
999
+ * @example
1000
+ * ```ts
1001
+ * const event = piped<MouseEvent | null>(null);
1002
+ * const lastClick = event.pipe(filterWith((e) => e?.type === 'click', null));
1003
+ * lastClick(); // null until the first click, then the most recent click event
1004
+ * ```
787
1005
  */
788
1006
  const filterWith = (predicate, initial) => (src) => linkedSignal({
789
1007
  source: src,
790
1008
  computation: (next, prev) => predicate(next) ? next : (prev?.value ?? initial),
791
1009
  });
792
- /** Emits `initial` first, then mirrors source. */
1010
+ /**
1011
+ * Emit `initial` on the first read, then mirror the source on every subsequent
1012
+ * read. Useful for giving a pipeline a sensible seed value before the source
1013
+ * is ready (e.g. loading state).
1014
+ *
1015
+ * @example
1016
+ * ```ts
1017
+ * const data = piped<User | null>(null);
1018
+ * const view = data.pipe(startWith<User | null, 'loading'>('loading'));
1019
+ * view(); // 'loading' on first read, then User | null afterward
1020
+ * ```
1021
+ */
793
1022
  const startWith = (initial) => (src) => linkedSignal({
794
1023
  source: src,
795
1024
  computation: (next, prev) => (prev === undefined ? initial : next),
796
1025
  });
797
- /** Emits `[prev, curr]` pairs. The first emission has prev = undefined. */
1026
+ /**
1027
+ * Emit `[prev, curr]` tuples so consumers can react to transitions instead of
1028
+ * raw values. On the first emission `prev` is `undefined`.
1029
+ *
1030
+ * @example
1031
+ * ```ts
1032
+ * const count = piped(0);
1033
+ * const delta = count.pipe(pairwise(), map(([prev, curr]) => curr - (prev ?? 0)));
1034
+ * count.set(5);
1035
+ * delta(); // 5
1036
+ * ```
1037
+ */
798
1038
  const pairwise = () => (src) => linkedSignal({
799
1039
  source: src,
800
1040
  computation: (next, prev) => [prev?.source, next],
801
1041
  });
802
- /** Reduce-like accumulator across emissions. */
1042
+ /**
1043
+ * Reduce-like accumulator that folds each emission into a running result.
1044
+ * Behaves like `Array.prototype.reduce` but applied over time, with the
1045
+ * accumulator persisted across emissions.
1046
+ *
1047
+ * @example
1048
+ * ```ts
1049
+ * const delta = piped(0);
1050
+ * const total = delta.pipe(scan((acc, n) => acc + n, 0));
1051
+ * delta.set(5); // total() === 5
1052
+ * delta.set(3); // total() === 8
1053
+ * ```
1054
+ */
803
1055
  const scan = (reducer, seed) => (src) => linkedSignal({
804
1056
  source: src,
805
1057
  computation: (next, prev) => reducer(prev?.value ?? seed, next),
@@ -968,6 +1220,15 @@ const EVENTS = [
968
1220
  * Battery Status API. Returns `null` until the underlying `getBattery()`
969
1221
  * promise resolves, or permanently when the API is unsupported (Firefox /
970
1222
  * Safari at the time of writing). SSR-safe.
1223
+ *
1224
+ * @example
1225
+ * ```ts
1226
+ * const battery = batteryStatus();
1227
+ * effect(() => {
1228
+ * const b = battery();
1229
+ * if (b) console.log(`${Math.round(b.level * 100)}% • charging: ${b.charging}`);
1230
+ * });
1231
+ * ```
971
1232
  */
972
1233
  function batteryStatus(debugName = 'batteryStatus') {
973
1234
  if (isPlatformServer(inject(PLATFORM_ID)) ||
@@ -1250,6 +1511,15 @@ function unwrap$1(target) {
1250
1511
  * Defaults `target` to the current `ElementRef` so it can be used inline in a
1251
1512
  * component's `class` field. SSR-safe — returns a constant `false` signal on
1252
1513
  * the server.
1514
+ *
1515
+ * @example
1516
+ * ```ts
1517
+ * @Component({ ... })
1518
+ * class MenuComponent {
1519
+ * // Defaults to the host element — flips true when focus is inside.
1520
+ * readonly hasFocus = focusWithin();
1521
+ * }
1522
+ * ```
1253
1523
  */
1254
1524
  function focusWithin(target = inject(ElementRef)) {
1255
1525
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1352,6 +1622,14 @@ const serverDate$1 = new Date();
1352
1622
  * activity) resets the timer and flips the signal back to `false`.
1353
1623
  *
1354
1624
  * SSR-safe — always `false` with a frozen `since` date on the server.
1625
+ *
1626
+ * @example
1627
+ * ```ts
1628
+ * const isAway = idle({ ms: 30_000 });
1629
+ * effect(() => {
1630
+ * if (isAway()) console.log('idle since', isAway.since());
1631
+ * });
1632
+ * ```
1355
1633
  */
1356
1634
  function idle(opt) {
1357
1635
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1715,6 +1993,14 @@ const serverDate = new Date();
1715
1993
  *
1716
1994
  * @param debugName Optional debug name for the signal.
1717
1995
  * @returns A `NetworkStatusSignal` instance.
1996
+ *
1997
+ * @example
1998
+ * ```ts
1999
+ * const online = networkStatus();
2000
+ * effect(() => {
2001
+ * if (!online()) console.log('offline since', online.since());
2002
+ * });
2003
+ * ```
1718
2004
  */
1719
2005
  function networkStatus(debugName = 'networkStatus') {
1720
2006
  if (isPlatformServer(inject(PLATFORM_ID))) {
@@ -1754,6 +2040,15 @@ const SSR_FALLBACK = {
1754
2040
  *
1755
2041
  * SSR-safe — returns a constant `portrait-primary / 0°` signal on the server
1756
2042
  * and in environments without `screen.orientation` support.
2043
+ *
2044
+ * @example
2045
+ * ```ts
2046
+ * const screenOrientation = orientation();
2047
+ * effect(() => {
2048
+ * const { type, angle } = screenOrientation();
2049
+ * console.log(`${type} at ${angle}°`);
2050
+ * });
2051
+ * ```
1757
2052
  */
1758
2053
  function orientation(debugName = 'orientation') {
1759
2054
  if (isPlatformServer(inject(PLATFORM_ID)) ||
@@ -2033,6 +2328,27 @@ function sensor(type, options) {
2033
2328
  throw new Error(`Unknown sensor type: ${type}`);
2034
2329
  }
2035
2330
  }
2331
+ /**
2332
+ * Bulk sensor factory — creates several sensor signals at once and returns
2333
+ * them keyed by sensor type. Convenient when a single consumer needs to react
2334
+ * to multiple browser signals; for a single sensor prefer {@link sensor}
2335
+ * directly.
2336
+ *
2337
+ * @typeParam TType The union of sensor keys being requested.
2338
+ * @param track Array of sensor type keys to create.
2339
+ * @param opt Optional per-sensor options keyed by sensor type.
2340
+ * @returns A record `{ [key]: <SensorReturnType> }` for each requested key.
2341
+ *
2342
+ * @example
2343
+ * ```ts
2344
+ * const { windowSize, networkStatus } = sensors(
2345
+ * ['windowSize', 'networkStatus'],
2346
+ * { windowSize: { throttle: 200 } },
2347
+ * );
2348
+ *
2349
+ * effect(() => console.log(windowSize(), networkStatus()));
2350
+ * ```
2351
+ */
2036
2352
  function sensors(track, opt) {
2037
2353
  return track.reduce((result, key) => {
2038
2354
  result[key] = sensor(key, opt?.[key]);
@@ -2084,6 +2400,12 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2084
2400
  }
2085
2401
 
2086
2402
  const IS_STORE = Symbol('MMSTACK::IS_STORE');
2403
+ const SCOPE_PARENT = Symbol('MMSTACK::SCOPE_PARENT');
2404
+ /**
2405
+ * @internal
2406
+ * Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
2407
+ * Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
2408
+ */
2087
2409
  const PROXY_CACHE = new WeakMap();
2088
2410
  const SIGNAL_FN_PROP = new Set([
2089
2411
  'set',
@@ -2092,6 +2414,11 @@ const SIGNAL_FN_PROP = new Set([
2092
2414
  'inline',
2093
2415
  'asReadonly',
2094
2416
  ]);
2417
+ /**
2418
+ * @internal
2419
+ * Test-only handle on the finalization registry (deliberately NOT re-exported from the public
2420
+ * barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
2421
+ */
2095
2422
  const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
2096
2423
  const storeCache = PROXY_CACHE.get(target);
2097
2424
  if (storeCache)
@@ -2106,20 +2433,35 @@ function isStore(value) {
2106
2433
  value !== null &&
2107
2434
  value[IS_STORE] === true);
2108
2435
  }
2109
- function isIndexProp(prop) {
2110
- return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
2111
- }
2112
2436
  function isRecord(value) {
2113
2437
  if (value === null || typeof value !== 'object')
2114
2438
  return false;
2115
2439
  const proto = Object.getPrototypeOf(value);
2116
2440
  return proto === Object.prototype || proto === null;
2117
2441
  }
2442
+ /**
2443
+ * @internal
2444
+ * Resolves the vivify shape for a node from its current value: a present record/array is a
2445
+ * certainty we keep (cached in the derivation, so it survives the value being nulled); an
2446
+ * unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
2447
+ */
2448
+ function resolveVivify(sample, option) {
2449
+ if (!option)
2450
+ return false;
2451
+ if (Array.isArray(sample))
2452
+ return 'array';
2453
+ if (isRecord(sample))
2454
+ return 'object';
2455
+ return 'auto';
2456
+ }
2457
+ function hasOwnKey(value, key) {
2458
+ return value != null && Object.hasOwn(value, key);
2459
+ }
2118
2460
  /**
2119
2461
  * @internal
2120
2462
  * Makes an array store
2121
2463
  */
2122
- function toArrayStore(source, injector) {
2464
+ function toArrayStore(source, injector, vivify) {
2123
2465
  if (isStore(source))
2124
2466
  return source;
2125
2467
  const isMutableSource = isMutable(source);
@@ -2199,31 +2541,39 @@ function toArrayStore(source, injector) {
2199
2541
  const value = untracked(target);
2200
2542
  const valueIsArray = Array.isArray(value);
2201
2543
  const valueIsRecord = isRecord(value);
2544
+ const nodeVivify = resolveVivify(value, vivify);
2545
+ const vivifyFn = createVivify(nodeVivify);
2202
2546
  const equalFn = (valueIsRecord || valueIsArray) &&
2203
2547
  isMutableSource &&
2204
2548
  typeof value[idx] === 'object'
2205
2549
  ? () => false
2206
2550
  : undefined;
2207
2551
  const computation = valueIsRecord
2208
- ? derived(target, idx, { equal: equalFn })
2552
+ ? derived(target, idx, {
2553
+ equal: equalFn,
2554
+ vivify: nodeVivify,
2555
+ })
2209
2556
  : derived(target, {
2210
2557
  from: (v) => v?.[idx],
2211
2558
  onChange: (newValue) => target.update((v) => {
2212
- if (v === null || v === undefined)
2213
- return v;
2559
+ const container = vivifyFn(v, idx);
2560
+ if (container === null || container === undefined)
2561
+ return container;
2214
2562
  try {
2215
- v[idx] = newValue;
2563
+ container[idx] = newValue;
2216
2564
  }
2217
2565
  catch (e) {
2218
2566
  if (isDevMode())
2219
2567
  console.error(`[store] Failed to set property "${String(idx)}"`, e);
2220
2568
  }
2221
- return v;
2569
+ return container;
2222
2570
  }),
2223
2571
  });
2224
- const proxy = Array.isArray(untracked(computation))
2225
- ? toArrayStore(computation, injector)
2226
- : toStore(computation, injector);
2572
+ const childSample = untracked(computation);
2573
+ const childVivify = resolveVivify(childSample, vivify);
2574
+ const proxy = Array.isArray(childSample)
2575
+ ? toArrayStore(computation, injector, childVivify)
2576
+ : toStore(computation, injector, childVivify);
2227
2577
  const ref = new WeakRef(proxy);
2228
2578
  storeCache.set(idx, ref);
2229
2579
  PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
@@ -2240,7 +2590,7 @@ function toArrayStore(source, injector) {
2240
2590
  * const state = store({ user: { name: 'John' } });
2241
2591
  * const nameSignal = state.user.name; // WritableSignal<string>
2242
2592
  */
2243
- function toStore(source, injector) {
2593
+ function toStore(source, injector, vivify = false) {
2244
2594
  if (isStore(source))
2245
2595
  return source;
2246
2596
  if (!injector)
@@ -2250,7 +2600,8 @@ function toStore(source, injector) {
2250
2600
  : toWritable(source, () => {
2251
2601
  // noop
2252
2602
  });
2253
- const isMutableSource = isMutable(writableSource);
2603
+ const isWritableSource = isWritableSignal(source);
2604
+ const isMutableSource = isWritableSource && isMutable(writableSource);
2254
2605
  const s = new Proxy(writableSource, {
2255
2606
  has(_, prop) {
2256
2607
  return Reflect.has(untracked(source), prop);
@@ -2278,10 +2629,16 @@ function toStore(source, injector) {
2278
2629
  return true;
2279
2630
  if (prop === 'asReadonlyStore')
2280
2631
  return () => {
2281
- if (!isWritableSignal(source))
2632
+ if (!isWritableSource)
2282
2633
  return s;
2283
- return untracked(() => toStore(source.asReadonly(), injector));
2634
+ return untracked(() => toStore(source.asReadonly(), injector, vivify));
2284
2635
  };
2636
+ if (prop === 'extend')
2637
+ return (seed) => scopedStore(s, seed, isMutableSource
2638
+ ? 'mutable'
2639
+ : isWritableSource
2640
+ ? 'writable'
2641
+ : 'readonly', injector);
2285
2642
  if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
2286
2643
  return target[prop];
2287
2644
  let storeCache = PROXY_CACHE.get(target);
@@ -2300,31 +2657,36 @@ function toStore(source, injector) {
2300
2657
  const value = untracked(target);
2301
2658
  const valueIsRecord = isRecord(value);
2302
2659
  const valueIsArray = Array.isArray(value);
2660
+ const nodeVivify = resolveVivify(value, vivify);
2661
+ const vivifyFn = createVivify(nodeVivify);
2303
2662
  const equalFn = (valueIsRecord || valueIsArray) &&
2304
2663
  isMutableSource &&
2305
2664
  typeof value[prop] === 'object'
2306
2665
  ? () => false
2307
2666
  : undefined;
2308
2667
  const computation = valueIsRecord
2309
- ? derived(target, prop, { equal: equalFn })
2668
+ ? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
2310
2669
  : derived(target, {
2311
2670
  from: (v) => v?.[prop],
2312
2671
  onChange: (newValue) => target.update((v) => {
2313
- if (v === null || v === undefined)
2314
- return v;
2672
+ const container = vivifyFn(v, prop);
2673
+ if (container === null || container === undefined)
2674
+ return container;
2315
2675
  try {
2316
- v[prop] = newValue;
2676
+ container[prop] = newValue;
2317
2677
  }
2318
2678
  catch (e) {
2319
2679
  if (isDevMode())
2320
2680
  console.error(`[store] Failed to set property "${String(prop)}"`, e);
2321
2681
  }
2322
- return v;
2682
+ return container;
2323
2683
  }),
2324
2684
  });
2325
- const proxy = Array.isArray(untracked(computation))
2326
- ? toArrayStore(computation, injector)
2327
- : toStore(computation, injector);
2685
+ const childSample = untracked(computation);
2686
+ const childVivify = resolveVivify(childSample, vivify);
2687
+ const proxy = Array.isArray(childSample)
2688
+ ? toArrayStore(computation, injector, childVivify)
2689
+ : toStore(computation, injector, childVivify);
2328
2690
  const ref = new WeakRef(proxy);
2329
2691
  storeCache.set(prop, ref);
2330
2692
  PROXY_CLEANUP.register(proxy, { target, prop }, ref);
@@ -2333,19 +2695,103 @@ function toStore(source, injector) {
2333
2695
  });
2334
2696
  return s;
2335
2697
  }
2698
+ /**
2699
+ * @internal
2700
+ * Backs `store.extend(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
2701
+ * plus any keys created later) is its own signal and `parent` is its own signal, so the getter
2702
+ * routes each key by consulting BOTH — local first, then parent, else local (so a write to an
2703
+ * as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
2704
+ * identity + two-way), while local keys never propagate upward. A merged `computed` is derived
2705
+ * only for whole-object reads / `has` / iteration — never for routing.
2706
+ */
2707
+ function scopedStore(parent, seed, kind, injector) {
2708
+ const local = isSignal(seed)
2709
+ ? toStore(seed, injector)
2710
+ : kind === 'mutable'
2711
+ ? mutableStore(seed, { injector })
2712
+ : kind === 'readonly'
2713
+ ? store(seed, { injector }).asReadonlyStore()
2714
+ : store(seed, { injector });
2715
+ const localValue = () => untracked(local);
2716
+ const parentValue = () => untracked(parent);
2717
+ const view = computed(() => ({
2718
+ ...parent(),
2719
+ ...local(),
2720
+ }), ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
2721
+ const splitSet = (next) => {
2722
+ const lv = localValue();
2723
+ const pv = parentValue();
2724
+ for (const key of Reflect.ownKeys(next)) {
2725
+ const layer = hasOwnKey(lv, key)
2726
+ ? local
2727
+ : hasOwnKey(pv, key)
2728
+ ? parent
2729
+ : local;
2730
+ layer[key].set(next[key]);
2731
+ }
2732
+ };
2733
+ const base = toWritable(view, kind === 'readonly' ? () => undefined : splitSet, undefined, { pure: false });
2734
+ if (kind === 'mutable') {
2735
+ base.mutate = (updater) => splitSet(updater(untracked(view)));
2736
+ base.inline = (updater) => base.mutate((prev) => {
2737
+ updater(prev);
2738
+ return prev;
2739
+ });
2740
+ }
2741
+ const scope = new Proxy(base, {
2742
+ get(target, prop) {
2743
+ if (prop === IS_STORE)
2744
+ return true;
2745
+ if (prop === SCOPE_PARENT)
2746
+ return parent;
2747
+ if (prop === 'extend')
2748
+ return (childSeed) => scopedStore(scope, childSeed, kind, injector);
2749
+ if (prop === 'asReadonlyStore')
2750
+ return () => toStore(computed(() => ({ ...parent(), ...local() })), injector);
2751
+ if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
2752
+ return target[prop];
2753
+ // Route by consulting both signals: local first, then parent, else local (new → local).
2754
+ if (hasOwnKey(localValue(), prop))
2755
+ return local[prop];
2756
+ if (hasOwnKey(parentValue(), prop))
2757
+ return parent[prop];
2758
+ return local[prop];
2759
+ },
2760
+ has(_, prop) {
2761
+ return hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop);
2762
+ },
2763
+ ownKeys() {
2764
+ return [
2765
+ ...new Set([
2766
+ ...Reflect.ownKeys(parentValue()),
2767
+ ...Reflect.ownKeys(localValue()),
2768
+ ]),
2769
+ ];
2770
+ },
2771
+ getOwnPropertyDescriptor(_, prop) {
2772
+ if (hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop))
2773
+ return { enumerable: true, configurable: true };
2774
+ return undefined;
2775
+ },
2776
+ getPrototypeOf() {
2777
+ return Object.prototype;
2778
+ },
2779
+ });
2780
+ return scope;
2781
+ }
2336
2782
  /**
2337
2783
  * Creates a WritableSignalStore from a value.
2338
2784
  * @see {@link toStore}
2339
2785
  */
2340
2786
  function store(value, opt) {
2341
- return toStore(signal(value, opt), opt?.injector);
2787
+ return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false);
2342
2788
  }
2343
2789
  /**
2344
2790
  * Creates a MutableSignalStore from a value.
2345
2791
  * @see {@link toStore}
2346
2792
  */
2347
2793
  function mutableStore(value, opt) {
2348
- return toStore(mutable(value, opt), opt?.injector);
2794
+ return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false);
2349
2795
  }
2350
2796
 
2351
2797
  // Internal dummy store for server-side rendering