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.
Files changed (2) hide show
  1. package/README.md +219 -31
  2. 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
- import { mirrorKey, collectKeyed } from "flurryx";
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 facade that aggregates feature stores:**
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
- // Session storemirrors state from feature stores
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 SessionFacade {
810
+ export class SessionStore {
649
811
  private readonly customerStore = inject(CustomerStore);
650
812
  private readonly orderStore = inject(OrderStore);
651
- private readonly sessionStore = inject(SessionStore);
813
+ private readonly store = inject(Store.for<SessionStoreConfig>().build());
652
814
  private readonly destroyRef = inject(DestroyRef);
653
815
 
654
- readonly customers = this.sessionStore.get('CUSTOMERS');
655
- readonly orders = this.sessionStore.get('ORDERS');
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.sessionStore, { destroyRef: this.destroyRef });
659
- mirrorKey(this.orderStore, 'ORDERS', this.sessionStore, { destroyRef: this.destroyRef });
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 SessionFacade {
897
+ export class SessionStore {
710
898
  private readonly customerStore = inject(CustomerStore);
711
- private readonly sessionStore = inject(SessionStore);
899
+ private readonly store = inject(Store.for<SessionStoreConfig>().build());
712
900
  private readonly destroyRef = inject(DestroyRef);
713
901
 
714
- readonly customerCache = this.sessionStore.get('CUSTOMER_CACHE');
902
+ readonly customerCache = this.store.get('CUSTOMER_CACHE');
715
903
 
716
904
  constructor() {
717
- collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.sessionStore, 'CUSTOMER_CACHE', {
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.1",
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.1",
42
- "@flurryx/store": "0.7.1",
43
- "@flurryx/rx": "0.7.1"
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",