@mmstack/primitives 20.5.7 → 20.5.9

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.
package/README.md CHANGED
@@ -69,6 +69,14 @@ const upper = derived(user, {
69
69
 
70
70
  When the source is a `MutableSignal`, the derived signal is also a `MutableSignal` — `derived(state, 'items').mutate(arr => { arr.push(...); return arr })` propagates correctly.
71
71
 
72
+ Pass `vivify` on the key/index form to create a missing container when writing through a `null`/`undefined` source — instead of throwing (mutable / array) or dropping the write. Choose `'object'`, `'array'`, `'auto'` (an array for index keys, an object otherwise), or a `() => container` factory; it defaults to off.
73
+
74
+ ```typescript
75
+ const user = signal<{ name: string } | null>(null);
76
+ derived(user, 'name', { vivify: 'object' }).set('Ada');
77
+ // user() === { name: 'Ada' }
78
+ ```
79
+
72
80
  ### `store` / `mutableStore`
73
81
 
74
82
  Proxies an object (or signal of an object) into a tree of `WritableSignal`s — one per property, lazily created and cached via `WeakRef`. Arrays expose indices as signals plus a `.length` signal and `Symbol.iterator`. Mutability propagates: if the root is a `MutableSignal`, every child is too.
@@ -92,8 +100,52 @@ settings.notifications.mutate((n) => {
92
100
  });
93
101
  ```
94
102
 
103
+ **Autovivification (opt-in).** By default, a write through a `null`/`undefined` path is dropped. Pass `vivify` to create the missing intermediate containers instead:
104
+
105
+ ```typescript
106
+ const form = store(
107
+ { user: null as { address?: { city: string } } | null },
108
+ { vivify: 'auto' },
109
+ );
110
+
111
+ form.user.address.city.set('NYC');
112
+ // form() === { user: { address: { city: 'NYC' } } }
113
+ ```
114
+
115
+ Each level's shape is resolved from what's known: a value that is currently an object/array re-creates as that same shape (resolved per path and cached, so it survives the value later being nulled), while genuinely-unknown levels follow your option — `'auto'` (an array for index keys, an object otherwise), `'object'`, `'array'`, or a `() => container` factory. `false` (the default) keeps writes through `null` as no-ops. Adding a key that simply wasn't present on an existing object always works and needs no `vivify`.
116
+
95
117
  Top-level array support isn't exposed yet — use `indexArray` / `keyArray` for those.
96
118
 
119
+ ### `extend` (scoped overlay)
120
+
121
+ `store.extend(seed)` (on any store kind) creates a **scoped overlay** — a child store that **shares** the parent's signals for inherited keys (the same `WritableSignal`: writes go through to the parent and parent changes flow down) while keeping the seed and any new keys in a **local layer** that never propagates upward. No diffing, no syncing — local keys simply aren't wired to the parent.
122
+
123
+ ```typescript
124
+ const app = store({ user: { name: 'Alice' }, theme: 'dark' });
125
+
126
+ const scope = app.extend({ draft: '' }); // inherits user + theme, adds a local draft
127
+
128
+ scope.user === app.user; // true — the same signal (shared, two-way)
129
+ scope.user.name.set('Bob'); // writes through to the parent
130
+ scope.draft.set('hello'); // local only — `app` never gains `draft`
131
+ scope(); // { user: { name: 'Bob' }, theme: 'dark', draft: 'hello' }
132
+ ```
133
+
134
+ Resolution per key is **local → parent → local**: a seed key (or one set on the scope before it exists on the parent) is local and _shadows_ the parent — and keeps shadowing even if the parent later grows that key; a key that exists only on the parent writes through to it; a brand-new key lands locally. `scope()` is the merged view (local shadowing), and `Object.keys(scope)` / `key in scope` are the union of both layers. `extend` composes — `a.extend(x).extend(y)` chains parents.
135
+
136
+ The seed may also be a **signal** of the matching kind, so an existing (externally-owned, reactive) signal becomes the local layer:
137
+
138
+ ```typescript
139
+ const draft = signal({ title: '' });
140
+ const scope = app.extend(draft); // writes to scope.title flow out to `draft`, and back in
141
+ ```
142
+
143
+ A few release notes:
144
+
145
+ - The local layer is a plain store (vivify off). Inherited paths vivify when the _parent_ was created with `vivify`; to autovivify local keys, seed with a vivify-enabled store — `app.extend(store(seed, { vivify: 'auto' }))`.
146
+ - Reserved names — `extend`, `asReadonlyStore`, and the signal methods (`set` / `update` / `mutate` / `inline` / `asReadonly`) — shadow same-named data keys, as on any store.
147
+ - `scope.asReadonlyStore()` returns a read-only **snapshot view** of the merge (reactive reads, no writes); it does not share sub-store identity.
148
+
97
149
  ### `toWritable`
98
150
 
99
151
  Turn any read-only `Signal<T>` into a `WritableSignal<T>` by providing custom `set` / `update` implementations. Powers `derived` internally; use it directly when you have a `computed` you want to expose as writable.
@@ -401,37 +401,158 @@ function isMutable(value) {
401
401
  return 'mutate' in value && typeof value.mutate === 'function';
402
402
  }
403
403
 
404
+ /**
405
+ * @internal
406
+ * Type guard for an array-index-like property key: a non-empty string that parses to a finite
407
+ * number (e.g. `'0'`, `'42'`). Used to choose array-vs-object shape during autovivification and
408
+ * deep store proxying.
409
+ */
410
+ function isIndexProp(prop) {
411
+ return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
412
+ }
413
+
414
+ // Container resolvers used by createVivify: each returns the current value when present and
415
+ // only creates a new container when it is null/undefined.
416
+ function identity(x) {
417
+ return x;
418
+ }
419
+ function createArray(cur) {
420
+ if (cur === null || cur === undefined)
421
+ return [];
422
+ return cur;
423
+ }
424
+ function createObject(cur) {
425
+ if (cur === null || cur === undefined)
426
+ return {};
427
+ return cur;
428
+ }
429
+ function createAuto(cur, key) {
430
+ if (cur === null || cur === undefined) {
431
+ return typeof key === 'number' || isIndexProp(key)
432
+ ? []
433
+ : {};
434
+ }
435
+ return cur;
436
+ }
437
+ /**
438
+ * @internal
439
+ * Resolves a {@link Vivify} option into a {@link VivifyFn}. The returned function leaves a
440
+ * present value untouched and only creates a new container — object, array, or factory result —
441
+ * when the current value is `null`/`undefined`.
442
+ */
443
+ function createVivify(option) {
444
+ switch (option) {
445
+ case false:
446
+ return identity;
447
+ case 'array':
448
+ return createArray;
449
+ case 'object':
450
+ return createObject;
451
+ case 'auto':
452
+ case true:
453
+ return createAuto;
454
+ default:
455
+ return typeof option === 'function'
456
+ ? (cur) => cur === null || cur === undefined ? option() : cur
457
+ : identity;
458
+ }
459
+ }
460
+
461
+ function createMutableArrayUpdater(source, index, vivifyFn) {
462
+ return (next) => source.mutate((cur) => {
463
+ const vivified = vivifyFn(cur, index);
464
+ if (vivified === null || vivified === undefined)
465
+ return vivified;
466
+ vivified[index] = next;
467
+ return vivified;
468
+ });
469
+ }
470
+ function createImmutableArrayUpdater(source, index, vivifyFn) {
471
+ return (next) => source.update((cur) => {
472
+ const vivified = vivifyFn(cur, index)?.slice();
473
+ if (vivified === null || vivified === undefined)
474
+ return vivified;
475
+ vivified[index] = next;
476
+ return vivified;
477
+ });
478
+ }
479
+ function createMutableObjectUpdater(source, key, vivifyFn) {
480
+ return (next) => source.mutate((cur) => {
481
+ const vivified = vivifyFn(cur, key);
482
+ if (vivified === null || vivified === undefined)
483
+ return vivified;
484
+ vivified[key] = next;
485
+ return vivified;
486
+ });
487
+ }
488
+ function createImmutableObjectUpdater(source, key, vivifyFn) {
489
+ return (next) => source.update((cur) => {
490
+ const vivified = vivifyFn(cur, key);
491
+ if (vivified === null || vivified === undefined)
492
+ return vivified;
493
+ return { ...vivified, [key]: next };
494
+ });
495
+ }
496
+ function createUpdater(source, key, vivify) {
497
+ const sample = untracked(source);
498
+ // fast path for when vivification is off
499
+ if (!vivify) {
500
+ if (Array.isArray(sample) && typeof key === 'number') {
501
+ const idx = key;
502
+ return isMutable(source)
503
+ ? (next) => source.mutate((cur) => {
504
+ cur[idx] = next;
505
+ return cur;
506
+ })
507
+ : (next) => source.update((cur) => {
508
+ const copy = cur.slice();
509
+ copy[idx] = next;
510
+ return copy;
511
+ });
512
+ }
513
+ return isMutable(source)
514
+ ? (next) => source.mutate((cur) => {
515
+ cur[key] = next;
516
+ return cur;
517
+ })
518
+ : (next) => source.update((cur) => ({
519
+ ...cur,
520
+ [key]: next,
521
+ }));
522
+ }
523
+ const present = sample !== null && sample !== undefined;
524
+ const keyIsIndex = typeof key === 'number' || isIndexProp(key);
525
+ let vivifyOpt = vivify;
526
+ if (vivifyOpt === 'auto' || vivifyOpt === true) {
527
+ vivifyOpt = ((present ? Array.isArray(sample) : keyIsIndex) ? 'array' : 'object');
528
+ }
529
+ const vivifyFn = createVivify(vivifyOpt);
530
+ // Route to the array updater whenever the container is (or will be vivified as) an
531
+ // array, so the updater and the created container agree on shape for a nullish source.
532
+ const isArray = vivifyOpt === 'array'
533
+ ? keyIsIndex
534
+ : vivifyOpt === 'object'
535
+ ? false
536
+ : Array.isArray(sample) && typeof key === 'number';
537
+ if (isArray)
538
+ return isMutable(source)
539
+ ? createMutableArrayUpdater(source, key, vivifyFn)
540
+ : createImmutableArrayUpdater(source, key, vivifyFn);
541
+ return isMutable(source)
542
+ ? createMutableObjectUpdater(source, key, vivifyFn)
543
+ : createImmutableObjectUpdater(source, key, vivifyFn);
544
+ }
404
545
  function derived(source, optOrKey, opt) {
405
- const isArray = Array.isArray(untracked(source)) && typeof optOrKey === 'number';
406
- const from = typeof optOrKey === 'object' ? optOrKey.from : (v) => v[optOrKey];
546
+ const vivify = typeof optOrKey === 'object' ? false : (opt?.vivify ?? false);
547
+ // With vivification the source may legitimately be null/undefined
548
+ const from = typeof optOrKey === 'object'
549
+ ? optOrKey.from
550
+ : vivify
551
+ ? (v) => v?.[optOrKey]
552
+ : (v) => v[optOrKey];
407
553
  const onChange = typeof optOrKey === 'object'
408
554
  ? optOrKey.onChange
409
- : isArray
410
- ? isMutable(source)
411
- ? (next) => {
412
- source.mutate((cur) => {
413
- cur[optOrKey] = next;
414
- return cur;
415
- });
416
- }
417
- : (next) => {
418
- source.update((cur) => {
419
- const newArray = [...cur];
420
- newArray[optOrKey] = next;
421
- return newArray;
422
- });
423
- }
424
- : isMutable(source)
425
- ? (next) => {
426
- source.mutate((cur) => {
427
- cur[optOrKey] =
428
- next;
429
- return cur;
430
- });
431
- }
432
- : (next) => {
433
- source.update((cur) => ({ ...cur, [optOrKey]: next }));
434
- };
555
+ : createUpdater(source, optOrKey, vivify);
435
556
  const rest = typeof optOrKey === 'object' ? { ...optOrKey, ...opt } : opt;
436
557
  const baseEqual = rest?.equal ?? Object.is;
437
558
  let cnt = 0;
@@ -2284,7 +2405,19 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
2284
2405
  return untracked(() => state.asReadonly());
2285
2406
  }
2286
2407
 
2408
+ /**
2409
+ * Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
2410
+ * has a `unique symbol` type, so the same symbol serves as both the property key written
2411
+ * by {@link opaque} and the type-level brand carried by {@link Opaque}.
2412
+ */
2413
+ const OPAQUE = Symbol('MMSTACK::OPAQUE');
2287
2414
  const IS_STORE = Symbol('MMSTACK::IS_STORE');
2415
+ const SCOPE_PARENT = Symbol('MMSTACK::SCOPE_PARENT');
2416
+ /**
2417
+ * @internal
2418
+ * Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
2419
+ * Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
2420
+ */
2288
2421
  const PROXY_CACHE = new WeakMap();
2289
2422
  const SIGNAL_FN_PROP = new Set([
2290
2423
  'set',
@@ -2293,6 +2426,11 @@ const SIGNAL_FN_PROP = new Set([
2293
2426
  'inline',
2294
2427
  'asReadonly',
2295
2428
  ]);
2429
+ /**
2430
+ * @internal
2431
+ * Test-only handle on the finalization registry (deliberately NOT re-exported from the public
2432
+ * barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
2433
+ */
2296
2434
  const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
2297
2435
  const storeCache = PROXY_CACHE.get(target);
2298
2436
  if (storeCache)
@@ -2307,20 +2445,37 @@ function isStore(value) {
2307
2445
  value !== null &&
2308
2446
  value[IS_STORE] === true);
2309
2447
  }
2310
- function isIndexProp(prop) {
2311
- return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
2312
- }
2313
2448
  function isRecord(value) {
2314
2449
  if (value === null || typeof value !== 'object')
2315
2450
  return false;
2451
+ if (value[OPAQUE] === true)
2452
+ return false; // opaque → leaf
2316
2453
  const proto = Object.getPrototypeOf(value);
2317
2454
  return proto === Object.prototype || proto === null;
2318
2455
  }
2456
+ /**
2457
+ * @internal
2458
+ * Resolves the vivify shape for a node from its current value: a present record/array is a
2459
+ * certainty we keep (cached in the derivation, so it survives the value being nulled); an
2460
+ * unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
2461
+ */
2462
+ function resolveVivify(sample, option) {
2463
+ if (!option)
2464
+ return false;
2465
+ if (Array.isArray(sample))
2466
+ return 'array';
2467
+ if (isRecord(sample))
2468
+ return 'object';
2469
+ return 'auto';
2470
+ }
2471
+ function hasOwnKey(value, key) {
2472
+ return value != null && Object.hasOwn(value, key);
2473
+ }
2319
2474
  /**
2320
2475
  * @internal
2321
2476
  * Makes an array store
2322
2477
  */
2323
- function toArrayStore(source, injector) {
2478
+ function toArrayStore(source, injector, vivify) {
2324
2479
  if (isStore(source))
2325
2480
  return source;
2326
2481
  const isMutableSource = isMutable(source);
@@ -2400,31 +2555,39 @@ function toArrayStore(source, injector) {
2400
2555
  const value = untracked(target);
2401
2556
  const valueIsArray = Array.isArray(value);
2402
2557
  const valueIsRecord = isRecord(value);
2558
+ const nodeVivify = resolveVivify(value, vivify);
2559
+ const vivifyFn = createVivify(nodeVivify);
2403
2560
  const equalFn = (valueIsRecord || valueIsArray) &&
2404
2561
  isMutableSource &&
2405
2562
  typeof value[idx] === 'object'
2406
2563
  ? () => false
2407
2564
  : undefined;
2408
2565
  const computation = valueIsRecord
2409
- ? derived(target, idx, { equal: equalFn })
2566
+ ? derived(target, idx, {
2567
+ equal: equalFn,
2568
+ vivify: nodeVivify,
2569
+ })
2410
2570
  : derived(target, {
2411
2571
  from: (v) => v?.[idx],
2412
2572
  onChange: (newValue) => target.update((v) => {
2413
- if (v === null || v === undefined)
2414
- return v;
2573
+ const container = vivifyFn(v, idx);
2574
+ if (container === null || container === undefined)
2575
+ return container;
2415
2576
  try {
2416
- v[idx] = newValue;
2577
+ container[idx] = newValue;
2417
2578
  }
2418
2579
  catch (e) {
2419
2580
  if (isDevMode())
2420
2581
  console.error(`[store] Failed to set property "${String(idx)}"`, e);
2421
2582
  }
2422
- return v;
2583
+ return container;
2423
2584
  }),
2424
2585
  });
2425
- const proxy = Array.isArray(untracked(computation))
2426
- ? toArrayStore(computation, injector)
2427
- : toStore(computation, injector);
2586
+ const childSample = untracked(computation);
2587
+ const childVivify = resolveVivify(childSample, vivify);
2588
+ const proxy = Array.isArray(childSample)
2589
+ ? toArrayStore(computation, injector, childVivify)
2590
+ : toStore(computation, injector, childVivify);
2428
2591
  const ref = new WeakRef(proxy);
2429
2592
  storeCache.set(idx, ref);
2430
2593
  PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
@@ -2441,7 +2604,7 @@ function toArrayStore(source, injector) {
2441
2604
  * const state = store({ user: { name: 'John' } });
2442
2605
  * const nameSignal = state.user.name; // WritableSignal<string>
2443
2606
  */
2444
- function toStore(source, injector) {
2607
+ function toStore(source, injector, vivify = false) {
2445
2608
  if (isStore(source))
2446
2609
  return source;
2447
2610
  if (!injector)
@@ -2451,7 +2614,8 @@ function toStore(source, injector) {
2451
2614
  : toWritable(source, () => {
2452
2615
  // noop
2453
2616
  });
2454
- const isMutableSource = isMutable(writableSource);
2617
+ const isWritableSource = isWritableSignal(source);
2618
+ const isMutableSource = isWritableSource && isMutable(writableSource);
2455
2619
  const s = new Proxy(writableSource, {
2456
2620
  has(_, prop) {
2457
2621
  return Reflect.has(untracked(source), prop);
@@ -2479,10 +2643,16 @@ function toStore(source, injector) {
2479
2643
  return true;
2480
2644
  if (prop === 'asReadonlyStore')
2481
2645
  return () => {
2482
- if (!isWritableSignal(source))
2646
+ if (!isWritableSource)
2483
2647
  return s;
2484
- return untracked(() => toStore(source.asReadonly(), injector));
2648
+ return untracked(() => toStore(source.asReadonly(), injector, vivify));
2485
2649
  };
2650
+ if (prop === 'extend')
2651
+ return (seed) => scopedStore(s, seed, isMutableSource
2652
+ ? 'mutable'
2653
+ : isWritableSource
2654
+ ? 'writable'
2655
+ : 'readonly', injector);
2486
2656
  if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
2487
2657
  return target[prop];
2488
2658
  let storeCache = PROXY_CACHE.get(target);
@@ -2501,31 +2671,36 @@ function toStore(source, injector) {
2501
2671
  const value = untracked(target);
2502
2672
  const valueIsRecord = isRecord(value);
2503
2673
  const valueIsArray = Array.isArray(value);
2674
+ const nodeVivify = resolveVivify(value, vivify);
2675
+ const vivifyFn = createVivify(nodeVivify);
2504
2676
  const equalFn = (valueIsRecord || valueIsArray) &&
2505
2677
  isMutableSource &&
2506
2678
  typeof value[prop] === 'object'
2507
2679
  ? () => false
2508
2680
  : undefined;
2509
2681
  const computation = valueIsRecord
2510
- ? derived(target, prop, { equal: equalFn })
2682
+ ? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
2511
2683
  : derived(target, {
2512
2684
  from: (v) => v?.[prop],
2513
2685
  onChange: (newValue) => target.update((v) => {
2514
- if (v === null || v === undefined)
2515
- return v;
2686
+ const container = vivifyFn(v, prop);
2687
+ if (container === null || container === undefined)
2688
+ return container;
2516
2689
  try {
2517
- v[prop] = newValue;
2690
+ container[prop] = newValue;
2518
2691
  }
2519
2692
  catch (e) {
2520
2693
  if (isDevMode())
2521
2694
  console.error(`[store] Failed to set property "${String(prop)}"`, e);
2522
2695
  }
2523
- return v;
2696
+ return container;
2524
2697
  }),
2525
2698
  });
2526
- const proxy = Array.isArray(untracked(computation))
2527
- ? toArrayStore(computation, injector)
2528
- : toStore(computation, injector);
2699
+ const childSample = untracked(computation);
2700
+ const childVivify = resolveVivify(childSample, vivify);
2701
+ const proxy = Array.isArray(childSample)
2702
+ ? toArrayStore(computation, injector, childVivify)
2703
+ : toStore(computation, injector, childVivify);
2529
2704
  const ref = new WeakRef(proxy);
2530
2705
  storeCache.set(prop, ref);
2531
2706
  PROXY_CLEANUP.register(proxy, { target, prop }, ref);
@@ -2534,19 +2709,119 @@ function toStore(source, injector) {
2534
2709
  });
2535
2710
  return s;
2536
2711
  }
2712
+ /**
2713
+ * @internal
2714
+ * Backs `store.extend(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
2715
+ * plus any keys created later) is its own signal and `parent` is its own signal, so the getter
2716
+ * routes each key by consulting BOTH — local first, then parent, else local (so a write to an
2717
+ * as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
2718
+ * identity + two-way), while local keys never propagate upward. A merged `computed` is derived
2719
+ * only for whole-object reads / `has` / iteration — never for routing.
2720
+ */
2721
+ function scopedStore(parent, seed, kind, injector) {
2722
+ const local = isSignal(seed)
2723
+ ? toStore(seed, injector)
2724
+ : kind === 'mutable'
2725
+ ? mutableStore(seed, { injector })
2726
+ : kind === 'readonly'
2727
+ ? store(seed, { injector }).asReadonlyStore()
2728
+ : store(seed, { injector });
2729
+ const localValue = () => untracked(local);
2730
+ const parentValue = () => untracked(parent);
2731
+ const view = computed(() => ({
2732
+ ...parent(),
2733
+ ...local(),
2734
+ }), ...(ngDevMode ? [{ debugName: "view" }] : []));
2735
+ const splitSet = (next) => {
2736
+ const lv = localValue();
2737
+ const pv = parentValue();
2738
+ for (const key of Reflect.ownKeys(next)) {
2739
+ const layer = hasOwnKey(lv, key)
2740
+ ? local
2741
+ : hasOwnKey(pv, key)
2742
+ ? parent
2743
+ : local;
2744
+ layer[key].set(next[key]);
2745
+ }
2746
+ };
2747
+ const base = toWritable(view, kind === 'readonly' ? () => undefined : splitSet, undefined, { pure: false });
2748
+ if (kind === 'mutable') {
2749
+ base.mutate = (updater) => splitSet(updater(untracked(view)));
2750
+ base.inline = (updater) => base.mutate((prev) => {
2751
+ updater(prev);
2752
+ return prev;
2753
+ });
2754
+ }
2755
+ const scope = new Proxy(base, {
2756
+ get(target, prop) {
2757
+ if (prop === IS_STORE)
2758
+ return true;
2759
+ if (prop === SCOPE_PARENT)
2760
+ return parent;
2761
+ if (prop === 'extend')
2762
+ return (childSeed) => scopedStore(scope, childSeed, kind, injector);
2763
+ if (prop === 'asReadonlyStore')
2764
+ return () => toStore(computed(() => ({ ...parent(), ...local() })), injector);
2765
+ if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
2766
+ return target[prop];
2767
+ // Route by consulting both signals: local first, then parent, else local (new → local).
2768
+ if (hasOwnKey(localValue(), prop))
2769
+ return local[prop];
2770
+ if (hasOwnKey(parentValue(), prop))
2771
+ return parent[prop];
2772
+ return local[prop];
2773
+ },
2774
+ has(_, prop) {
2775
+ return hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop);
2776
+ },
2777
+ ownKeys() {
2778
+ return [
2779
+ ...new Set([
2780
+ ...Reflect.ownKeys(parentValue()),
2781
+ ...Reflect.ownKeys(localValue()),
2782
+ ]),
2783
+ ];
2784
+ },
2785
+ getOwnPropertyDescriptor(_, prop) {
2786
+ if (hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop))
2787
+ return { enumerable: true, configurable: true };
2788
+ return undefined;
2789
+ },
2790
+ getPrototypeOf() {
2791
+ return Object.prototype;
2792
+ },
2793
+ });
2794
+ return scope;
2795
+ }
2537
2796
  /**
2538
2797
  * Creates a WritableSignalStore from a value.
2539
2798
  * @see {@link toStore}
2540
2799
  */
