@mmstack/primitives 21.0.24 → 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.
- package/README.md +52 -0
- package/fesm2022/mmstack-primitives.mjs +305 -54
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +135 -19
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.
|
|
@@ -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
|
|
402
|
-
|
|
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
|
-
:
|
|
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;
|
|
@@ -2279,6 +2400,12 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
2279
2400
|
}
|
|
2280
2401
|
|
|
2281
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
|
+
*/
|
|
2282
2409
|
const PROXY_CACHE = new WeakMap();
|
|
2283
2410
|
const SIGNAL_FN_PROP = new Set([
|
|
2284
2411
|
'set',
|
|
@@ -2287,6 +2414,11 @@ const SIGNAL_FN_PROP = new Set([
|
|
|
2287
2414
|
'inline',
|
|
2288
2415
|
'asReadonly',
|
|
2289
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
|
+
*/
|
|
2290
2422
|
const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
|
|
2291
2423
|
const storeCache = PROXY_CACHE.get(target);
|
|
2292
2424
|
if (storeCache)
|
|
@@ -2301,20 +2433,35 @@ function isStore(value) {
|
|
|
2301
2433
|
value !== null &&
|
|
2302
2434
|
value[IS_STORE] === true);
|
|
2303
2435
|
}
|
|
2304
|
-
function isIndexProp(prop) {
|
|
2305
|
-
return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
|
|
2306
|
-
}
|
|
2307
2436
|
function isRecord(value) {
|
|
2308
2437
|
if (value === null || typeof value !== 'object')
|
|
2309
2438
|
return false;
|
|
2310
2439
|
const proto = Object.getPrototypeOf(value);
|
|
2311
2440
|
return proto === Object.prototype || proto === null;
|
|
2312
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
|
+
}
|
|
2313
2460
|
/**
|
|
2314
2461
|
* @internal
|
|
2315
2462
|
* Makes an array store
|
|
2316
2463
|
*/
|
|
2317
|
-
function toArrayStore(source, injector) {
|
|
2464
|
+
function toArrayStore(source, injector, vivify) {
|
|
2318
2465
|
if (isStore(source))
|
|
2319
2466
|
return source;
|
|
2320
2467
|
const isMutableSource = isMutable(source);
|
|
@@ -2394,31 +2541,39 @@ function toArrayStore(source, injector) {
|
|
|
2394
2541
|
const value = untracked(target);
|
|
2395
2542
|
const valueIsArray = Array.isArray(value);
|
|
2396
2543
|
const valueIsRecord = isRecord(value);
|
|
2544
|
+
const nodeVivify = resolveVivify(value, vivify);
|
|
2545
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
2397
2546
|
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
2398
2547
|
isMutableSource &&
|
|
2399
2548
|
typeof value[idx] === 'object'
|
|
2400
2549
|
? () => false
|
|
2401
2550
|
: undefined;
|
|
2402
2551
|
const computation = valueIsRecord
|
|
2403
|
-
? derived(target, idx, {
|
|
2552
|
+
? derived(target, idx, {
|
|
2553
|
+
equal: equalFn,
|
|
2554
|
+
vivify: nodeVivify,
|
|
2555
|
+
})
|
|
2404
2556
|
: derived(target, {
|
|
2405
2557
|
from: (v) => v?.[idx],
|
|
2406
2558
|
onChange: (newValue) => target.update((v) => {
|
|
2407
|
-
|
|
2408
|
-
|
|
2559
|
+
const container = vivifyFn(v, idx);
|
|
2560
|
+
if (container === null || container === undefined)
|
|
2561
|
+
return container;
|
|
2409
2562
|
try {
|
|
2410
|
-
|
|
2563
|
+
container[idx] = newValue;
|
|
2411
2564
|
}
|
|
2412
2565
|
catch (e) {
|
|
2413
2566
|
if (isDevMode())
|
|
2414
2567
|
console.error(`[store] Failed to set property "${String(idx)}"`, e);
|
|
2415
2568
|
}
|
|
2416
|
-
return
|
|
2569
|
+
return container;
|
|
2417
2570
|
}),
|
|
2418
2571
|
});
|
|
2419
|
-
const
|
|
2420
|
-
|
|
2421
|
-
|
|
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);
|
|
2422
2577
|
const ref = new WeakRef(proxy);
|
|
2423
2578
|
storeCache.set(idx, ref);
|
|
2424
2579
|
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
@@ -2435,7 +2590,7 @@ function toArrayStore(source, injector) {
|
|
|
2435
2590
|
* const state = store({ user: { name: 'John' } });
|
|
2436
2591
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
2437
2592
|
*/
|
|
2438
|
-
function toStore(source, injector) {
|
|
2593
|
+
function toStore(source, injector, vivify = false) {
|
|
2439
2594
|
if (isStore(source))
|
|
2440
2595
|
return source;
|
|
2441
2596
|
if (!injector)
|
|
@@ -2445,7 +2600,8 @@ function toStore(source, injector) {
|
|
|
2445
2600
|
: toWritable(source, () => {
|
|
2446
2601
|
// noop
|
|
2447
2602
|
});
|
|
2448
|
-
const
|
|
2603
|
+
const isWritableSource = isWritableSignal(source);
|
|
2604
|
+
const isMutableSource = isWritableSource && isMutable(writableSource);
|
|
2449
2605
|
const s = new Proxy(writableSource, {
|
|
2450
2606
|
has(_, prop) {
|
|
2451
2607
|
return Reflect.has(untracked(source), prop);
|
|
@@ -2473,10 +2629,16 @@ function toStore(source, injector) {
|
|
|
2473
2629
|
return true;
|
|
2474
2630
|
if (prop === 'asReadonlyStore')
|
|
2475
2631
|
return () => {
|
|
2476
|
-
if (!
|
|
2632
|
+
if (!isWritableSource)
|
|
2477
2633
|
return s;
|
|
2478
|
-
return untracked(() => toStore(source.asReadonly(), injector));
|
|
2634
|
+
return untracked(() => toStore(source.asReadonly(), injector, vivify));
|
|
2479
2635
|
};
|
|
2636
|
+
if (prop === 'extend')
|
|
2637
|
+
return (seed) => scopedStore(s, seed, isMutableSource
|
|
2638
|
+
? 'mutable'
|
|
2639
|
+
: isWritableSource
|
|
2640
|
+
? 'writable'
|
|
2641
|
+
: 'readonly', injector);
|
|
2480
2642
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
2481
2643
|
return target[prop];
|
|
2482
2644
|
let storeCache = PROXY_CACHE.get(target);
|
|
@@ -2495,31 +2657,36 @@ function toStore(source, injector) {
|
|
|
2495
2657
|
const value = untracked(target);
|
|
2496
2658
|
const valueIsRecord = isRecord(value);
|
|
2497
2659
|
const valueIsArray = Array.isArray(value);
|
|
2660
|
+
const nodeVivify = resolveVivify(value, vivify);
|
|
2661
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
2498
2662
|
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
2499
2663
|
isMutableSource &&
|
|
2500
2664
|
typeof value[prop] === 'object'
|
|
2501
2665
|
? () => false
|
|
2502
2666
|
: undefined;
|
|
2503
2667
|
const computation = valueIsRecord
|
|
2504
|
-
? derived(target, prop, { equal: equalFn })
|
|
2668
|
+
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
2505
2669
|
: derived(target, {
|
|
2506
2670
|
from: (v) => v?.[prop],
|
|
2507
2671
|
onChange: (newValue) => target.update((v) => {
|
|
2508
|
-
|
|
2509
|
-
|
|
2672
|
+
const container = vivifyFn(v, prop);
|
|
2673
|
+
if (container === null || container === undefined)
|
|
2674
|
+
return container;
|
|
2510
2675
|
try {
|
|
2511
|
-
|
|
2676
|
+
container[prop] = newValue;
|
|
2512
2677
|
}
|
|
2513
2678
|
catch (e) {
|
|
2514
2679
|
if (isDevMode())
|
|
2515
2680
|
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
2516
2681
|
}
|
|
2517
|
-
return
|
|
2682
|
+
return container;
|
|
2518
2683
|
}),
|
|
2519
2684
|
});
|
|
2520
|
-
const
|
|
2521
|
-
|
|
2522
|
-
|
|
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);
|
|
2523
2690
|
const ref = new WeakRef(proxy);
|
|
2524
2691
|
storeCache.set(prop, ref);
|
|
2525
2692
|
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
@@ -2528,19 +2695,103 @@ function toStore(source, injector) {
|
|
|
2528
2695
|
});
|
|
2529
2696
|
return s;
|
|
2530
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
|
+
}
|
|
2531
2782
|
/**
|
|
2532
2783
|
* Creates a WritableSignalStore from a value.
|
|
2533
2784
|
* @see {@link toStore}
|
|
2534
2785
|
*/
|
|
2535
2786
|
function store(value, opt) {
|
|
2536
|
-
return toStore(signal(value, opt), opt?.injector);
|
|
2787
|
+
return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false);
|
|
2537
2788
|
}
|
|
2538
2789
|
/**
|
|
2539
2790
|
* Creates a MutableSignalStore from a value.
|
|
2540
2791
|
* @see {@link toStore}
|
|
2541
2792
|
*/
|
|
2542
2793
|
function mutableStore(value, opt) {
|
|
2543
|
-
return toStore(mutable(value, opt), opt?.injector);
|
|
2794
|
+
return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false);
|
|
2544
2795
|
}
|
|
2545
2796
|
|
|
2546
2797
|
// Internal dummy store for server-side rendering
|