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