flurryx 0.7.1 → 0.7.3
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 +142 -31
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ 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)
|
|
69
70
|
- [mirrorKey](#mirrorkey)
|
|
70
71
|
- [collectKeyed](#collectkeyed)
|
|
71
72
|
- [Design Decisions](#design-decisions)
|
|
@@ -599,14 +600,114 @@ import {
|
|
|
599
600
|
|
|
600
601
|
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
602
|
|
|
603
|
+
```
|
|
604
|
+
+--------------------+ +--------------------+
|
|
605
|
+
| Feature Store A | | |
|
|
606
|
+
| (CUSTOMERS) |-- mirrorKey ------>| |
|
|
607
|
+
+--------------------+ | |
|
|
608
|
+
| Session Store |
|
|
609
|
+
+--------------------+ | (aggregated) |
|
|
610
|
+
| Feature Store B | | |
|
|
611
|
+
| (ORDERS) |-- mirrorKey ------>| CUSTOMERS + |
|
|
612
|
+
+--------------------+ | ORDERS + |
|
|
613
|
+
| CUSTOMER_CACHE + |
|
|
614
|
+
+--------------------+ | |
|
|
615
|
+
| Feature Store C | | |
|
|
616
|
+
| (CUSTOMER_DETAIL) |-- collectKeyed --->| |
|
|
617
|
+
+--------------------+ +--------------------+
|
|
618
|
+
```
|
|
619
|
+
|
|
602
620
|
```typescript
|
|
603
|
-
import { mirrorKey, collectKeyed } from "flurryx";
|
|
621
|
+
import { Store, mirrorKey, collectKeyed } from "flurryx";
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Builder .mirror()
|
|
625
|
+
|
|
626
|
+
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.
|
|
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();
|
|
604
639
|
```
|
|
605
640
|
|
|
641
|
+
**Interface-based builder** (recommended):
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
interface SessionStoreConfig {
|
|
645
|
+
CUSTOMERS: Customer[];
|
|
646
|
+
ORDERS: Order[];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
650
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
651
|
+
.mirror(OrderStore, 'ORDERS')
|
|
652
|
+
.build();
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Fluent chaining:**
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
export const SessionStore = Store
|
|
659
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
660
|
+
.resource('ORDERS').as<Order[]>()
|
|
661
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
662
|
+
.mirror(OrderStore, 'ORDERS')
|
|
663
|
+
.build();
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
**Enum-constrained:**
|
|
667
|
+
|
|
668
|
+
```typescript
|
|
669
|
+
const SessionEnum = { CUSTOMERS: 'CUSTOMERS', ORDERS: 'ORDERS' } as const;
|
|
670
|
+
|
|
671
|
+
export const SessionStore = Store.for(SessionEnum)
|
|
672
|
+
.resource('CUSTOMERS').as<Customer[]>()
|
|
673
|
+
.resource('ORDERS').as<Order[]>()
|
|
674
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
675
|
+
.mirror(OrderStore, 'ORDERS')
|
|
676
|
+
.build();
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Different source and target keys:**
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
export const SessionStore = Store.for<{ ARTICLES: Item[] }>()
|
|
683
|
+
.mirror(ItemStore, 'ITEMS', 'ARTICLES')
|
|
684
|
+
.build();
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
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.
|
|
688
|
+
|
|
606
689
|
### mirrorKey
|
|
607
690
|
|
|
608
691
|
Mirrors a resource key from one store to another. When the source updates, the target is updated with the same state.
|
|
609
692
|
|
|
693
|
+
```
|
|
694
|
+
+------------------+--------------------------------+------------------+
|
|
695
|
+
| CustomerStore | mirrorKey | SessionStore |
|
|
696
|
+
| | | |
|
|
697
|
+
| CUSTOMERS -------|--- onUpdate --> update ------->| CUSTOMERS |
|
|
698
|
+
| | (same key or different) | |
|
|
699
|
+
| { data, | | { data, |
|
|
700
|
+
| status, | | status, |
|
|
701
|
+
| isLoading } | | isLoading } |
|
|
702
|
+
+------------------+--------------------------------+------------------+
|
|
703
|
+
|
|
704
|
+
source.update('CUSTOMERS', { data: [...], status: 'Success' })
|
|
705
|
+
|
|
|
706
|
+
'--> target is automatically updated with the same state
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
You wire it once. Every future update — data, loading, errors — flows automatically. Call the cleanup function or use `destroyRef` to stop.
|
|
710
|
+
|
|
610
711
|
```typescript
|
|
611
712
|
// Same key on both stores (default)
|
|
612
713
|
mirrorKey(customersStore, 'CUSTOMERS', sessionStore);
|
|
@@ -623,40 +724,24 @@ mirrorKey(customersStore, 'CUSTOMERS', sessionStore, { destroyRef });
|
|
|
623
724
|
mirrorKey(customersStore, 'ITEMS', sessionStore, 'ARTICLES', { destroyRef });
|
|
624
725
|
```
|
|
625
726
|
|
|
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();
|
|
727
|
+
**Full example — session store that aggregates feature stores:**
|
|
639
728
|
|
|
640
|
-
|
|
641
|
-
interface SessionStoreConfig {
|
|
642
|
-
CUSTOMERS: Customer[];
|
|
643
|
-
ORDERS: Order[];
|
|
644
|
-
}
|
|
645
|
-
export const SessionStore = Store.for<SessionStoreConfig>().build();
|
|
729
|
+
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
730
|
|
|
731
|
+
```typescript
|
|
647
732
|
@Injectable({ providedIn: 'root' })
|
|
648
|
-
export class
|
|
733
|
+
export class SessionStore {
|
|
649
734
|
private readonly customerStore = inject(CustomerStore);
|
|
650
735
|
private readonly orderStore = inject(OrderStore);
|
|
651
|
-
private readonly
|
|
736
|
+
private readonly store = inject(Store.for<SessionStoreConfig>().build());
|
|
652
737
|
private readonly destroyRef = inject(DestroyRef);
|
|
653
738
|
|
|
654
|
-
readonly customers = this.
|
|
655
|
-
readonly orders = this.
|
|
739
|
+
readonly customers = this.store.get('CUSTOMERS');
|
|
740
|
+
readonly orders = this.store.get('ORDERS');
|
|
656
741
|
|
|
657
742
|
constructor() {
|
|
658
|
-
mirrorKey(this.customerStore, 'CUSTOMERS', this.
|
|
659
|
-
mirrorKey(this.orderStore, 'ORDERS', this.
|
|
743
|
+
mirrorKey(this.customerStore, 'CUSTOMERS', this.store, { destroyRef: this.destroyRef });
|
|
744
|
+
mirrorKey(this.orderStore, 'ORDERS', this.store, { destroyRef: this.destroyRef });
|
|
660
745
|
}
|
|
661
746
|
}
|
|
662
747
|
```
|
|
@@ -667,6 +752,33 @@ Everything — loading flags, data, status, errors — is mirrored automatically
|
|
|
667
752
|
|
|
668
753
|
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
754
|
|
|
755
|
+
```
|
|
756
|
+
+--------------------+-----------------+--------------------------+
|
|
757
|
+
| CustomerStore | collectKeyed | SessionStore |
|
|
758
|
+
| | | |
|
|
759
|
+
| CUSTOMER_DETAILS | extractId(data) | CUSTOMER_CACHE |
|
|
760
|
+
| (one at a time) | finds the key | (KeyedResourceData) |
|
|
761
|
+
+--------+-----------+-----------------+ |
|
|
762
|
+
| | entities: |
|
|
763
|
+
| fetch("c1") -> Success | c1: { id, name } |
|
|
764
|
+
| fetch("c2") -> Success | c2: { id, name } |
|
|
765
|
+
| fetch("c3") -> Error | |
|
|
766
|
+
| | isLoading: |
|
|
767
|
+
| clear() -> removes last | c1: false |
|
|
768
|
+
| entity | c2: false |
|
|
769
|
+
| | |
|
|
770
|
+
'---- accumulates ----------->| status: |
|
|
771
|
+
| c1: 'Success' |
|
|
772
|
+
| c2: 'Success' |
|
|
773
|
+
| c3: 'Error' |
|
|
774
|
+
| |
|
|
775
|
+
| errors: |
|
|
776
|
+
| c3: [{ code, msg }] |
|
|
777
|
+
+--------------------------+
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
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.
|
|
781
|
+
|
|
670
782
|
```typescript
|
|
671
783
|
// Same key on both stores
|
|
672
784
|
collectKeyed(customerStore, 'CUSTOMER_DETAILS', sessionStore, {
|
|
@@ -703,18 +815,17 @@ export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
|
703
815
|
interface SessionStoreConfig {
|
|
704
816
|
CUSTOMER_CACHE: KeyedResourceData<string, Customer>;
|
|
705
817
|
}
|
|
706
|
-
export const SessionStore = Store.for<SessionStoreConfig>().build();
|
|
707
818
|
|
|
708
819
|
@Injectable({ providedIn: 'root' })
|
|
709
|
-
export class
|
|
820
|
+
export class SessionStore {
|
|
710
821
|
private readonly customerStore = inject(CustomerStore);
|
|
711
|
-
private readonly
|
|
822
|
+
private readonly store = inject(Store.for<SessionStoreConfig>().build());
|
|
712
823
|
private readonly destroyRef = inject(DestroyRef);
|
|
713
824
|
|
|
714
|
-
readonly customerCache = this.
|
|
825
|
+
readonly customerCache = this.store.get('CUSTOMER_CACHE');
|
|
715
826
|
|
|
716
827
|
constructor() {
|
|
717
|
-
collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.
|
|
828
|
+
collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.store, 'CUSTOMER_CACHE', {
|
|
718
829
|
extractId: (data) => data?.id,
|
|
719
830
|
destroyRef: this.destroyRef,
|
|
720
831
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flurryx",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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.3",
|
|
42
|
+
"@flurryx/store": "0.7.3",
|
|
43
|
+
"@flurryx/rx": "0.7.3"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@angular/core": ">=17.0.0",
|