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