2541
2800
  function store(value, opt) {
2542
- return toStore(signal(value, opt), opt?.injector);
2801
+ return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false);
2543
2802
  }
2544
2803
  /**
2545
2804
  * Creates a MutableSignalStore from a value.
2546
2805
  * @see {@link toStore}
2547
2806
  */
2548
2807
  function mutableStore(value, opt) {
2549
- return toStore(mutable(value, opt), opt?.injector);
2808
+ return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false);
2809
+ }
2810
+ /**
2811
+ * Marks a plain object as opaque so {@link store} treats it as an indivisible leaf
2812
+ * (returned whole, never deep-proxied) — the same way it treats a `Date` or `RegExp`.
2813
+ * The marker is a non-enumerable symbol, so it never appears in spreads or iteration.
2814
+ * Idempotent. Call before freezing (`defineProperty` fails on a frozen object).
2815
+ *
2816
+ * @example
2817
+ * const s = store({ config: opaque({ theme: 'dark', nested: { a: 1 } }) });
2818
+ * s.config(); // the whole object, not a child store
2819
+ * s.config.set(opaque({ theme: 'light', nested: { a: 2 } }));
2820
+ */
2821
+ function opaque(value) {
2822
+ if (value[OPAQUE] !== true)
2823
+ Object.defineProperty(value, OPAQUE, { value: true, enumerable: false });
2824
+ return value;
2550
2825
  }
2551
2826
 
2552
2827
  // Internal dummy store for server-side rendering
@@ -3032,5 +3307,5 @@ function withHistory(sourceOrValue, opt) {
3032
3307
  * Generated bundle index. Do not edit.
3033
3308
  */
3034
3309
 
3035
- 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 };
3310
+ 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, opaque, 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 };
3036
3311
  //# sourceMappingURL=mmstack-primitives.mjs.map