@mmstack/primitives 21.3.0 → 21.4.0
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 +18 -0
- package/fesm2022/mmstack-primitives.mjs +100 -44
- package/fesm2022/mmstack-primitives.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mmstack-primitives.d.ts +23 -6
package/README.md
CHANGED
|
@@ -119,6 +119,10 @@ Top-level array support isn't exposed yet — use `indexArray` / `keyArray` for
|
|
|
119
119
|
|
|
120
120
|
**Union leaves (perf opt-in).** `noUnionLeaves: true` promises no node ever flips between a leaf and a sub-store, so each node's leaf-ness is resolved once on first access and cached instead of staying reactive. Off by default — leave it off if a value can switch between a primitive and an object/array.
|
|
121
121
|
|
|
122
|
+
**Unions are fully supported by default.** A node may flip between array ↔ record ↔ primitive ↔ `null` freely: routing (`keys`/iteration/prototype) follows the live kind, and a child signal you grabbed **before** a flip stays correct after it — reads resolve against the new shape (`undefined` through a `null` parent, no throw) and writes copy by the container's live shape, so writing through a pre-flip child never turns an array into a plain object.
|
|
123
|
+
|
|
124
|
+
> Reserved keys: `set`, `update`, `mutate`, `inline`, `asReadonly` (and `extend`, until its removal next minor) resolve to the signal's own methods, so record keys with those names aren't reachable as child stores — read them off the value (`s().set`) instead.
|
|
125
|
+
|
|
122
126
|
### `extendStore` (scoped overlay)
|
|
123
127
|
|
|
124
128
|
`extendStore(store, 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.
|
|
@@ -461,6 +465,15 @@ const t = startTransaction(() => applyBulkEdit()); // live state updates; the di
|
|
|
461
465
|
await t.done; // committed, display revealed in one frame
|
|
462
466
|
```
|
|
463
467
|
|
|
468
|
+
Every exit settles: a throwing body rolls back, and if the calling context is **destroyed
|
|
469
|
+
mid-flight** the hold is released (writes kept) and `done` resolves — a transaction can never
|
|
470
|
+
leave a surviving ancestor scope frozen.
|
|
471
|
+
|
|
472
|
+
Attribution is **per transaction**: a load already in flight when it starts is not adopted —
|
|
473
|
+
it can neither commit the transaction early nor block its settle. (The same applies to
|
|
474
|
+
`startTransition`.) A pre-existing flight re-triggered by the transaction's own writes counts
|
|
475
|
+
once it restarts.
|
|
476
|
+
|
|
464
477
|
### `holdUntilReady`
|
|
465
478
|
|
|
466
479
|
The **structural** counterpart to `keepPrevious`: where that holds a _value_ through a reload, this holds a _structure_ through a swap. Given a `target` signal and a `ready` predicate, it keeps yielding the previous value until `ready()` is true, then swaps to the current target. Mount the incoming structure off to the side so its resources can settle and flip `ready`, keep showing the held one meanwhile, and let the old one go once `ready` releases the swap. (`@mmstack/router-core`'s `<mm-transition-outlet>` is this pattern applied to routes.)
|
|
@@ -677,6 +690,11 @@ outer one on the same tree (e.g. a nested sortable). Reads are throttled
|
|
|
677
690
|
(`throttle`, default 16ms); `drag.unthrottled()` exposes the un-throttled view
|
|
678
691
|
for logic that needs the exact release position.
|
|
679
692
|
|
|
693
|
+
The idle state carries the **end reason**: `cancelled` is `true` when the gesture
|
|
694
|
+
was aborted (Escape, `pointercancel`, `.cancel()`) rather than released, and stays
|
|
695
|
+
set until the next `pointerdown` — so a drag consumer can tell "drop here" from
|
|
696
|
+
"abort" (`@mmstack/dnd` uses this to cancel instead of committing).
|
|
697
|
+
|
|
680
698
|
```typescript
|
|
681
699
|
import { sensor } from '@mmstack/primitives';
|
|
682
700
|
|
|
@@ -628,6 +628,38 @@ function provideForwardingTransitionScope() {
|
|
|
628
628
|
function getTransitionScope(injector) {
|
|
629
629
|
return injector.get(TRANSITION_SCOPE, null);
|
|
630
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* @internal Transaction-attributed pending for `startTransition`/`startTransaction`: like
|
|
633
|
+
* `scope.pending`, but loads already in flight when the tracker is created are NOT attributed —
|
|
634
|
+
* a pre-existing background load can neither settle the transaction early nor block its settle
|
|
635
|
+
* forever. A pre-existing flight is excluded only until it first settles; a later re-trigger of
|
|
636
|
+
* the same resource (e.g. the transaction's write changed its request) counts as the
|
|
637
|
+
* transaction's own work.
|
|
638
|
+
*/
|
|
639
|
+
function createAttributedPending(scope) {
|
|
640
|
+
const isInFlight = (ref) => {
|
|
641
|
+
const s = untracked(ref.status);
|
|
642
|
+
return s === 'loading' || s === 'reloading';
|
|
643
|
+
};
|
|
644
|
+
const preexisting = new Set(untracked(scope.resources).filter(isInFlight));
|
|
645
|
+
return computed(() => {
|
|
646
|
+
let pending = false;
|
|
647
|
+
for (const ref of scope.resources()) {
|
|
648
|
+
const s = ref.status();
|
|
649
|
+
const loading = s === 'loading' || s === 'reloading';
|
|
650
|
+
if (preexisting.has(ref)) {
|
|
651
|
+
// deletes are monotonic, so this stays sound under re-computation
|
|
652
|
+
if (loading)
|
|
653
|
+
continue;
|
|
654
|
+
preexisting.delete(ref);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
if (loading)
|
|
658
|
+
pending = true;
|
|
659
|
+
}
|
|
660
|
+
return pending;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
631
663
|
/**
|
|
632
664
|
* Returns a register function bound to the nearest transition scope: it adds a resource
|
|
633
665
|
* to the scope and removes it when the caller's injection context is destroyed. Pass any
|
|
@@ -665,38 +697,43 @@ function registerResource(res, opt) {
|
|
|
665
697
|
function injectStartTransition() {
|
|
666
698
|
const scope = injectTransitionScope();
|
|
667
699
|
const injector = inject(Injector);
|
|
700
|
+
const destroyRef = inject(DestroyRef);
|
|
668
701
|
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
669
702
|
return (fn) => {
|
|
703
|
+
// attributed: loads already in flight when the transition starts are not ours —
|
|
704
|
+
// they can neither settle this transition early nor block it forever
|
|
705
|
+
const pending = createAttributedPending(scope);
|
|
670
706
|
untracked(fn);
|
|
671
707
|
let sawPending = false;
|
|
672
708
|
const done = new Promise((resolve) => {
|
|
709
|
+
const settle = () => {
|
|
710
|
+
releaseDestroy();
|
|
711
|
+
watcher.destroy();
|
|
712
|
+
resolve();
|
|
713
|
+
};
|
|
673
714
|
const watcher = effect(() => {
|
|
674
|
-
const p =
|
|
715
|
+
const p = pending();
|
|
675
716
|
if (p)
|
|
676
717
|
sawPending = true;
|
|
677
718
|
// settle: requests went in flight and then drained
|
|
678
|
-
if (sawPending && !p)
|
|
679
|
-
|
|
680
|
-
resolve();
|
|
681
|
-
}
|
|
719
|
+
if (sawPending && !p)
|
|
720
|
+
settle();
|
|
682
721
|
}, { ...(ngDevMode ? { debugName: "watcher" } : /* istanbul ignore next */ {}), injector });
|
|
722
|
+
// a destroy mid-flight kills the watcher — resolve so awaiters never hang
|
|
723
|
+
const releaseDestroy = destroyRef.onDestroy(settle);
|
|
683
724
|
if (onServer) {
|
|
684
|
-
if (!untracked(
|
|
685
|
-
|
|
686
|
-
resolve();
|
|
687
|
-
}
|
|
725
|
+
if (!untracked(pending))
|
|
726
|
+
settle();
|
|
688
727
|
return;
|
|
689
728
|
}
|
|
690
729
|
// no-async fallback: once the reactive system has processed the writes (afterNextRender),
|
|
691
730
|
// if nothing ever went in flight, the transition is already complete.
|
|
692
731
|
afterNextRender(() => {
|
|
693
|
-
if (!sawPending && !untracked(
|
|
694
|
-
|
|
695
|
-
resolve();
|
|
696
|
-
}
|
|
732
|
+
if (!sawPending && !untracked(pending))
|
|
733
|
+
settle();
|
|
697
734
|
}, { injector });
|
|
698
735
|
});
|
|
699
|
-
return { pending
|
|
736
|
+
return { pending, done };
|
|
700
737
|
};
|
|
701
738
|
}
|
|
702
739
|
|
|
@@ -767,8 +804,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
767
804
|
}] });
|
|
768
805
|
/**
|
|
769
806
|
* Unscoped suspense boundary — **reads the ambient scope** instead of providing one. For cases where
|
|
770
|
-
* the resources to coordinate are registered *above* the boundary
|
|
771
|
-
* manifests/connectors register at a higher injector), so the boundary observes that outer scope
|
|
807
|
+
* the resources to coordinate are registered *above* the boundary so the boundary observes that outer scope
|
|
772
808
|
* rather than opening a fresh one. Pair with a `provideTransitionScope()` (or another boundary) in an
|
|
773
809
|
* ancestor.
|
|
774
810
|
*/
|
|
@@ -833,9 +869,13 @@ function runInTransaction(txn, fn) {
|
|
|
833
869
|
function injectStartTransaction() {
|
|
834
870
|
const scope = injectTransitionScope();
|
|
835
871
|
const injector = inject(Injector);
|
|
872
|
+
const destroyRef = inject(DestroyRef);
|
|
836
873
|
const onServer = isPlatformServer(inject(PLATFORM_ID, { optional: true }) ?? 'browser');
|
|
837
874
|
return (fn) => {
|
|
838
875
|
const txn = createTransaction();
|
|
876
|
+
// attributed: loads already in flight when the transaction starts are not ours —
|
|
877
|
+
// they can neither commit this transaction early nor block its settle forever
|
|
878
|
+
const pending = createAttributedPending(scope);
|
|
839
879
|
// Hold BEFORE the writes, so the display freezes at pre-transaction values.
|
|
840
880
|
scope.beginHold();
|
|
841
881
|
let finished = false;
|
|
@@ -852,6 +892,7 @@ function injectStartTransaction() {
|
|
|
852
892
|
if (finished)
|
|
853
893
|
return;
|
|
854
894
|
finished = true;
|
|
895
|
+
releaseDestroy();
|
|
855
896
|
watcher?.destroy();
|
|
856
897
|
if (restore)
|
|
857
898
|
txn.restore();
|
|
@@ -860,6 +901,10 @@ function injectStartTransaction() {
|
|
|
860
901
|
scope.endHold();
|
|
861
902
|
resolveDone();
|
|
862
903
|
};
|
|
904
|
+
// The scope may outlive the calling context (a component transacting on an ancestor
|
|
905
|
+
// boundary): a destroy mid-flight kills the settle watcher, so without this the hold
|
|
906
|
+
// would leak and freeze the surviving scope forever. Keep the writes — they landed live.
|
|
907
|
+
const releaseDestroy = destroyRef.onDestroy(() => finish(false));
|
|
863
908
|
try {
|
|
864
909
|
runInTransaction(txn, fn);
|
|
865
910
|
}
|
|
@@ -869,25 +914,25 @@ function injectStartTransaction() {
|
|
|
869
914
|
}
|
|
870
915
|
let sawPending = false;
|
|
871
916
|
watcher = effect(() => {
|
|
872
|
-
const p =
|
|
917
|
+
const p = pending();
|
|
873
918
|
if (p)
|
|
874
919
|
sawPending = true;
|
|
875
920
|
if (sawPending && !p)
|
|
876
921
|
finish(false);
|
|
877
922
|
}, { injector });
|
|
878
923
|
if (onServer) {
|
|
879
|
-
if (!untracked(
|
|
924
|
+
if (!untracked(pending))
|
|
880
925
|
finish(false);
|
|
881
926
|
}
|
|
882
927
|
else {
|
|
883
928
|
// no-async fallback: if nothing ever went in flight, settle once the writes are processed.
|
|
884
929
|
afterNextRender(() => {
|
|
885
|
-
if (!sawPending && !untracked(
|
|
930
|
+
if (!sawPending && !untracked(pending))
|
|
886
931
|
finish(false);
|
|
887
932
|
}, { injector });
|
|
888
933
|
}
|
|
889
934
|
return {
|
|
890
|
-
pending
|
|
935
|
+
pending,
|
|
891
936
|
done,
|
|
892
937
|
abort: () => finish(true),
|
|
893
938
|
};
|
|
@@ -2895,9 +2940,13 @@ const IDLE = {
|
|
|
2895
2940
|
button: -1,
|
|
2896
2941
|
pointerType: '',
|
|
2897
2942
|
origin: null,
|
|
2943
|
+
cancelled: false,
|
|
2898
2944
|
};
|
|
2945
|
+
/** Terminal state of an aborted gesture — same idle shape, `cancelled: true`. */
|
|
2946
|
+
const CANCELLED = { ...IDLE, cancelled: true };
|
|
2899
2947
|
function stateEqual(a, b) {
|
|
2900
2948
|
return (a.active === b.active &&
|
|
2949
|
+
a.cancelled === b.cancelled &&
|
|
2901
2950
|
a.pointerId === b.pointerId &&
|
|
2902
2951
|
a.current.x === b.current.x &&
|
|
2903
2952
|
a.current.y === b.current.y &&
|
|
@@ -2975,7 +3024,7 @@ function createPointerDrag(opt) {
|
|
|
2975
3024
|
ctrl: e.ctrlKey,
|
|
2976
3025
|
meta: e.metaKey,
|
|
2977
3026
|
});
|
|
2978
|
-
const end = () => {
|
|
3027
|
+
const end = (cancelled = false) => {
|
|
2979
3028
|
gesture?.abort();
|
|
2980
3029
|
gesture = null;
|
|
2981
3030
|
activePointerId = null;
|
|
@@ -2983,8 +3032,8 @@ function createPointerDrag(opt) {
|
|
|
2983
3032
|
activePointerType = '';
|
|
2984
3033
|
activeOrigin = null;
|
|
2985
3034
|
activated = false;
|
|
2986
|
-
state.set(IDLE);
|
|
2987
|
-
state.flush(); // terminal transition: reflect
|
|
3035
|
+
state.set(cancelled ? CANCELLED : IDLE);
|
|
3036
|
+
state.flush(); // terminal transition: reflect idle now, not on the trailing edge
|
|
2988
3037
|
};
|
|
2989
3038
|
const onMove = (e) => {
|
|
2990
3039
|
if (e.pointerId !== activePointerId)
|
|
@@ -3004,6 +3053,7 @@ function createPointerDrag(opt) {
|
|
|
3004
3053
|
button: activeButton, // pointermove button is -1; keep the down-button
|
|
3005
3054
|
pointerType: activePointerType,
|
|
3006
3055
|
origin: activeOrigin,
|
|
3056
|
+
cancelled: false,
|
|
3007
3057
|
});
|
|
3008
3058
|
};
|
|
3009
3059
|
const onUp = (e) => {
|
|
@@ -3012,11 +3062,11 @@ function createPointerDrag(opt) {
|
|
|
3012
3062
|
};
|
|
3013
3063
|
const onCancel = (e) => {
|
|
3014
3064
|
if (e.pointerId === activePointerId)
|
|
3015
|
-
end();
|
|
3065
|
+
end(true);
|
|
3016
3066
|
};
|
|
3017
3067
|
const onKey = (e) => {
|
|
3018
3068
|
if (e.key === 'Escape' && activePointerId !== null)
|
|
3019
|
-
end();
|
|
3069
|
+
end(true);
|
|
3020
3070
|
};
|
|
3021
3071
|
const onDown = (el) => (e) => {
|
|
3022
3072
|
if (activePointerId !== null)
|
|
@@ -3061,6 +3111,7 @@ function createPointerDrag(opt) {
|
|
|
3061
3111
|
button: e.button,
|
|
3062
3112
|
pointerType: activePointerType,
|
|
3063
3113
|
origin: activeOrigin,
|
|
3114
|
+
cancelled: false,
|
|
3064
3115
|
});
|
|
3065
3116
|
};
|
|
3066
3117
|
const attach = (el) => {
|
|
@@ -3070,7 +3121,7 @@ function createPointerDrag(opt) {
|
|
|
3070
3121
|
});
|
|
3071
3122
|
return () => {
|
|
3072
3123
|
controller.abort();
|
|
3073
|
-
end();
|
|
3124
|
+
end(true); // teardown mid-gesture is an abort, not a drop
|
|
3074
3125
|
};
|
|
3075
3126
|
};
|
|
3076
3127
|
if (isSignal(target)) {
|
|
@@ -3088,7 +3139,7 @@ function createPointerDrag(opt) {
|
|
|
3088
3139
|
}
|
|
3089
3140
|
const base = state.asReadonly();
|
|
3090
3141
|
base.unthrottled = state.original;
|
|
3091
|
-
base.cancel = end;
|
|
3142
|
+
base.cancel = () => end(true);
|
|
3092
3143
|
return base;
|
|
3093
3144
|
}
|
|
3094
3145
|
|
|
@@ -3684,30 +3735,35 @@ function getCachedChild(target, prop, build, cache, cleanupRegistry) {
|
|
|
3684
3735
|
cleanupRegistry.register(proxy, { target, prop }, ref);
|
|
3685
3736
|
return proxy;
|
|
3686
3737
|
}
|
|
3738
|
+
/**
|
|
3739
|
+
* @internal Whether a mutable parent's child value must always re-notify: in-place mutation
|
|
3740
|
+
* keeps an object child's reference stable, so `Object.is` would swallow the change. Decided
|
|
3741
|
+
* per-VALUE (not snapshotted at build) so a union child that becomes an object later still
|
|
3742
|
+
* propagates parent-level mutations.
|
|
3743
|
+
*/
|
|
3744
|
+
function mutableChildEqual(a, b) {
|
|
3745
|
+
if (typeof a === 'object' && a !== null)
|
|
3746
|
+
return false;
|
|
3747
|
+
return Object.is(a, b);
|
|
3748
|
+
}
|
|
3687
3749
|
/**
|
|
3688
3750
|
* @internal Builds the derived child signal for `prop` and wraps it as an array/object substore.
|
|
3689
|
-
*
|
|
3690
|
-
*
|
|
3691
|
-
* is constructed.
|
|
3751
|
+
* Both the read (`v?.[prop]`) and the write (`createFallbackOnChange` copies by the container's
|
|
3752
|
+
* LIVE shape) are shape-adaptive, so a child cached before an array↔record↔null union flip stays
|
|
3753
|
+
* correct after it. The only place a child node is constructed — shared by every container kind.
|
|
3692
3754
|
*/
|
|
3693
3755
|
function buildChildNode(target, prop, isMutableSource, options) {
|
|
3694
3756
|
const value = untracked(target);
|
|
3695
|
-
const valueIsRecord = isRecord(value);
|
|
3696
|
-
const valueIsArray = Array.isArray(value);
|
|
3697
3757
|
const nodeVivify = resolveVivify(value, options.vivify);
|
|
3698
3758
|
const vivifyFn = createVivify(nodeVivify);
|
|
3699
|
-
const equalFn = (
|
|
3700
|
-
|
|
3701
|
-
typeof value[prop] === 'object'
|
|
3702
|
-
? () => false
|
|
3759
|
+
const equalFn = isMutableSource && (isRecord(value) || Array.isArray(value))
|
|
3760
|
+
? mutableChildEqual
|
|
3703
3761
|
: undefined;
|
|
3704
|
-
const computation =
|
|
3705
|
-
|
|
3706
|
-
:
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
equal: equalFn,
|
|
3710
|
-
});
|
|
3762
|
+
const computation = derived(target, {
|
|
3763
|
+
from: (v) => v?.[prop],
|
|
3764
|
+
onChange: createFallbackOnChange(target, prop, vivifyFn, isMutableSource),
|
|
3765
|
+
equal: equalFn,
|
|
3766
|
+
});
|
|
3711
3767
|
const childSample = untracked(computation);
|
|
3712
3768
|
const childVivify = resolveVivify(childSample, options.vivify);
|
|
3713
3769
|
const proxy = toStore(computation, options);
|
|
@@ -4592,5 +4648,5 @@ function withHistory(sourceOrValue, opt) {
|
|
|
4592
4648
|
* Generated bundle index. Do not edit.
|
|
4593
4649
|
*/
|
|
4594
4650
|
|
|
4595
|
-
export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
4651
|
+
export { MmActivity, PAUSABLE_OPTIONS, SuspenseBoundary, SuspenseBoundaryBase, UnscopedSuspenseBoundary, activeTransaction, batteryStatus, chunked, clipboard, combineWith, createAttributedPending, createForwardingScope, createTransaction, createTransitionScope, debounce, debounced, derived, distinct, elementSize, elementVisibility, extendStore, filter, filterWith, focusWithin, forkStore, geolocation, getTransitionScope, holdUntilReady, idle, indexArray, injectPaused, injectRegisterResource, injectStartTransaction, injectStartTransition, injectTransitionScope, isDerivation, isLeaf, isMutable, isOpaque, isStore, keepPrevious, keyArray, map, mapArray, mapObject, mediaQuery, merge3, mousePosition, mutable, mutableStore, nestedEffect, networkStatus, opaque, orientation, pageVisibility, pairwise, pausableComputed, pausableEffect, pausableSignal, pipeable, piped, pointerDrag, pooled, pooledArray, pooledMap, pooledSet, prefersDarkMode, prefersReducedMotion, provideForwardingTransitionScope, providePausableOptions, providePaused, provideTransitionScope, registerResource, resolvePause, scan, scrollPosition, select, sensor, sensors, signalFromEvent, startWith, store, stored, tabSync, tap, throttle, throttled, toFakeDerivation, toFakeSignalDerivation, toStore, toWritable, until, windowSize, withHistory };
|
|
4596
4652
|
//# sourceMappingURL=mmstack-primitives.mjs.map
|