@mmstack/primitives 19.3.7 → 19.3.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 +52 -0
- package/fesm2022/mmstack-primitives.mjs +330 -55
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/index.d.ts +2 -2
- package/lib/derived.d.ts +17 -7
- package/lib/pooled/index.d.ts +2 -0
- package/lib/{provided-pools.d.ts → pooled/provided-pools.d.ts} +1 -1
- package/lib/store.d.ts +119 -14
- package/lib/util/index.d.ts +2 -0
- package/lib/util/is-index-prop.d.ts +7 -0
- package/lib/util/vivify.d.ts +63 -0
- package/package.json +1 -1
- /package/lib/{pooled.d.ts → pooled/pooled.d.ts} +0 -0
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.
|
|
@@ -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
|
|
404
|
-
|
|
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
|
-
:
|
|
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;
|
|
@@ -2281,7 +2402,19 @@ function signalFromEvent(target, eventName, initial, projectOrOpt, maybeOpt) {
|
|
|
2281
2402
|
return untracked(() => state.asReadonly());
|
|
2282
2403
|
}
|
|
2283
2404
|
|
|
2405
|
+
/**
|
|
2406
|
+
* Runtime marker + compile-time brand for an opaque value. A `const`-declared `Symbol`
|
|
2407
|
+
* has a `unique symbol` type, so the same symbol serves as both the property key written
|
|
2408
|
+
* by {@link opaque} and the type-level brand carried by {@link Opaque}.
|
|
2409
|
+
*/
|
|
2410
|
+
const OPAQUE = Symbol('MMSTACK::OPAQUE');
|
|
2284
2411
|
const IS_STORE = Symbol('MMSTACK::IS_STORE');
|
|
2412
|
+
const SCOPE_PARENT = Symbol('MMSTACK::SCOPE_PARENT');
|
|
2413
|
+
/**
|
|
2414
|
+
* @internal
|
|
2415
|
+
* Test-only handle on the proxy cache (deliberately NOT re-exported from the public barrel).
|
|
2416
|
+
* Maps a store's backing signal to its lazily-built child proxies, each held via a `WeakRef`.
|
|
2417
|
+
*/
|
|
2285
2418
|
const PROXY_CACHE = new WeakMap();
|
|
2286
2419
|
const SIGNAL_FN_PROP = new Set([
|
|
2287
2420
|
'set',
|
|
@@ -2290,6 +2423,11 @@ const SIGNAL_FN_PROP = new Set([
|
|
|
2290
2423
|
'inline',
|
|
2291
2424
|
'asReadonly',
|
|
2292
2425
|
]);
|
|
2426
|
+
/**
|
|
2427
|
+
* @internal
|
|
2428
|
+
* Test-only handle on the finalization registry (deliberately NOT re-exported from the public
|
|
2429
|
+
* barrel). Prunes a cache entry once its proxy is reclaimed by the GC.
|
|
2430
|
+
*/
|
|
2293
2431
|
const PROXY_CLEANUP = new FinalizationRegistry(({ target, prop }) => {
|
|
2294
2432
|
const storeCache = PROXY_CACHE.get(target);
|
|
2295
2433
|
if (storeCache)
|
|
@@ -2304,20 +2442,37 @@ function isStore(value) {
|
|
|
2304
2442
|
value !== null &&
|
|
2305
2443
|
value[IS_STORE] === true);
|
|
2306
2444
|
}
|
|
2307
|
-
function isIndexProp(prop) {
|
|
2308
|
-
return typeof prop === 'string' && prop.trim() !== '' && !isNaN(+prop);
|
|
2309
|
-
}
|
|
2310
2445
|
function isRecord(value) {
|
|
2311
2446
|
if (value === null || typeof value !== 'object')
|
|
2312
2447
|
return false;
|
|
2448
|
+
if (value[OPAQUE] === true)
|
|
2449
|
+
return false; // opaque → leaf
|
|
2313
2450
|
const proto = Object.getPrototypeOf(value);
|
|
2314
2451
|
return proto === Object.prototype || proto === null;
|
|
2315
2452
|
}
|
|
2453
|
+
/**
|
|
2454
|
+
* @internal
|
|
2455
|
+
* Resolves the vivify shape for a node from its current value: a present record/array is a
|
|
2456
|
+
* certainty we keep (cached in the derivation, so it survives the value being nulled); an
|
|
2457
|
+
* unknown value (`null`/`undefined`) defers to the caller's option. Off stays off.
|
|
2458
|
+
*/
|
|
2459
|
+
function resolveVivify(sample, option) {
|
|
2460
|
+
if (!option)
|
|
2461
|
+
return false;
|
|
2462
|
+
if (Array.isArray(sample))
|
|
2463
|
+
return 'array';
|
|
2464
|
+
if (isRecord(sample))
|
|
2465
|
+
return 'object';
|
|
2466
|
+
return 'auto';
|
|
2467
|
+
}
|
|
2468
|
+
function hasOwnKey(value, key) {
|
|
2469
|
+
return value != null && Object.hasOwn(value, key);
|
|
2470
|
+
}
|
|
2316
2471
|
/**
|
|
2317
2472
|
* @internal
|
|
2318
2473
|
* Makes an array store
|
|
2319
2474
|
*/
|
|
2320
|
-
function toArrayStore(source, injector) {
|
|
2475
|
+
function toArrayStore(source, injector, vivify) {
|
|
2321
2476
|
if (isStore(source))
|
|
2322
2477
|
return source;
|
|
2323
2478
|
const isMutableSource = isMutable(source);
|
|
@@ -2397,31 +2552,39 @@ function toArrayStore(source, injector) {
|
|
|
2397
2552
|
const value = untracked(target);
|
|
2398
2553
|
const valueIsArray = Array.isArray(value);
|
|
2399
2554
|
const valueIsRecord = isRecord(value);
|
|
2555
|
+
const nodeVivify = resolveVivify(value, vivify);
|
|
2556
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
2400
2557
|
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
2401
2558
|
isMutableSource &&
|
|
2402
2559
|
typeof value[idx] === 'object'
|
|
2403
2560
|
? () => false
|
|
2404
2561
|
: undefined;
|
|
2405
2562
|
const computation = valueIsRecord
|
|
2406
|
-
? derived(target, idx, {
|
|
2563
|
+
? derived(target, idx, {
|
|
2564
|
+
equal: equalFn,
|
|
2565
|
+
vivify: nodeVivify,
|
|
2566
|
+
})
|
|
2407
2567
|
: derived(target, {
|
|
2408
2568
|
from: (v) => v?.[idx],
|
|
2409
2569
|
onChange: (newValue) => target.update((v) => {
|
|
2410
|
-
|
|
2411
|
-
|
|
2570
|
+
const container = vivifyFn(v, idx);
|
|
2571
|
+
if (container === null || container === undefined)
|
|
2572
|
+
return container;
|
|
2412
2573
|
try {
|
|
2413
|
-
|
|
2574
|
+
container[idx] = newValue;
|
|
2414
2575
|
}
|
|
2415
2576
|
catch (e) {
|
|
2416
2577
|
if (isDevMode())
|
|
2417
2578
|
console.error(`[store] Failed to set property "${String(idx)}"`, e);
|
|
2418
2579
|
}
|
|
2419
|
-
return
|
|
2580
|
+
return container;
|
|
2420
2581
|
}),
|
|
2421
2582
|
});
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2583
|
+
const childSample = untracked(computation);
|
|
2584
|
+
const childVivify = resolveVivify(childSample, vivify);
|
|
2585
|
+
const proxy = Array.isArray(childSample)
|
|
2586
|
+
? toArrayStore(computation, injector, childVivify)
|
|
2587
|
+
: toStore(computation, injector, childVivify);
|
|
2425
2588
|
const ref = new WeakRef(proxy);
|
|
2426
2589
|
storeCache.set(idx, ref);
|
|
2427
2590
|
PROXY_CLEANUP.register(proxy, { target, prop: idx }, ref);
|
|
@@ -2438,7 +2601,7 @@ function toArrayStore(source, injector) {
|
|
|
2438
2601
|
* const state = store({ user: { name: 'John' } });
|
|
2439
2602
|
* const nameSignal = state.user.name; // WritableSignal<string>
|
|
2440
2603
|
*/
|
|
2441
|
-
function toStore(source, injector) {
|
|
2604
|
+
function toStore(source, injector, vivify = false) {
|
|
2442
2605
|
if (isStore(source))
|
|
2443
2606
|
return source;
|
|
2444
2607
|
if (!injector)
|
|
@@ -2448,7 +2611,8 @@ function toStore(source, injector) {
|
|
|
2448
2611
|
: toWritable(source, () => {
|
|
2449
2612
|
// noop
|
|
2450
2613
|
});
|
|
2451
|
-
const
|
|
2614
|
+
const isWritableSource = isWritableSignal(source);
|
|
2615
|
+
const isMutableSource = isWritableSource && isMutable(writableSource);
|
|
2452
2616
|
const s = new Proxy(writableSource, {
|
|
2453
2617
|
has(_, prop) {
|
|
2454
2618
|
return Reflect.has(untracked(source), prop);
|
|
@@ -2476,10 +2640,16 @@ function toStore(source, injector) {
|
|
|
2476
2640
|
return true;
|
|
2477
2641
|
if (prop === 'asReadonlyStore')
|
|
2478
2642
|
return () => {
|
|
2479
|
-
if (!
|
|
2643
|
+
if (!isWritableSource)
|
|
2480
2644
|
return s;
|
|
2481
|
-
return untracked(() => toStore(source.asReadonly(), injector));
|
|
2645
|
+
return untracked(() => toStore(source.asReadonly(), injector, vivify));
|
|
2482
2646
|
};
|
|
2647
|
+
if (prop === 'extend')
|
|
2648
|
+
return (seed) => scopedStore(s, seed, isMutableSource
|
|
2649
|
+
? 'mutable'
|
|
2650
|
+
: isWritableSource
|
|
2651
|
+
? 'writable'
|
|
2652
|
+
: 'readonly', injector);
|
|
2483
2653
|
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
2484
2654
|
return target[prop];
|
|
2485
2655
|
let storeCache = PROXY_CACHE.get(target);
|
|
@@ -2498,31 +2668,36 @@ function toStore(source, injector) {
|
|
|
2498
2668
|
const value = untracked(target);
|
|
2499
2669
|
const valueIsRecord = isRecord(value);
|
|
2500
2670
|
const valueIsArray = Array.isArray(value);
|
|
2671
|
+
const nodeVivify = resolveVivify(value, vivify);
|
|
2672
|
+
const vivifyFn = createVivify(nodeVivify);
|
|
2501
2673
|
const equalFn = (valueIsRecord || valueIsArray) &&
|
|
2502
2674
|
isMutableSource &&
|
|
2503
2675
|
typeof value[prop] === 'object'
|
|
2504
2676
|
? () => false
|
|
2505
2677
|
: undefined;
|
|
2506
2678
|
const computation = valueIsRecord
|
|
2507
|
-
? derived(target, prop, { equal: equalFn })
|
|
2679
|
+
? derived(target, prop, { equal: equalFn, vivify: nodeVivify })
|
|
2508
2680
|
: derived(target, {
|
|
2509
2681
|
from: (v) => v?.[prop],
|
|
2510
2682
|
onChange: (newValue) => target.update((v) => {
|
|
2511
|
-
|
|
2512
|
-
|
|
2683
|
+
const container = vivifyFn(v, prop);
|
|
2684
|
+
if (container === null || container === undefined)
|
|
2685
|
+
return container;
|
|
2513
2686
|
try {
|
|
2514
|
-
|
|
2687
|
+
container[prop] = newValue;
|
|
2515
2688
|
}
|
|
2516
2689
|
catch (e) {
|
|
2517
2690
|
if (isDevMode())
|
|
2518
2691
|
console.error(`[store] Failed to set property "${String(prop)}"`, e);
|
|
2519
2692
|
}
|
|
2520
|
-
return
|
|
2693
|
+
return container;
|
|
2521
2694
|
}),
|
|
2522
2695
|
});
|
|
2523
|
-
const
|
|
2524
|
-
|
|
2525
|
-
|
|
2696
|
+
const childSample = untracked(computation);
|
|
2697
|
+
const childVivify = resolveVivify(childSample, vivify);
|
|
2698
|
+
const proxy = Array.isArray(childSample)
|
|
2699
|
+
? toArrayStore(computation, injector, childVivify)
|
|
2700
|
+
: toStore(computation, injector, childVivify);
|
|
2526
2701
|
const ref = new WeakRef(proxy);
|
|
2527
2702
|
storeCache.set(prop, ref);
|
|
2528
2703
|
PROXY_CLEANUP.register(proxy, { target, prop }, ref);
|
|
@@ -2531,19 +2706,119 @@ function toStore(source, injector) {
|
|
|
2531
2706
|
});
|
|
2532
2707
|
return s;
|
|
2533
2708
|
}
|
|
2709
|
+
/**
|
|
2710
|
+
* @internal
|
|
2711
|
+
* Backs `store.extend(...)`. Builds a scoped overlay over `parent`: the local layer (the seed
|
|
2712
|
+
* plus any keys created later) is its own signal and `parent` is its own signal, so the getter
|
|
2713
|
+
* routes each key by consulting BOTH — local first, then parent, else local (so a write to an
|
|
2714
|
+
* as-yet-unknown key lands locally). Inherited keys return the parent's own sub-store (shared
|
|
2715
|
+
* identity + two-way), while local keys never propagate upward. A merged `computed` is derived
|
|
2716
|
+
* only for whole-object reads / `has` / iteration — never for routing.
|
|
2717
|
+
*/
|
|
2718
|
+
function scopedStore(parent, seed, kind, injector) {
|
|
2719
|
+
const local = isSignal(seed)
|
|
2720
|
+
? toStore(seed, injector)
|
|
2721
|
+
: kind === 'mutable'
|
|
2722
|
+
? mutableStore(seed, { injector })
|
|
2723
|
+
: kind === 'readonly'
|
|
2724
|
+
? store(seed, { injector }).asReadonlyStore()
|
|
2725
|
+
: store(seed, { injector });
|
|
2726
|
+
const localValue = () => untracked(local);
|
|
2727
|
+
const parentValue = () => untracked(parent);
|
|
2728
|
+
const view = computed(() => ({
|
|
2729
|
+
...parent(),
|
|
2730
|
+
...local(),
|
|
2731
|
+
}));
|
|
2732
|
+
const splitSet = (next) => {
|
|
2733
|
+
const lv = localValue();
|
|
2734
|
+
const pv = parentValue();
|
|
2735
|
+
for (const key of Reflect.ownKeys(next)) {
|
|
2736
|
+
const layer = hasOwnKey(lv, key)
|
|
2737
|
+
? local
|
|
2738
|
+
: hasOwnKey(pv, key)
|
|
2739
|
+
? parent
|
|
2740
|
+
: local;
|
|
2741
|
+
layer[key].set(next[key]);
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
const base = toWritable(view, kind === 'readonly' ? () => undefined : splitSet, undefined, { pure: false });
|
|
2745
|
+
if (kind === 'mutable') {
|
|
2746
|
+
base.mutate = (updater) => splitSet(updater(untracked(view)));
|
|
2747
|
+
base.inline = (updater) => base.mutate((prev) => {
|
|
2748
|
+
updater(prev);
|
|
2749
|
+
return prev;
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
const scope = new Proxy(base, {
|
|
2753
|
+
get(target, prop) {
|
|
2754
|
+
if (prop === IS_STORE)
|
|
2755
|
+
return true;
|
|
2756
|
+
if (prop === SCOPE_PARENT)
|
|
2757
|
+
return parent;
|
|
2758
|
+
if (prop === 'extend')
|
|
2759
|
+
return (childSeed) => scopedStore(scope, childSeed, kind, injector);
|
|
2760
|
+
if (prop === 'asReadonlyStore')
|
|
2761
|
+
return () => toStore(computed(() => ({ ...parent(), ...local() })), injector);
|
|
2762
|
+
if (typeof prop === 'symbol' || SIGNAL_FN_PROP.has(prop))
|
|
2763
|
+
return target[prop];
|
|
2764
|
+
// Route by consulting both signals: local first, then parent, else local (new → local).
|
|
2765
|
+
if (hasOwnKey(localValue(), prop))
|
|
2766
|
+
return local[prop];
|
|
2767
|
+
if (hasOwnKey(parentValue(), prop))
|
|
2768
|
+
return parent[prop];
|
|
2769
|
+
return local[prop];
|
|
2770
|
+
},
|
|
2771
|
+
has(_, prop) {
|
|
2772
|
+
return hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop);
|
|
2773
|
+
},
|
|
2774
|
+
ownKeys() {
|
|
2775
|
+
return [
|
|
2776
|
+
...new Set([
|
|
2777
|
+
...Reflect.ownKeys(parentValue()),
|
|
2778
|
+
...Reflect.ownKeys(localValue()),
|
|
2779
|
+
]),
|
|
2780
|
+
];
|
|
2781
|
+
},
|
|
2782
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
2783
|
+
if (hasOwnKey(localValue(), prop) || hasOwnKey(parentValue(), prop))
|
|
2784
|
+
return { enumerable: true, configurable: true };
|
|
2785
|
+
return undefined;
|
|
2786
|
+
},
|
|
2787
|
+
getPrototypeOf() {
|
|
2788
|
+
return Object.prototype;
|
|
2789
|
+
},
|
|
2790
|
+
});
|
|
2791
|
+
return scope;
|
|
2792
|
+
}
|
|
2534
2793
|
/**
|
|
2535
2794
|
* Creates a WritableSignalStore from a value.
|
|
2536
2795
|
* @see {@link toStore}
|
|
2537
2796
|
*/
|
|
2538
2797
|
function store(value, opt) {
|
|
2539
|
-
return toStore(signal(value, opt), opt?.injector);
|
|
2798
|
+
return toStore(signal(value, opt), opt?.injector, opt?.vivify ?? false);
|
|
2540
2799
|
}
|
|
2541
2800
|
/**
|
|
2542
2801
|
* Creates a MutableSignalStore from a value.
|
|
2543
2802
|
* @see {@link toStore}
|
|
2544
2803
|
*/
|
|
2545
2804
|
function mutableStore(value, opt) {
|
|
2546
|
-
return toStore(mutable(value, opt), opt?.injector);
|
|
2805
|
+
return toStore(mutable(value, opt), opt?.injector, opt?.vivify ?? false);
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Marks a plain object as opaque so {@link store} treats it as an indivisible leaf
|
|
2809
|
+
* (returned whole, never deep-proxied) — the same way it treats a `Date` or `RegExp`.
|
|
2810
|
+
* The marker is a non-enumerable symbol, so it never appears in spreads or iteration.
|
|
2811
|
+
* Idempotent. Call before freezing (`defineProperty` fails on a frozen object).
|
|
2812
|
+
*
|
|
2813
|
+
* @example
|
|
2814
|
+
* const s = store({ config: opaque({ theme: 'dark', nested: { a: 1 } }) });
|
|
2815
|
+
* s.config(); // the whole object, not a child store
|
|
2816
|
+
* s.config.set(opaque({ theme: 'light', nested: { a: 2 } }));
|
|
2817
|
+
*/
|
|
2818
|
+
function opaque(value) {
|
|
2819
|
+
if (value[OPAQUE] !== true)
|
|
2820
|
+
Object.defineProperty(value, OPAQUE, { value: true, enumerable: false });
|
|
2821
|
+
return value;
|
|
2547
2822
|
}
|
|
2548
2823
|
|
|
2549
2824
|
// Internal dummy store for server-side rendering
|
|
@@ -3022,5 +3297,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
3022
3297
|
* Generated bundle index. Do not edit.
|
|
3023
3298
|
*/
|
|
3024
3299
|
|
|
3025
|
-
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 };
|
|
3300
|
+
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 };
|
|
3026
3301
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|