flurryx 0.7.1 → 0.7.4
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 +219 -31
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
|
|
|
66
66
|
- [Constants](#constants)
|
|
67
67
|
- [Keyed Resources](#keyed-resources)
|
|
68
68
|
- [Store Mirroring](#store-mirroring)
|
|
69
|
+
- [Builder .mirror()](#builder-mirror)
|
|
70
|
+
- [Builder .mirrorKeyed()](#builder-mirrorkeyed)
|
|
69
71
|
- [mirrorKey](#mirrorkey)
|
|
70
72
|
- [collectKeyed](#collectkeyed)
|
|
71
73
|
- [Design Decisions](#design-decisions)
|
|
@@ -599,14 +601,190 @@ import {
|
|
|
599
601
|
|
|
600
602
|
When building session or aggregation stores that combine state from multiple feature stores, you typically need `onUpdate` listeners, cleanup arrays, and `DestroyRef` wiring. The `mirrorKey` and `collectKeyed` utilities reduce that to a single call.
|
|
601
603
|
|
|
604
|
+
```
|
|
605
|
+
+--------------------+ +--------------------+
|
|
606
|
+
| Feature Store A | | |
|
|
607
|
+
| (CUSTOMERS) |-- mirrorKey ------>| |
|
|
608
|
+
+--------------------+ | |
|
|
609
|
+
| Session Store |
|
|
610
|
+
+--------------------+ | (aggregated) |
|
|
611
|
+
| Feature Store B | | |
|
|
612
|
+
| (ORDERS) |-- mirrorKey ------>| CUSTOMERS + |
|
|
613
|
+
+--------------------+ | ORDERS + |
|
|
614
|
+
| CUSTOMER_CACHE + |
|
|
615
|
+
+--------------------+ | ORDER_CACHE + |
|
|
616
|
+
| Feature Store C | | |
|
|
617
|
+
| (CUSTOMER_DETAIL) |-- collectKeyed --->| |
|
|
618
|
+
+--------------------+ | |
|
|
619
|
+
| |
|
|
620
|
+
+--------------------+ | |
|
|
621
|
+
| Feature Store D | | |
|
|
622
|
+
| (ORDER_DETAIL) |-- mirrorKeyed --->| |
|
|
623
|
+
+--------------------+ +--------------------+
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
import { Store, mirrorKey, collectKeyed } from "flurryx";
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Builder .mirror()
|
|
631
|
+
|
|
632
|
+
The simplest way to set up mirroring is directly in the store builder. Chain `.mirror()` to declare which source stores to mirror from — the wiring happens automatically when Angular creates the store.
|
|
633
|
+
|
|
634
|
+
```typescript
|
|
635
|
+
// Feature stores
|
|
636
|
+
interface CustomerStoreConfig {
|
|
637
|
+
CUSTOMERS: Customer[];
|
|
638
|
+
}
|
|
639
|
+
export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
640
|
+
|
|
641
|
+
interface OrderStoreConfig {
|
|
642
|
+
ORDERS: Order[];
|
|
643
|
+
}
|
|
644
|
+
export const OrderStore = Store.for<OrderStoreConfig>().build();
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Interface-based builder** (recommended):
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
interface SessionStoreConfig {
|
|
651
|
+
CUSTOMERS: Customer[];
|
|
652
|
+
ORDERS: Order[];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
656
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
657
|
+
.mirror(OrderStore, 'ORDERS')
|
|
658
|
+
.build();
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Fluent chaining:**
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
export const SessionStore = Store
|
|
665
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
666
|
+
.resource('ORDERS').as<Order[]>()
|
|
667
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
668
|
+
.mirror(OrderStore, 'ORDERS')
|
|
669
|
+
.build();
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Enum-constrained:**
|
|
673
|
+
|
|
602
674
|
```typescript
|
|
603
|
-
|
|
675
|
+
const SessionEnum = { CUSTOMERS: 'CUSTOMERS', ORDERS: 'ORDERS' } as const;
|
|
676
|
+
|
|
677
|
+
export const SessionStore = Store.for(SessionEnum)
|
|
678
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
679
|
+
.resource('ORDERS').as<Order[]>()
|
|
680
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
681
|
+
.mirror(OrderStore, 'ORDERS')
|
|
682
|
+
.build();
|
|
604
683
|
```
|
|
605
684
|
|
|
685
|
+
**Different source and target keys:**
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
export const SessionStore = Store.for<{ ARTICLES: Item[] }>()
|
|
689
|
+
.mirror(ItemStore, 'ITEMS', 'ARTICLES')
|
|
690
|
+
.build();
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
The builder calls `inject()` under the hood, so source stores are resolved through Angular's DI. Everything — data, loading, status, errors — is mirrored automatically. No manual cleanup needed; the mirrors live as long as the store.
|
|
694
|
+
|
|
695
|
+
### Builder .mirrorKeyed()
|
|
696
|
+
|
|
697
|
+
When the source store holds a single-entity slot (e.g. `CUSTOMER_DETAILS: Customer`) and you want to accumulate those fetches into a `KeyedResourceData` cache on the target, use `.mirrorKeyed()`. It is the builder equivalent of [`collectKeyed`](#collectkeyed).
|
|
698
|
+
|
|
699
|
+
```typescript
|
|
700
|
+
// Feature store — fetches one customer at a time
|
|
701
|
+
interface CustomerStoreConfig {
|
|
702
|
+
CUSTOMERS: Customer[];
|
|
703
|
+
CUSTOMER_DETAILS: Customer;
|
|
704
|
+
}
|
|
705
|
+
export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
**Interface-based builder** (recommended):
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
interface SessionStoreConfig {
|
|
712
|
+
CUSTOMERS: Customer[];
|
|
713
|
+
CUSTOMER_CACHE: KeyedResourceData<string, Customer>;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
717
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
718
|
+
.mirrorKeyed(CustomerStore, 'CUSTOMER_DETAILS', {
|
|
719
|
+
extractId: (data) => data?.id,
|
|
720
|
+
}, 'CUSTOMER_CACHE')
|
|
721
|
+
.build();
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
**Fluent chaining:**
|
|
725
|
+
|
|
726
|
+
```typescript
|
|
727
|
+
export const SessionStore = Store
|
|
728
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
729
|
+
.resource('CUSTOMER_CACHE').as<KeyedResourceData<string, Customer>>()
|
|
730
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
731
|
+
.mirrorKeyed(CustomerStore, 'CUSTOMER_DETAILS', {
|
|
732
|
+
extractId: (data) => data?.id,
|
|
733
|
+
}, 'CUSTOMER_CACHE')
|
|
734
|
+
.build();
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
**Enum-constrained:**
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
const SessionEnum = { CUSTOMERS: 'CUSTOMERS', CUSTOMER_CACHE: 'CUSTOMER_CACHE' } as const;
|
|
741
|
+
|
|
742
|
+
export const SessionStore = Store.for(SessionEnum)
|
|
743
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
744
|
+
.resource('CUSTOMER_CACHE').as<KeyedResourceData<string, Customer>>()
|
|
745
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
746
|
+
.mirrorKeyed(CustomerStore, 'CUSTOMER_DETAILS', {
|
|
747
|
+
extractId: (data) => data?.id,
|
|
748
|
+
}, 'CUSTOMER_CACHE')
|
|
749
|
+
.build();
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
**Same source and target key** — when the key names match, the last argument can be omitted:
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
export const SessionStore = Store.for<{
|
|
756
|
+
CUSTOMER_DETAILS: KeyedResourceData<string, Customer>;
|
|
757
|
+
}>()
|
|
758
|
+
.mirrorKeyed(CustomerStore, 'CUSTOMER_DETAILS', {
|
|
759
|
+
extractId: (data) => data?.id,
|
|
760
|
+
})
|
|
761
|
+
.build();
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
Each entity fetched through the source slot is accumulated by ID into the target's `KeyedResourceData`. Loading, status, and errors are tracked per entity. When the source is cleared, the corresponding entity is removed from the cache.
|
|
765
|
+
|
|
606
766
|
### mirrorKey
|
|
607
767
|
|
|
608
768
|
Mirrors a resource key from one store to another. When the source updates, the target is updated with the same state.
|
|
609
769
|
|
|
770
|
+
```
|
|
771
|
+
+------------------+--------------------------------+------------------+
|
|
772
|
+
| CustomerStore | mirrorKey | SessionStore |
|
|
773
|
+
| | | |
|
|
774
|
+
| CUSTOMERS -------|--- onUpdate --> update ------->| CUSTOMERS |
|
|
775
|
+
| | (same key or different) | |
|
|
776
|
+
| { data, | | { data, |
|
|
777
|
+
| status, | | status, |
|
|
778
|
+
| isLoading } | | isLoading } |
|
|
779
|
+
+------------------+--------------------------------+------------------+
|
|
780
|
+
|
|
781
|
+
source.update('CUSTOMERS', { data: [...], status: 'Success' })
|
|
782
|
+
|
|
|
783
|
+
'--> target is automatically updated with the same state
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
You wire it once. Every future update — data, loading, errors — flows automatically. Call the cleanup function or use `destroyRef` to stop.
|
|
787
|
+
|
|
610
788
|
```typescript
|
|
611
789
|
// Same key on both stores (default)
|
|
612
790
|
mirrorKey(customersStore, 'CUSTOMERS', sessionStore);
|
|
@@ -623,40 +801,24 @@ mirrorKey(customersStore, 'CUSTOMERS', sessionStore, { destroyRef });
|
|
|
623
801
|
mirrorKey(customersStore, 'ITEMS', sessionStore, 'ARTICLES', { destroyRef });
|
|
624
802
|
```
|
|
625
803
|
|
|
626
|
-
**Full example — session
|
|
627
|
-
|
|
628
|
-
```typescript
|
|
629
|
-
// Feature stores
|
|
630
|
-
interface CustomerStoreConfig {
|
|
631
|
-
CUSTOMERS: Customer[];
|
|
632
|
-
}
|
|
633
|
-
export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
634
|
-
|
|
635
|
-
interface OrderStoreConfig {
|
|
636
|
-
ORDERS: Order[];
|
|
637
|
-
}
|
|
638
|
-
export const OrderStore = Store.for<OrderStoreConfig>().build();
|
|
804
|
+
**Full example — session store that aggregates feature stores:**
|
|
639
805
|
|
|
640
|
-
|
|
641
|
-
interface SessionStoreConfig {
|
|
642
|
-
CUSTOMERS: Customer[];
|
|
643
|
-
ORDERS: Order[];
|
|
644
|
-
}
|
|
645
|
-
export const SessionStore = Store.for<SessionStoreConfig>().build();
|
|
806
|
+
For simple aggregation, prefer the [builder `.mirror()` approach](#builder-mirror). Use `mirrorKey` when you need imperative control — e.g. conditional mirroring, late setup, or `DestroyRef`-based cleanup:
|
|
646
807
|
|
|
808
|
+
```typescript
|
|
647
809
|
@Injectable({ providedIn: 'root' })
|
|
648
|
-
export class
|
|
810
|
+
export class SessionStore {
|
|
649
811
|
private readonly customerStore = inject(CustomerStore);
|
|
650
812
|
private readonly orderStore = inject(OrderStore);
|
|
651
|
-
private readonly
|
|
813
|
+
private readonly store = inject(Store.for<SessionStoreConfig>().build());
|
|
652
814
|
private readonly destroyRef = inject(DestroyRef);
|
|
653
815
|
|
|
654
|
-
readonly customers = this.
|
|
655
|
-
readonly orders = this.
|
|
816
|
+
readonly customers = this.store.get('CUSTOMERS');
|
|
817
|
+
readonly orders = this.store.get('ORDERS');
|
|
656
818
|
|
|
657
819
|
constructor() {
|
|
658
|
-
mirrorKey(this.customerStore, 'CUSTOMERS', this.
|
|
659
|
-
mirrorKey(this.orderStore, 'ORDERS', this.
|
|
820
|
+
mirrorKey(this.customerStore, 'CUSTOMERS', this.store, { destroyRef: this.destroyRef });
|
|
821
|
+
mirrorKey(this.orderStore, 'ORDERS', this.store, { destroyRef: this.destroyRef });
|
|
660
822
|
}
|
|
661
823
|
}
|
|
662
824
|
```
|
|
@@ -667,6 +829,33 @@ Everything — loading flags, data, status, errors — is mirrored automatically
|
|
|
667
829
|
|
|
668
830
|
Accumulates single-entity fetches into a `KeyedResourceData` cache on a target store. Each time the source emits a successful entity, it is merged into the target's keyed map by a user-provided `extractId` function.
|
|
669
831
|
|
|
832
|
+
```
|
|
833
|
+
+--------------------+-----------------+--------------------------+
|
|
834
|
+
| CustomerStore | collectKeyed | SessionStore |
|
|
835
|
+
| | | |
|
|
836
|
+
| CUSTOMER_DETAILS | extractId(data) | CUSTOMER_CACHE |
|
|
837
|
+
| (one at a time) | finds the key | (KeyedResourceData) |
|
|
838
|
+
+--------+-----------+-----------------+ |
|
|
839
|
+
| | entities: |
|
|
840
|
+
| fetch("c1") -> Success | c1: { id, name } |
|
|
841
|
+
| fetch("c2") -> Success | c2: { id, name } |
|
|
842
|
+
| fetch("c3") -> Error | |
|
|
843
|
+
| | isLoading: |
|
|
844
|
+
| clear() -> removes last | c1: false |
|
|
845
|
+
| entity | c2: false |
|
|
846
|
+
| | |
|
|
847
|
+
'---- accumulates ----------->| status: |
|
|
848
|
+
| c1: 'Success' |
|
|
849
|
+
| c2: 'Success' |
|
|
850
|
+
| c3: 'Error' |
|
|
851
|
+
| |
|
|
852
|
+
| errors: |
|
|
853
|
+
| c3: [{ code, msg }] |
|
|
854
|
+
+--------------------------+
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Each entity is tracked independently — its own loading flag, status, and errors. The source store fetches one entity at a time; `collectKeyed` builds up the full cache on the target.
|
|
858
|
+
|
|
670
859
|
```typescript
|
|
671
860
|
// Same key on both stores
|
|
672
861
|
collectKeyed(customerStore, 'CUSTOMER_DETAILS', sessionStore, {
|
|
@@ -703,18 +892,17 @@ export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
|
703
892
|
interface SessionStoreConfig {
|
|
704
893
|
CUSTOMER_CACHE: KeyedResourceData<string, Customer>;
|
|
705
894
|
}
|
|
706
|
-
export const SessionStore = Store.for<SessionStoreConfig>().build();
|
|
707
895
|
|
|
708
896
|
@Injectable({ providedIn: 'root' })
|
|
709
|
-
export class
|
|
897
|
+
export class SessionStore {
|
|
710
898
|
private readonly customerStore = inject(CustomerStore);
|
|
711
|
-
private readonly
|
|
899
|
+
private readonly store = inject(Store.for<SessionStoreConfig>().build());
|
|
712
900
|
private readonly destroyRef = inject(DestroyRef);
|
|
713
901
|
|
|
714
|
-
readonly customerCache = this.
|
|
902
|
+
readonly customerCache = this.store.get('CUSTOMER_CACHE');
|
|
715
903
|
|
|
716
904
|
constructor() {
|
|
717
|
-
collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.
|
|
905
|
+
collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.store, 'CUSTOMER_CACHE', {
|
|
718
906
|
extractId: (data) => data?.id,
|
|
719
907
|
destroyRef: this.destroyRef,
|
|
720
908
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flurryx",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Signal-first reactive state management for Angular",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"sideEffects": false,
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flurryx/core": "0.7.
|
|
42
|
-
"@flurryx/store": "0.7.
|
|
43
|
-
"@flurryx/rx": "0.7.
|
|
41
|
+
"@flurryx/core": "0.7.4",
|
|
42
|
+
"@flurryx/store": "0.7.4",
|
|
43
|
+
"@flurryx/rx": "0.7.4"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@angular/core": ">=17.0.0",
|