@mmstack/primitives 19.3.6 → 19.3.8

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