@sladg/apex-state 3.7.3 → 3.9.1
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/dist/index.d.ts +446 -13
- package/dist/index.js +157 -31
- package/dist/index.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +33 -15
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -497,9 +497,154 @@ type ConcernRegistrationMap<DATA extends object, CONCERNS extends readonly any[]
|
|
|
497
497
|
}>;
|
|
498
498
|
|
|
499
499
|
/**
|
|
500
|
-
*
|
|
500
|
+
* Lazy listener validators — O(N) base + O(K) per-listener validation
|
|
501
501
|
*
|
|
502
|
-
*
|
|
502
|
+
* These types power the `listeners()` helper function that scales to large state
|
|
503
|
+
* types without hitting TS2589.
|
|
504
|
+
*
|
|
505
|
+
* Unlike the direct `ListenerRegistration<DATA>[]` type which resolves
|
|
506
|
+
* `DeepKey<DATA>` for path/scope on every element, these validators only evaluate
|
|
507
|
+
* `DeepValue` for the K listeners you actually write.
|
|
508
|
+
*
|
|
509
|
+
* Valid listener combinations:
|
|
510
|
+
* 1. `{ path: string, fn }` — scope defaults to path → scoped state
|
|
511
|
+
* 2. `{ path: string, scope: null, fn }` — explicit null scope → full DATA
|
|
512
|
+
* 3. `{ path: string, scope: string, fn }` — scope must be prefix of path
|
|
513
|
+
* 4. `{ path: null, fn }` — watch everything, scope defaults to null → full DATA
|
|
514
|
+
* 5. `{ path: null, scope: null, fn }` — same as above, explicit
|
|
515
|
+
*
|
|
516
|
+
* **Inline fn restriction**: Fns with untyped (any) parameters are rejected.
|
|
517
|
+
* Use `OnStateListener<DATA, ScopedState>` to explicitly type your fn.
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* True if Prefix is a dot-separated ancestor of Path (or equals Path).
|
|
522
|
+
*
|
|
523
|
+
* @example
|
|
524
|
+
* IsPrefixOf<'user', 'user'> → true (equal)
|
|
525
|
+
* IsPrefixOf<'user', 'user.name'> → true (parent)
|
|
526
|
+
* IsPrefixOf<'user', 'username'> → false (different segment)
|
|
527
|
+
* IsPrefixOf<'cart', 'user.name'> → false (unrelated)
|
|
528
|
+
*/
|
|
529
|
+
type IsPrefixOf<Prefix extends string, Path extends string> = Path extends Prefix ? true : Path extends `${Prefix}.${string}` ? true : false;
|
|
530
|
+
/**
|
|
531
|
+
* Derive the sub-state type from scope.
|
|
532
|
+
* - null scope → full DATA
|
|
533
|
+
* - string scope → DeepValue<DATA, Scope>
|
|
534
|
+
*
|
|
535
|
+
* Uses `Scope & string` to satisfy DeepValue's string constraint
|
|
536
|
+
* when Scope is inferred from a mapped type.
|
|
537
|
+
*/
|
|
538
|
+
type ScopedState<DATA extends object, Scope> = Scope extends null ? DATA : Scope extends string ? DeepValue<DATA, Scope & string> : DATA;
|
|
539
|
+
/**
|
|
540
|
+
* Extract the effective scope from a listener element.
|
|
541
|
+
*
|
|
542
|
+
* When 'scope' key exists with a non-undefined value, returns that value.
|
|
543
|
+
* When 'scope' key is missing or undefined, returns the path as default.
|
|
544
|
+
*/
|
|
545
|
+
type EffectiveScope<T, P> = 'scope' extends keyof T ? Exclude<T[keyof T & 'scope'], undefined> extends never ? P : Exclude<T[keyof T & 'scope'], undefined> : P;
|
|
546
|
+
/**
|
|
547
|
+
* Detect if a function's first parameter is `any`.
|
|
548
|
+
* Uses the `0 extends 1 & T` trick: only true when T is `any`.
|
|
549
|
+
*
|
|
550
|
+
* When true, the fn has untyped parameters (e.g., `(_changes, _state) => ...`
|
|
551
|
+
* without explicit typing). We reject these to force users to use
|
|
552
|
+
* `OnStateListener<DATA, ScopedState>` for type safety.
|
|
553
|
+
*/
|
|
554
|
+
type HasAnyFirstParam<F> = F extends (...args: infer A) => any ? A extends [infer P1, ...any[]] ? 0 extends 1 & P1 ? true : false : false : false;
|
|
555
|
+
/**
|
|
556
|
+
* Error marker type for untyped listener fns.
|
|
557
|
+
* Produces a clear error message when the user passes an untyped inline fn.
|
|
558
|
+
*/
|
|
559
|
+
type TypedFnRequired<DATA extends object, SUB_STATE, META extends GenericMeta> = OnStateListener<DATA, SUB_STATE, META> & {
|
|
560
|
+
__error: 'Listener fn must be explicitly typed. Use OnStateListener<DATA, ScopedState> or type the parameters directly.';
|
|
561
|
+
};
|
|
562
|
+
/**
|
|
563
|
+
* Returns the fn type for a listener element, rejecting untyped fns.
|
|
564
|
+
*
|
|
565
|
+
* When the user's fn has `any` params (implicit from inference), returns
|
|
566
|
+
* a branded error type that produces a clear type error.
|
|
567
|
+
* When the fn is explicitly typed (standalone or annotated), returns the
|
|
568
|
+
* expected OnStateListener type for assignability checking.
|
|
569
|
+
*/
|
|
570
|
+
type ValidatedFn<DATA extends object, META extends GenericMeta, SUB_STATE, F> = HasAnyFirstParam<F> extends true ? TypedFnRequired<DATA, SUB_STATE, META> : OnStateListener<DATA, SUB_STATE, META>;
|
|
571
|
+
/**
|
|
572
|
+
* Validates a single listener element.
|
|
573
|
+
*
|
|
574
|
+
* When scope is omitted (undefined), it defaults to path:
|
|
575
|
+
* - path is string → fn gets DeepValue<DATA, path>
|
|
576
|
+
* - path is null → fn gets full DATA
|
|
577
|
+
*
|
|
578
|
+
* When scope is explicitly provided:
|
|
579
|
+
* - scope: null → fn gets full DATA
|
|
580
|
+
* - scope: string → must be prefix of path, fn gets DeepValue<DATA, scope>
|
|
581
|
+
*
|
|
582
|
+
* Rejects fns with untyped (any) parameters — forces explicit typing via
|
|
583
|
+
* OnStateListener<DATA, ScopedState>.
|
|
584
|
+
*/
|
|
585
|
+
type CheckListenerElement<DATA extends object, META extends GenericMeta, T> = T extends {
|
|
586
|
+
path: infer P;
|
|
587
|
+
fn: infer F;
|
|
588
|
+
} ? EffectiveScope<T, P> extends infer ES ? [
|
|
589
|
+
P,
|
|
590
|
+
ES
|
|
591
|
+
] extends [string, string] ? IsPrefixOf<ES & string, P & string> extends true ? 'scope' extends keyof T ? {
|
|
592
|
+
path: P;
|
|
593
|
+
scope: ES;
|
|
594
|
+
fn: ValidatedFn<DATA, META, ScopedState<DATA, ES>, F>;
|
|
595
|
+
} : {
|
|
596
|
+
path: P;
|
|
597
|
+
scope?: undefined;
|
|
598
|
+
fn: ValidatedFn<DATA, META, ScopedState<DATA, ES>, F>;
|
|
599
|
+
} : {
|
|
600
|
+
path: P;
|
|
601
|
+
scope: never;
|
|
602
|
+
fn: OnStateListener<DATA, never, META>;
|
|
603
|
+
} : [
|
|
604
|
+
P,
|
|
605
|
+
ES
|
|
606
|
+
] extends [string, null] ? {
|
|
607
|
+
path: P;
|
|
608
|
+
scope: null;
|
|
609
|
+
fn: ValidatedFn<DATA, META, DATA, F>;
|
|
610
|
+
} : [
|
|
611
|
+
P,
|
|
612
|
+
ES
|
|
613
|
+
] extends [null, null] ? 'scope' extends keyof T ? {
|
|
614
|
+
path: null;
|
|
615
|
+
scope: null;
|
|
616
|
+
fn: ValidatedFn<DATA, META, DATA, F>;
|
|
617
|
+
} : {
|
|
618
|
+
path: null;
|
|
619
|
+
scope?: undefined;
|
|
620
|
+
fn: ValidatedFn<DATA, META, DATA, F>;
|
|
621
|
+
} : T : T : T;
|
|
622
|
+
/**
|
|
623
|
+
* Validates an array of listener objects lazily — O(K) where K = listeners written.
|
|
624
|
+
*
|
|
625
|
+
* Per element:
|
|
626
|
+
* 1. path must be ResolvableDeepKey<DATA> | null (enforced by function constraint)
|
|
627
|
+
* 2. scope is optional — defaults to path when omitted
|
|
628
|
+
* 3. When both non-null: scope must be prefix of path
|
|
629
|
+
* 4. fn must be explicitly typed (rejects untyped `any` params)
|
|
630
|
+
* 5. fn receives correctly-typed scoped state
|
|
631
|
+
*/
|
|
632
|
+
type CheckListeners<DATA extends object, META extends GenericMeta, T extends readonly {
|
|
633
|
+
path: string | null;
|
|
634
|
+
scope?: string | null | undefined;
|
|
635
|
+
fn: (...args: any[]) => any;
|
|
636
|
+
}[], _Depth extends number = DefaultDepth> = {
|
|
637
|
+
[I in keyof T]: CheckListenerElement<DATA, META, T[I]>;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Direct pair types — O(N²) path unions for side effects
|
|
642
|
+
*
|
|
643
|
+
* These types enumerate all valid path combinations eagerly.
|
|
644
|
+
* Simple to use as type annotations but hit TS2589 at ~1,500 paths.
|
|
645
|
+
*
|
|
646
|
+
* For large state types, use the curried helper functions (syncPairs, flipPairs, etc.)
|
|
647
|
+
* which use lazy validators from `pair-validators.ts` instead.
|
|
503
648
|
*
|
|
504
649
|
* @example
|
|
505
650
|
* ```typescript
|
|
@@ -527,6 +672,11 @@ type PathsWithSameValueAs<DATA extends object, PATH extends ResolvableDeepKey<DA
|
|
|
527
672
|
* A tuple of two paths that must have the same value type.
|
|
528
673
|
* Format: [path1, path2]
|
|
529
674
|
*
|
|
675
|
+
* **Scaling note**: This type is O(N²) where N = number of paths. It hits TS2589
|
|
676
|
+
* at ~1,500 paths. For large state types, use the `syncPairs()` helper function
|
|
677
|
+
* or `store.syncPairs()` (pre-warmed from `createGenericStore`) instead —
|
|
678
|
+
* they scale to ~50K–80K paths.
|
|
679
|
+
*
|
|
530
680
|
* @example
|
|
531
681
|
* const pair: SyncPair<State> = ['user.email', 'profile.email']
|
|
532
682
|
*
|
|
@@ -543,9 +693,12 @@ type SyncPair<DATA extends object, Depth extends number = DefaultDepth> = {
|
|
|
543
693
|
];
|
|
544
694
|
}[ResolvableDeepKey<DATA, Depth>];
|
|
545
695
|
/**
|
|
546
|
-
* A tuple of two paths for flip (alias for SyncPair)
|
|
696
|
+
* A tuple of two paths for flip (alias for SyncPair).
|
|
547
697
|
* Format: [path1, path2]
|
|
548
698
|
*
|
|
699
|
+
* **Scaling note**: O(N²) — hits TS2589 at ~1,500 paths. Use `flipPairs()` helper
|
|
700
|
+
* or `store.flipPairs()` for large state types.
|
|
701
|
+
*
|
|
549
702
|
* @example
|
|
550
703
|
* const pair: FlipPair<State> = ['isActive', 'isInactive']
|
|
551
704
|
*/
|
|
@@ -558,6 +711,9 @@ type FlipPair<DATA extends object, Depth extends number = DefaultDepth> = SyncPa
|
|
|
558
711
|
*
|
|
559
712
|
* Multiple pairs can point to same target for multi-source aggregation.
|
|
560
713
|
*
|
|
714
|
+
* **Scaling note**: O(N²) — hits TS2589 at ~1,500 paths. Use `aggregationPairs()` helper
|
|
715
|
+
* or `store.aggregationPairs()` for large state types.
|
|
716
|
+
*
|
|
561
717
|
* @example
|
|
562
718
|
* // target <- source (target is always first/left)
|
|
563
719
|
* const aggs: AggregationPair<State>[] = [
|
|
@@ -581,6 +737,9 @@ type ComputationOp = 'SUM' | 'AVG';
|
|
|
581
737
|
*
|
|
582
738
|
* Multiple pairs can point to same target for multi-source computation.
|
|
583
739
|
*
|
|
740
|
+
* **Scaling note**: O(N²) — hits TS2589 at ~1,500 paths. Use `computationPairs()` helper
|
|
741
|
+
* or `store.computationPairs()` for large state types.
|
|
742
|
+
*
|
|
584
743
|
* @example
|
|
585
744
|
* const comps: ComputationPair<State>[] = [
|
|
586
745
|
* ['SUM', 'total', 'price1'],
|
|
@@ -598,6 +757,70 @@ type ComputationPair<DATA extends object, Depth extends number = DefaultDepth> =
|
|
|
598
757
|
];
|
|
599
758
|
}[DeepKeyFiltered<DATA, number, Depth>];
|
|
600
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Lazy pair validators — O(N) base + O(K) per-pair DeepValue check
|
|
762
|
+
*
|
|
763
|
+
* These types power the helper functions (syncPairs, flipPairs, etc.)
|
|
764
|
+
* that scale to large state types without hitting TS2589.
|
|
765
|
+
*
|
|
766
|
+
* Unlike the direct pair types (SyncPair, FlipPair, etc.) which are O(N²),
|
|
767
|
+
* these validators only evaluate DeepValue for the K pairs you actually write.
|
|
768
|
+
*/
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Validates that two paths resolve to the same value type.
|
|
772
|
+
* Only evaluates DeepValue when P1, P2 are concrete literals — O(1) per pair.
|
|
773
|
+
* Wraps in [T] extends [U] to prevent distributive conditional.
|
|
774
|
+
*
|
|
775
|
+
* Returns [P1, P2] on match, [P1, never] on mismatch.
|
|
776
|
+
*/
|
|
777
|
+
type CheckPairValueMatch<DATA extends object, P1 extends string, P2 extends string, Depth extends number = DefaultDepth> = [P1] extends [ResolvableDeepKey<DATA, Depth>] ? [P2] extends [ResolvableDeepKey<DATA, Depth>] ? [NonNullable<DeepValue<DATA, P1>>] extends [
|
|
778
|
+
NonNullable<DeepValue<DATA, P2>>
|
|
779
|
+
] ? [NonNullable<DeepValue<DATA, P2>>] extends [
|
|
780
|
+
NonNullable<DeepValue<DATA, P1>>
|
|
781
|
+
] ? [P1, P2] : [P1, never] : [P1, never] : never : never;
|
|
782
|
+
/**
|
|
783
|
+
* Validates an array of [P1, P2] sync/flip pairs lazily — O(K) where K = pairs written.
|
|
784
|
+
*/
|
|
785
|
+
type CheckSyncPairs<DATA extends object, T extends readonly [string, string][], Depth extends number = DefaultDepth> = {
|
|
786
|
+
[I in keyof T]: T[I] extends [
|
|
787
|
+
infer P1 extends string,
|
|
788
|
+
infer P2 extends string
|
|
789
|
+
] ? CheckPairValueMatch<DATA, P1, P2, Depth> : T[I];
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* Validates that two paths both resolve to number.
|
|
793
|
+
* Returns [ComputationOp, P1, P2] or [ComputationOp, P1, P2, BoolLogic] on match.
|
|
794
|
+
*/
|
|
795
|
+
type CheckComputationPairValueMatch<DATA extends object, P1 extends string, P2 extends string, Depth extends number = DefaultDepth> = [P1] extends [DeepKeyFiltered<DATA, number, Depth>] ? [P2] extends [DeepKeyFiltered<DATA, number, Depth>] ? true : false : false;
|
|
796
|
+
/**
|
|
797
|
+
* Validates an array of aggregation pairs: [target, source] or [target, source, BoolLogic].
|
|
798
|
+
* Each pair checks that target and source resolve to the same value type.
|
|
799
|
+
*/
|
|
800
|
+
type CheckAggregationPairs<DATA extends object, T extends readonly (readonly [string, string] | readonly [string, string, BoolLogic<DATA, Depth>])[], Depth extends number = DefaultDepth> = {
|
|
801
|
+
[I in keyof T]: T[I] extends readonly [
|
|
802
|
+
infer P1 extends string,
|
|
803
|
+
infer P2 extends string,
|
|
804
|
+
infer BL
|
|
805
|
+
] ? CheckPairValueMatch<DATA, P1, P2, Depth> extends [P1, P2] ? [P1, P2, BL] : [P1, never, BL] : T[I] extends readonly [infer P1 extends string, infer P2 extends string] ? CheckPairValueMatch<DATA, P1, P2, Depth> : T[I];
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* Validates an array of computation pairs: [op, target, source] or [op, target, source, BoolLogic].
|
|
809
|
+
* Both target and source must be number paths.
|
|
810
|
+
*/
|
|
811
|
+
type CheckComputationPairs<DATA extends object, T extends readonly (readonly [ComputationOp, string, string] | readonly [ComputationOp, string, string, BoolLogic<DATA, Depth>])[], Depth extends number = DefaultDepth> = {
|
|
812
|
+
[I in keyof T]: T[I] extends readonly [
|
|
813
|
+
infer Op extends ComputationOp,
|
|
814
|
+
infer P1 extends string,
|
|
815
|
+
infer P2 extends string,
|
|
816
|
+
infer BL
|
|
817
|
+
] ? CheckComputationPairValueMatch<DATA, P1, P2, Depth> extends true ? [Op, P1, P2, BL] : [Op, P1, never, BL] : T[I] extends readonly [
|
|
818
|
+
infer Op extends ComputationOp,
|
|
819
|
+
infer P1 extends string,
|
|
820
|
+
infer P2 extends string
|
|
821
|
+
] ? CheckComputationPairValueMatch<DATA, P1, P2, Depth> extends true ? [Op, P1, P2] : [Op, P1, never] : T[I];
|
|
822
|
+
};
|
|
823
|
+
|
|
601
824
|
/** Recursively makes all properties required, stripping undefined */
|
|
602
825
|
type DeepRequired<T> = {
|
|
603
826
|
[K in keyof T]-?: NonNullable<T[K]> extends object ? DeepRequired<NonNullable<T[K]>> : NonNullable<T[K]>;
|
|
@@ -613,6 +836,56 @@ type DeepPartial<T> = T extends (infer U)[] ? DeepPartial<U>[] : T extends reado
|
|
|
613
836
|
[K in keyof T]?: DeepPartial<T[K]>;
|
|
614
837
|
} : T;
|
|
615
838
|
|
|
839
|
+
/**
|
|
840
|
+
* Validated pair branded types
|
|
841
|
+
*
|
|
842
|
+
* Phantom types for pre-validated pair arrays returned by helper functions
|
|
843
|
+
* (syncPairs, flipPairs, aggregationPairs, computationPairs).
|
|
844
|
+
*
|
|
845
|
+
* Brands serve two purposes:
|
|
846
|
+
* 1. SideEffects accepts them without re-resolving the O(N²) direct pair types
|
|
847
|
+
* 2. DATA generic prevents cross-store use (pairs from StoreA can't go to StoreB)
|
|
848
|
+
*
|
|
849
|
+
* Zero runtime cost — brands are erased at compile time.
|
|
850
|
+
*/
|
|
851
|
+
|
|
852
|
+
declare const VALIDATED: unique symbol;
|
|
853
|
+
declare const STORE_DATA: unique symbol;
|
|
854
|
+
/** Pre-validated sync pair array. Returned by `syncPairs()`. Branded with DATA to prevent cross-store use. */
|
|
855
|
+
type ValidatedSyncPairs<DATA extends object = object> = [
|
|
856
|
+
string,
|
|
857
|
+
string
|
|
858
|
+
][] & {
|
|
859
|
+
[VALIDATED]: 'sync';
|
|
860
|
+
[STORE_DATA]: DATA;
|
|
861
|
+
};
|
|
862
|
+
/** Pre-validated flip pair array. Returned by `flipPairs()`. Branded with DATA to prevent cross-store use. */
|
|
863
|
+
type ValidatedFlipPairs<DATA extends object = object> = [
|
|
864
|
+
string,
|
|
865
|
+
string
|
|
866
|
+
][] & {
|
|
867
|
+
[VALIDATED]: 'flip';
|
|
868
|
+
[STORE_DATA]: DATA;
|
|
869
|
+
};
|
|
870
|
+
/** Pre-validated aggregation pair array. Returned by `aggregationPairs()`. Branded with DATA to prevent cross-store use. */
|
|
871
|
+
type ValidatedAggregationPairs<DATA extends object = object> = ([string, string] | [string, string, unknown])[] & {
|
|
872
|
+
[VALIDATED]: 'aggregation';
|
|
873
|
+
[STORE_DATA]: DATA;
|
|
874
|
+
};
|
|
875
|
+
/** Pre-validated computation pair array. Returned by `computationPairs()`. Branded with DATA to prevent cross-store use. */
|
|
876
|
+
type ValidatedComputationPairs<DATA extends object = object> = ([ComputationOp, string, string] | [ComputationOp, string, string, unknown])[] & {
|
|
877
|
+
[VALIDATED]: 'computation';
|
|
878
|
+
[STORE_DATA]: DATA;
|
|
879
|
+
};
|
|
880
|
+
/** Pre-validated listener array. Returned by `listeners()`. Branded with DATA to prevent cross-store use.
|
|
881
|
+
* Uses intersection with ListenerRegistration[] so it's assignable to the listeners field
|
|
882
|
+
* without needing a union — preserving contextual typing for inline fn parameters.
|
|
883
|
+
*/
|
|
884
|
+
type ValidatedListeners<DATA extends object = object, META extends GenericMeta = GenericMeta> = ListenerRegistration<DATA, META>[] & {
|
|
885
|
+
[VALIDATED]: 'listeners';
|
|
886
|
+
[STORE_DATA]: DATA;
|
|
887
|
+
};
|
|
888
|
+
|
|
616
889
|
interface BaseConcernProps<STATE, PATH extends string> {
|
|
617
890
|
state: STATE;
|
|
618
891
|
path: PATH;
|
|
@@ -934,8 +1207,14 @@ interface Aggregation {
|
|
|
934
1207
|
targetPath: string;
|
|
935
1208
|
sourcePaths: string[];
|
|
936
1209
|
}
|
|
937
|
-
/** Reacts to scoped changes - receives relative paths and scoped state. Only fires for NESTED paths, not the path itself.
|
|
938
|
-
|
|
1210
|
+
/** Reacts to scoped changes - receives relative paths and scoped state. Only fires for NESTED paths, not the path itself.
|
|
1211
|
+
* Both input changes and return changes use paths relative to scope (or full paths when scope is null).
|
|
1212
|
+
*
|
|
1213
|
+
* When SUB_STATE is `any` (as in ListenerRegistration's default), the return type falls back to
|
|
1214
|
+
* ArrayOfChanges<DATA> to avoid DeepKey<any> resolving to never.
|
|
1215
|
+
*/
|
|
1216
|
+
type IsAnyState<T> = 0 extends 1 & T ? true : false;
|
|
1217
|
+
type OnStateListener<DATA extends object = object, SUB_STATE = DATA, META extends GenericMeta = GenericMeta> = (changes: ArrayOfChanges<SUB_STATE, META>, state: SUB_STATE) => IsAnyState<SUB_STATE> extends true ? ArrayOfChanges<DATA, META> | undefined : ArrayOfChanges<SUB_STATE, META> | undefined;
|
|
939
1218
|
/**
|
|
940
1219
|
* Listener registration with path (what to watch) and scope (how to present data)
|
|
941
1220
|
*
|
|
@@ -980,12 +1259,13 @@ interface ListenerRegistration<DATA extends object = object, META extends Generi
|
|
|
980
1259
|
path: DeepKey<DATA, Depth> | null;
|
|
981
1260
|
/**
|
|
982
1261
|
* Scope for state and changes presentation
|
|
1262
|
+
* - If omitted/undefined: defaults to `path` (scoped state matching the watched path)
|
|
983
1263
|
* - If null: state is full DATA, changes use FULL paths
|
|
984
1264
|
* - If set: state is value at scope, changes use paths RELATIVE to scope
|
|
985
1265
|
*
|
|
986
1266
|
* Note: Changes are filtered based on `path`, even when scope is null
|
|
987
1267
|
*/
|
|
988
|
-
scope
|
|
1268
|
+
scope?: DeepKey<DATA, Depth> | null;
|
|
989
1269
|
fn: OnStateListener<DATA, any, META>;
|
|
990
1270
|
}
|
|
991
1271
|
interface ListenerHandlerRef {
|
|
@@ -1289,7 +1569,7 @@ type ClearPathRule<DATA extends object, Depth extends number = DefaultDepth> = [
|
|
|
1289
1569
|
* fn: (changes, state) => {
|
|
1290
1570
|
* // changes: [['name', 'Alice', {}]] // RELATIVE to scope
|
|
1291
1571
|
* // state: user.profile sub-object
|
|
1292
|
-
* return [['status', 'updated', {}]] // Return
|
|
1572
|
+
* return [['status', 'updated', {}]] // Return SCOPED paths (relative to scope)
|
|
1293
1573
|
* }
|
|
1294
1574
|
* },
|
|
1295
1575
|
* {
|
|
@@ -1307,7 +1587,7 @@ type ClearPathRule<DATA extends object, Depth extends number = DefaultDepth> = [
|
|
|
1307
1587
|
* fn: (changes, state) => {
|
|
1308
1588
|
* // changes: [['data.strike', value, {}]] // RELATIVE to scope
|
|
1309
1589
|
* // state: p.123.g.abc object
|
|
1310
|
-
* return [['
|
|
1590
|
+
* return [['data.computed', computed, {}]] // SCOPED path (relative to scope)
|
|
1311
1591
|
* }
|
|
1312
1592
|
* }
|
|
1313
1593
|
* ]
|
|
@@ -1318,19 +1598,22 @@ interface SideEffects<DATA extends object, META extends GenericMeta = GenericMet
|
|
|
1318
1598
|
/**
|
|
1319
1599
|
* Sync paths - keeps specified paths synchronized
|
|
1320
1600
|
* Format: [path1, path2] - both paths stay in sync
|
|
1601
|
+
* Accepts direct `SyncPair<T>[]` or pre-validated result from `syncPairs()`.
|
|
1321
1602
|
*/
|
|
1322
|
-
syncPaths?: SyncPair<DATA, Depth>[]
|
|
1603
|
+
syncPaths?: SyncPair<DATA, Depth>[] | ValidatedSyncPairs<DATA>;
|
|
1323
1604
|
/**
|
|
1324
1605
|
* Flip paths - keeps specified paths with opposite values
|
|
1325
1606
|
* Format: [path1, path2] - paths have inverse boolean values
|
|
1607
|
+
* Accepts direct `FlipPair<T>[]` or pre-validated result from `flipPairs()`.
|
|
1326
1608
|
*/
|
|
1327
|
-
flipPaths?: FlipPair<DATA, Depth>[]
|
|
1609
|
+
flipPaths?: FlipPair<DATA, Depth>[] | ValidatedFlipPairs<DATA>;
|
|
1328
1610
|
/**
|
|
1329
1611
|
* Aggregations - aggregates sources into target
|
|
1330
1612
|
* Format: [target, source] - target is ALWAYS first (left)
|
|
1331
1613
|
* Multiple pairs can point to same target for multi-source aggregation
|
|
1614
|
+
* Accepts direct `AggregationPair<T>[]` or pre-validated result from `aggregationPairs()`.
|
|
1332
1615
|
*/
|
|
1333
|
-
aggregations?: AggregationPair<DATA, Depth>[]
|
|
1616
|
+
aggregations?: AggregationPair<DATA, Depth>[] | ValidatedAggregationPairs<DATA>;
|
|
1334
1617
|
/**
|
|
1335
1618
|
* Clear paths - "when X changes, set Y to null"
|
|
1336
1619
|
* Format: [triggers[], targets[], { expandMatch?: boolean }?]
|
|
@@ -1343,15 +1626,26 @@ interface SideEffects<DATA extends object, META extends GenericMeta = GenericMet
|
|
|
1343
1626
|
* Format: [operation, target, source] - target is computed from sources
|
|
1344
1627
|
* Multiple pairs can point to same target for multi-source computation
|
|
1345
1628
|
* Unidirectional: source → target only (writes to target are no-op)
|
|
1629
|
+
* Accepts direct `ComputationPair<T>[]` or pre-validated result from `computationPairs()`.
|
|
1346
1630
|
*/
|
|
1347
|
-
computations?: ComputationPair<DATA, Depth>[]
|
|
1631
|
+
computations?: ComputationPair<DATA, Depth>[] | ValidatedComputationPairs<DATA>;
|
|
1348
1632
|
/**
|
|
1349
1633
|
* Listeners - react to state changes with scoped state
|
|
1634
|
+
* Accepts direct `ListenerRegistration<T>[]` or pre-validated result from `listeners()`.
|
|
1350
1635
|
*/
|
|
1351
1636
|
listeners?: ListenerRegistration<DATA, META>[];
|
|
1352
1637
|
}
|
|
1353
1638
|
|
|
1354
1639
|
declare const createGenericStore: <DATA extends object, META extends GenericMeta = GenericMeta, CONCERNS extends readonly ConcernType<string, any, any>[] = typeof defaultConcerns>(config?: StoreConfig) => {
|
|
1640
|
+
syncPairs: <T extends [Exclude<DeepKey<DATA, 20>, `${string}??`>, Exclude<DeepKey<DATA, 20>, `${string}??`>][]>(pairs: CheckSyncPairs<DATA, T, 20>) => ValidatedSyncPairs<DATA>;
|
|
1641
|
+
flipPairs: <T extends [Exclude<DeepKey<DATA, 20>, `${string}??`>, Exclude<DeepKey<DATA, 20>, `${string}??`>][]>(pairs: CheckSyncPairs<DATA, T, 20>) => ValidatedFlipPairs<DATA>;
|
|
1642
|
+
aggregationPairs: <T extends ([Exclude<DeepKey<DATA, 20>, `${string}??`>, Exclude<DeepKey<DATA, 20>, `${string}??`>] | [Exclude<DeepKey<DATA, 20>, `${string}??`>, Exclude<DeepKey<DATA, 20>, `${string}??`>, BoolLogic<DATA, 20>])[]>(pairs: CheckAggregationPairs<DATA, T, 20>) => ValidatedAggregationPairs<DATA>;
|
|
1643
|
+
computationPairs: <T extends ([ComputationOp, DeepKeyFiltered<DATA, number, 20>, DeepKeyFiltered<DATA, number, 20>] | [ComputationOp, DeepKeyFiltered<DATA, number, 20>, DeepKeyFiltered<DATA, number, 20>, BoolLogic<DATA, 20>])[]>(pairs: CheckComputationPairs<DATA, T, 20>) => ValidatedComputationPairs<DATA>;
|
|
1644
|
+
listeners: <T extends readonly {
|
|
1645
|
+
path: Exclude<DeepKey<DATA, 20>, `${string}??`> | null;
|
|
1646
|
+
scope?: Exclude<DeepKey<DATA, 20>, `${string}??`> | null | undefined;
|
|
1647
|
+
fn: (...args: any[]) => any;
|
|
1648
|
+
}[]>(items: CheckListeners<DATA, META, T, 20>) => ValidatedListeners<DATA, META>;
|
|
1355
1649
|
Provider: {
|
|
1356
1650
|
(props: ProviderProps<DATA>): react_jsx_runtime.JSX.Element;
|
|
1357
1651
|
displayName: string;
|
|
@@ -1609,6 +1903,145 @@ declare const registerSyncPairsBatch: <DATA extends object, META extends Generic
|
|
|
1609
1903
|
|
|
1610
1904
|
declare const registerSideEffects: <DATA extends object, META extends GenericMeta = GenericMeta>(store: StoreInstance<DATA, META>, id: string, effects: SideEffects<DATA, META>) => (() => void);
|
|
1611
1905
|
|
|
1906
|
+
/**
|
|
1907
|
+
* Lazy-validated pair helper functions
|
|
1908
|
+
*
|
|
1909
|
+
* These curried helpers provide O(N) base type + O(K) per-pair validation,
|
|
1910
|
+
* avoiding the O(N²) explosion of SyncPair/FlipPair/AggregationPair/ComputationPair
|
|
1911
|
+
* on large state types (1500+ paths).
|
|
1912
|
+
*
|
|
1913
|
+
* Runtime behavior: identity function (returns input as-is).
|
|
1914
|
+
* Type behavior: validates each pair via CheckSyncPairs / CheckAggregationPairs / CheckComputationPairs.
|
|
1915
|
+
*
|
|
1916
|
+
* **Why these exist**: The direct pair types (`SyncPair<T>`, `FlipPair<T>`, etc.) distribute
|
|
1917
|
+
* over all N paths to build an O(N²) union of valid pairs. This hits TS2589 at ~1,500 paths.
|
|
1918
|
+
* These helpers defer validation: the function constraint uses `ResolvableDeepKey<T>` (O(N))
|
|
1919
|
+
* for autocomplete, then `CheckPairValueMatch` validates only the K pairs you actually write
|
|
1920
|
+
* via `DeepValue` comparison (O(1) per pair).
|
|
1921
|
+
*
|
|
1922
|
+
* **Inference safety**: Parameter types use mapped types only (`{ [I in keyof T]: Check<T[I]> }`)
|
|
1923
|
+
* — never `[...T] & MappedType<T>`. This avoids the TS inference competition where `[...T]`
|
|
1924
|
+
* and a mapped type both try to infer T, causing tuple widening on small state types.
|
|
1925
|
+
*
|
|
1926
|
+
* **Branded returns**: Each helper returns a `Validated*` branded type so that
|
|
1927
|
+
* `useSideEffects({ syncPaths: result })` skips the O(N²) re-validation.
|
|
1928
|
+
*
|
|
1929
|
+
* **Scaling limits** (measured on Apple M4 Pro):
|
|
1930
|
+
* - ~82,500 paths (500 flat sectors, depth 4): PASS in 7.1s / 617 MB
|
|
1931
|
+
* - ~52,800 paths (binary 6 levels, depth 9): PASS in 4.7s / 574 MB
|
|
1932
|
+
* - ~105,600 paths (binary 7 levels, depth 10): TS2589
|
|
1933
|
+
* - Practical limit: ~50K–80K paths, bounded by `ResolvableDeepKey` union resolution
|
|
1934
|
+
* at the function constraint, not by the validation logic.
|
|
1935
|
+
* - Old `SyncPair<T>` hit TS2589 at ~1,500 paths — a ~30–50x improvement.
|
|
1936
|
+
*
|
|
1937
|
+
* @example
|
|
1938
|
+
* ```typescript
|
|
1939
|
+
* const syncs = syncPairs<State>()([
|
|
1940
|
+
* ['user.email', 'profile.email'], // ✓ both string
|
|
1941
|
+
* ['user.age', 'profile.name'], // ✗ number vs string → type error
|
|
1942
|
+
* ])
|
|
1943
|
+
* ```
|
|
1944
|
+
*/
|
|
1945
|
+
|
|
1946
|
+
/**
|
|
1947
|
+
* Lazy-validated sync pairs. Curried: `syncPairs<State>()(pairs)`.
|
|
1948
|
+
*
|
|
1949
|
+
* Provides autocomplete for valid paths (O(N)) and validates that both paths
|
|
1950
|
+
* in each pair resolve to the same value type (O(K) per pair).
|
|
1951
|
+
* Returns branded `ValidatedSyncPairs` — accepted by `useSideEffects` without re-validation.
|
|
1952
|
+
*
|
|
1953
|
+
* @example
|
|
1954
|
+
* ```typescript
|
|
1955
|
+
* const syncs = syncPairs<MyState>()([
|
|
1956
|
+
* ['user.email', 'profile.email'],
|
|
1957
|
+
* ['user.name', 'profile.name'],
|
|
1958
|
+
* ])
|
|
1959
|
+
* useSideEffects('my-syncs', { syncPaths: syncs }) // no O(N²) re-check
|
|
1960
|
+
* ```
|
|
1961
|
+
*/
|
|
1962
|
+
declare const syncPairs: <DATA extends object, Depth extends number = DefaultDepth>() => <T extends [ResolvableDeepKey<DATA, Depth>, ResolvableDeepKey<DATA, Depth>][]>(pairs: CheckSyncPairs<DATA, T, Depth>) => ValidatedSyncPairs<DATA>;
|
|
1963
|
+
/**
|
|
1964
|
+
* Lazy-validated flip pairs. Curried: `flipPairs<State>()(pairs)`.
|
|
1965
|
+
*
|
|
1966
|
+
* Identical to syncPairs — validates both paths resolve to the same value type.
|
|
1967
|
+
* Returns branded `ValidatedFlipPairs` — accepted by `useSideEffects` without re-validation.
|
|
1968
|
+
*
|
|
1969
|
+
* @example
|
|
1970
|
+
* ```typescript
|
|
1971
|
+
* const flips = flipPairs<MyState>()([
|
|
1972
|
+
* ['isActive', 'isInactive'],
|
|
1973
|
+
* ])
|
|
1974
|
+
* useSideEffects('my-flips', { flipPaths: flips }) // no O(N²) re-check
|
|
1975
|
+
* ```
|
|
1976
|
+
*/
|
|
1977
|
+
declare const flipPairs: <DATA extends object, Depth extends number = DefaultDepth>() => <T extends [ResolvableDeepKey<DATA, Depth>, ResolvableDeepKey<DATA, Depth>][]>(pairs: CheckSyncPairs<DATA, T, Depth>) => ValidatedFlipPairs<DATA>;
|
|
1978
|
+
/**
|
|
1979
|
+
* Lazy-validated aggregation pairs. Curried: `aggregationPairs<State>()(pairs)`.
|
|
1980
|
+
*
|
|
1981
|
+
* Each pair is [target, source] or [target, source, BoolLogic].
|
|
1982
|
+
* Validates that target and source resolve to the same value type.
|
|
1983
|
+
* Returns branded `ValidatedAggregationPairs` — accepted by `useSideEffects` without re-validation.
|
|
1984
|
+
*
|
|
1985
|
+
* @example
|
|
1986
|
+
* ```typescript
|
|
1987
|
+
* const aggs = aggregationPairs<MyState>()([
|
|
1988
|
+
* ['total', 'price1'],
|
|
1989
|
+
* ['total', 'price2', { IS_EQUAL: ['price2.disabled', true] }],
|
|
1990
|
+
* ])
|
|
1991
|
+
* useSideEffects('my-aggs', { aggregations: aggs }) // no O(N²) re-check
|
|
1992
|
+
* ```
|
|
1993
|
+
*/
|
|
1994
|
+
declare const aggregationPairs: <DATA extends object, Depth extends number = DefaultDepth>() => <T extends ([ResolvableDeepKey<DATA, Depth>, ResolvableDeepKey<DATA, Depth>] | [ResolvableDeepKey<DATA, Depth>, ResolvableDeepKey<DATA, Depth>, BoolLogic<DATA, Depth>])[]>(pairs: CheckAggregationPairs<DATA, T, Depth>) => ValidatedAggregationPairs<DATA>;
|
|
1995
|
+
/**
|
|
1996
|
+
* Lazy-validated computation pairs. Curried: `computationPairs<State>()(pairs)`.
|
|
1997
|
+
*
|
|
1998
|
+
* Each pair is [op, target, source] or [op, target, source, BoolLogic].
|
|
1999
|
+
* Validates that both target and source are number paths.
|
|
2000
|
+
* Returns branded `ValidatedComputationPairs` — accepted by `useSideEffects` without re-validation.
|
|
2001
|
+
*
|
|
2002
|
+
* @example
|
|
2003
|
+
* ```typescript
|
|
2004
|
+
* const comps = computationPairs<MyState>()([
|
|
2005
|
+
* ['SUM', 'total', 'price1'],
|
|
2006
|
+
* ['AVG', 'average', 'score1'],
|
|
2007
|
+
* ])
|
|
2008
|
+
* useSideEffects('my-comps', { computations: comps }) // no O(N²) re-check
|
|
2009
|
+
* ```
|
|
2010
|
+
*/
|
|
2011
|
+
declare const computationPairs: <DATA extends object, Depth extends number = DefaultDepth>() => <T extends ([ComputationOp, DeepKeyFiltered<DATA, number, Depth>, DeepKeyFiltered<DATA, number, Depth>] | [ComputationOp, DeepKeyFiltered<DATA, number, Depth>, DeepKeyFiltered<DATA, number, Depth>, BoolLogic<DATA, Depth>])[]>(pairs: CheckComputationPairs<DATA, T, Depth>) => ValidatedComputationPairs<DATA>;
|
|
2012
|
+
/**
|
|
2013
|
+
* Lazy-validated listeners. Curried: `listeners<State>()(items)`.
|
|
2014
|
+
*
|
|
2015
|
+
* Provides autocomplete for valid paths (O(N)) and validates each listener:
|
|
2016
|
+
* - path and scope are valid `ResolvableDeepKey<DATA>` or null
|
|
2017
|
+
* - When both non-null: scope must be a dot-separated prefix of path
|
|
2018
|
+
* - fn receives correctly-typed scoped state based on scope
|
|
2019
|
+
*
|
|
2020
|
+
* Returns branded `ValidatedListeners` — accepted by `useSideEffects` without re-validation.
|
|
2021
|
+
*
|
|
2022
|
+
* @example
|
|
2023
|
+
* ```typescript
|
|
2024
|
+
* const myListeners = listeners<MyState>()([
|
|
2025
|
+
* {
|
|
2026
|
+
* path: 'user.profile.name',
|
|
2027
|
+
* scope: 'user.profile',
|
|
2028
|
+
* fn: (changes, state) => {
|
|
2029
|
+
* // changes: ArrayOfChanges relative to user.profile scope
|
|
2030
|
+
* // state: typed as MyState['user']['profile']
|
|
2031
|
+
* // return: ArrayOfChanges relative to scope, or undefined
|
|
2032
|
+
* return [['name', `updated-${state.name}`, {}]]
|
|
2033
|
+
* }
|
|
2034
|
+
* },
|
|
2035
|
+
* ])
|
|
2036
|
+
* useSideEffects('my-listeners', { listeners: myListeners })
|
|
2037
|
+
* ```
|
|
2038
|
+
*/
|
|
2039
|
+
declare const listeners: <DATA extends object, META extends GenericMeta = GenericMeta, Depth extends number = DefaultDepth>() => <T extends readonly {
|
|
2040
|
+
path: ResolvableDeepKey<DATA, Depth> | null;
|
|
2041
|
+
scope?: ResolvableDeepKey<DATA, Depth> | null | undefined;
|
|
2042
|
+
fn: (...args: any[]) => any;
|
|
2043
|
+
}[]>(items: CheckListeners<DATA, META, T, Depth>) => ValidatedListeners<DATA, META>;
|
|
2044
|
+
|
|
1612
2045
|
declare const evaluateBoolLogic: <STATE extends object>(logic: BoolLogic<STATE>, state: STATE) => boolean;
|
|
1613
2046
|
|
|
1614
2047
|
/**
|
|
@@ -1822,4 +2255,4 @@ declare const applyChangesToObject: <T extends object>(obj: T, changes: ArrayOfC
|
|
|
1822
2255
|
*/
|
|
1823
2256
|
declare const deepClone: <T>(value: T) => T;
|
|
1824
2257
|
|
|
1825
|
-
export { type Aggregation, type AggregationPair, type ArrayOfChanges, type BaseConcernProps, type BoolLogic, type BufferedField, type ClearPathRule, type ComputationOp, type ComputationPair, type ConcernRegistration, type ConcernRegistrationMap, type ConcernType, type DebugConfig, type DebugTrack, type DebugTrackEntry, type DeepKey, type DeepKeyFiltered, type DeepPartial, type DeepRequired, type DeepValue, type EvaluatedConcerns, type ExtractEvaluateReturn, type FieldInput$2 as FieldInput, type FlipPair, type GenericMeta, type GenericStoreApi, type HASH_KEY, type KeyboardSelectConfig, type ListenerRegistration, type OnStateListener, type PathsWithSameValueAs, type ProviderProps, type SelectOption, type SideEffects, type StoreConfig, type StoreInstance, type SyncPair, type ThrottleConfig, type ThrottleFieldInput, type TransformConfig, type ValidationError, type ValidationSchema, type ValidationStateConcern, type ValidationStateInput, type ValidationStateResult, _, applyChangesToObject, createGenericStore, deepClone, defaultConcerns, dot, evaluateBoolLogic, extractPlaceholders, findConcern, hashKey, interpolateTemplate, is, index as prebuilts, registerFlipPair, registerListenerLegacy, registerSideEffects, registerSyncPairsBatch, useBufferedField, useKeyboardSelect, useThrottledField, useTransformedField };
|
|
2258
|
+
export { type Aggregation, type AggregationPair, type ArrayOfChanges, type BaseConcernProps, type BoolLogic, type BufferedField, type CheckAggregationPairs, type CheckComputationPairs, type CheckPairValueMatch, type CheckSyncPairs, type ClearPathRule, type ComputationOp, type ComputationPair, type ConcernRegistration, type ConcernRegistrationMap, type ConcernType, type DebugConfig, type DebugTrack, type DebugTrackEntry, type DeepKey, type DeepKeyFiltered, type DeepPartial, type DeepRequired, type DeepValue, type EvaluatedConcerns, type ExtractEvaluateReturn, type FieldInput$2 as FieldInput, type FlipPair, type GenericMeta, type GenericStoreApi, type HASH_KEY, type KeyboardSelectConfig, type ListenerRegistration, type OnStateListener, type PathsWithSameValueAs, type ProviderProps, type SelectOption, type SideEffects, type StoreConfig, type StoreInstance, type SyncPair, type ThrottleConfig, type ThrottleFieldInput, type TransformConfig, type ValidationError, type ValidationSchema, type ValidationStateConcern, type ValidationStateInput, type ValidationStateResult, _, aggregationPairs, applyChangesToObject, computationPairs, createGenericStore, deepClone, defaultConcerns, dot, evaluateBoolLogic, extractPlaceholders, findConcern, flipPairs, hashKey, interpolateTemplate, is, listeners, index as prebuilts, registerFlipPair, registerListenerLegacy, registerSideEffects, registerSyncPairsBatch, syncPairs, useBufferedField, useKeyboardSelect, useThrottledField, useTransformedField };
|