flurryx 0.7.0 → 0.7.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/README.md CHANGED
@@ -65,6 +65,9 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
65
65
  - [Error Normalization](#error-normalization)
66
66
  - [Constants](#constants)
67
67
  - [Keyed Resources](#keyed-resources)
68
+ - [Store Mirroring](#store-mirroring)
69
+ - [mirrorKey](#mirrorkey)
70
+ - [collectKeyed](#collectkeyed)
68
71
  - [Design Decisions](#design-decisions)
69
72
  - [Contributing](#contributing)
70
73
  - [License](#license)
@@ -592,6 +595,143 @@ import {
592
595
 
593
596
  ---
594
597
 
598
+ ## Store Mirroring
599
+
600
+ 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
+ ```typescript
603
+ import { mirrorKey, collectKeyed } from "flurryx";
604
+ ```
605
+
606
+ ### mirrorKey
607
+
608
+ Mirrors a resource key from one store to another. When the source updates, the target is updated with the same state.
609
+
610
+ ```typescript
611
+ // Same key on both stores (default)
612
+ mirrorKey(customersStore, 'CUSTOMERS', sessionStore);
613
+
614
+ // Different keys
615
+ mirrorKey(customersStore, 'ITEMS', sessionStore, 'ARTICLES');
616
+
617
+ // Manual cleanup
618
+ const cleanup = mirrorKey(customersStore, 'CUSTOMERS', sessionStore);
619
+ cleanup(); // stop mirroring
620
+
621
+ // Auto-cleanup with Angular DestroyRef
622
+ mirrorKey(customersStore, 'CUSTOMERS', sessionStore, { destroyRef });
623
+ mirrorKey(customersStore, 'ITEMS', sessionStore, 'ARTICLES', { destroyRef });
624
+ ```
625
+
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();
639
+
640
+ // Session store — mirrors state from feature stores
641
+ interface SessionStoreConfig {
642
+ CUSTOMERS: Customer[];
643
+ ORDERS: Order[];
644
+ }
645
+ export const SessionStore = Store.for<SessionStoreConfig>().build();
646
+
647
+ @Injectable({ providedIn: 'root' })
648
+ export class SessionFacade {
649
+ private readonly customerStore = inject(CustomerStore);
650
+ private readonly orderStore = inject(OrderStore);
651
+ private readonly sessionStore = inject(SessionStore);
652
+ private readonly destroyRef = inject(DestroyRef);
653
+
654
+ readonly customers = this.sessionStore.get('CUSTOMERS');
655
+ readonly orders = this.sessionStore.get('ORDERS');
656
+
657
+ constructor() {
658
+ mirrorKey(this.customerStore, 'CUSTOMERS', this.sessionStore, { destroyRef: this.destroyRef });
659
+ mirrorKey(this.orderStore, 'ORDERS', this.sessionStore, { destroyRef: this.destroyRef });
660
+ }
661
+ }
662
+ ```
663
+
664
+ Everything — loading flags, data, status, errors — is mirrored automatically. No manual `onUpdate` + cleanup boilerplate.
665
+
666
+ ### collectKeyed
667
+
668
+ 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
+
670
+ ```typescript
671
+ // Same key on both stores
672
+ collectKeyed(customerStore, 'CUSTOMER_DETAILS', sessionStore, {
673
+ extractId: (data) => data?.id,
674
+ destroyRef,
675
+ });
676
+
677
+ // Different keys
678
+ collectKeyed(customerStore, 'CUSTOMER_DETAILS', sessionStore, 'CUSTOMER_CACHE', {
679
+ extractId: (data) => data?.id,
680
+ destroyRef,
681
+ });
682
+ ```
683
+
684
+ **What it does on each source update:**
685
+
686
+ | Source state | Action |
687
+ |---|---|
688
+ | `status: 'Success'` + valid ID | Merges entity into target's keyed data |
689
+ | `status: 'Error'` + valid ID | Records per-key error and status |
690
+ | `isLoading: true` + valid ID | Sets per-key loading flag |
691
+ | Data cleared (e.g. `source.clear()`) | Removes previous entity from target |
692
+
693
+ **Full example — collect individual customer lookups into a cache:**
694
+
695
+ ```typescript
696
+ // Feature store — fetches one customer at a time
697
+ interface CustomerStoreConfig {
698
+ CUSTOMER_DETAILS: Customer;
699
+ }
700
+ export const CustomerStore = Store.for<CustomerStoreConfig>().build();
701
+
702
+ // Session store — accumulates all fetched customers
703
+ interface SessionStoreConfig {
704
+ CUSTOMER_CACHE: KeyedResourceData<string, Customer>;
705
+ }
706
+ export const SessionStore = Store.for<SessionStoreConfig>().build();
707
+
708
+ @Injectable({ providedIn: 'root' })
709
+ export class SessionFacade {
710
+ private readonly customerStore = inject(CustomerStore);
711
+ private readonly sessionStore = inject(SessionStore);
712
+ private readonly destroyRef = inject(DestroyRef);
713
+
714
+ readonly customerCache = this.sessionStore.get('CUSTOMER_CACHE');
715
+
716
+ constructor() {
717
+ collectKeyed(this.customerStore, 'CUSTOMER_DETAILS', this.sessionStore, 'CUSTOMER_CACHE', {
718
+ extractId: (data) => data?.id,
719
+ destroyRef: this.destroyRef,
720
+ });
721
+ }
722
+
723
+ // After loading customers "c1" and "c2", the cache contains:
724
+ // {
725
+ // entities: { c1: Customer, c2: Customer },
726
+ // isLoading: { c1: false, c2: false },
727
+ // status: { c1: 'Success', c2: 'Success' },
728
+ // errors: {}
729
+ // }
730
+ }
731
+ ```
732
+
733
+ ---
734
+
595
735
  ## Design Decisions
596
736
 
597
737
  **Why signals instead of BehaviorSubject?**
package/dist/index.cjs CHANGED
@@ -26,10 +26,12 @@ __export(index_exports, {
26
26
  Loading: () => import_rx.Loading,
27
27
  SkipIfCached: () => import_rx.SkipIfCached,
28
28
  Store: () => import_store.Store,
29
+ collectKeyed: () => import_store.collectKeyed,
29
30
  createKeyedResourceData: () => import_core.createKeyedResourceData,
30
31
  defaultErrorNormalizer: () => import_rx.defaultErrorNormalizer,
31
32
  isAnyKeyLoading: () => import_core.isAnyKeyLoading,
32
33
  isKeyedResourceData: () => import_core.isKeyedResourceData,
34
+ mirrorKey: () => import_store.mirrorKey,
33
35
  syncToKeyedStore: () => import_rx.syncToKeyedStore,
34
36
  syncToStore: () => import_rx.syncToStore
35
37
  });
@@ -45,10 +47,12 @@ var import_rx = require("@flurryx/rx");
45
47
  Loading,
46
48
  SkipIfCached,
47
49
  Store,
50
+ collectKeyed,
48
51
  createKeyedResourceData,
49
52
  defaultErrorNormalizer,
50
53
  isAnyKeyLoading,
51
54
  isKeyedResourceData,
55
+ mirrorKey,
52
56
  syncToKeyedStore,
53
57
  syncToStore
54
58
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @flurryx/core\nexport type {\n ResourceState,\n StoreEnum,\n KeyedResourceData,\n KeyedResourceKey,\n ResourceStatus,\n ResourceErrors,\n} from \"@flurryx/core\";\nexport {\n isKeyedResourceData,\n createKeyedResourceData,\n isAnyKeyLoading,\n CACHE_NO_TIMEOUT,\n DEFAULT_CACHE_TTL_MS,\n} from \"@flurryx/core\";\n\n// @flurryx/store\nexport { BaseStore, Store } from \"@flurryx/store\";\n\n// @flurryx/rx\nexport {\n syncToStore,\n syncToKeyedStore,\n SkipIfCached,\n Loading,\n defaultErrorNormalizer,\n} from \"@flurryx/rx\";\nexport type {\n SyncToStoreOptions,\n SyncToKeyedStoreOptions,\n ErrorNormalizer,\n} from \"@flurryx/rx\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,kBAMO;AAGP,mBAAiC;AAGjC,gBAMO;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @flurryx/core\nexport type {\n ResourceState,\n StoreEnum,\n KeyedResourceData,\n KeyedResourceKey,\n ResourceStatus,\n ResourceErrors,\n} from \"@flurryx/core\";\nexport {\n isKeyedResourceData,\n createKeyedResourceData,\n isAnyKeyLoading,\n CACHE_NO_TIMEOUT,\n DEFAULT_CACHE_TTL_MS,\n} from \"@flurryx/core\";\n\n// @flurryx/store\nexport { BaseStore, Store, mirrorKey, collectKeyed } from \"@flurryx/store\";\nexport type { MirrorOptions, CollectKeyedOptions } from \"@flurryx/store\";\n\n// @flurryx/rx\nexport {\n syncToStore,\n syncToKeyedStore,\n SkipIfCached,\n Loading,\n defaultErrorNormalizer,\n} from \"@flurryx/rx\";\nexport type {\n SyncToStoreOptions,\n SyncToKeyedStoreOptions,\n ErrorNormalizer,\n} from \"@flurryx/rx\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,kBAMO;AAGP,mBAA0D;AAI1D,gBAMO;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { CACHE_NO_TIMEOUT, DEFAULT_CACHE_TTL_MS, KeyedResourceData, KeyedResourceKey, ResourceErrors, ResourceState, ResourceStatus, StoreEnum, createKeyedResourceData, isAnyKeyLoading, isKeyedResourceData } from '@flurryx/core';
2
- export { BaseStore, Store } from '@flurryx/store';
2
+ export { BaseStore, CollectKeyedOptions, MirrorOptions, Store, collectKeyed, mirrorKey } from '@flurryx/store';
3
3
  export { ErrorNormalizer, Loading, SkipIfCached, SyncToKeyedStoreOptions, SyncToStoreOptions, defaultErrorNormalizer, syncToKeyedStore, syncToStore } from '@flurryx/rx';
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { CACHE_NO_TIMEOUT, DEFAULT_CACHE_TTL_MS, KeyedResourceData, KeyedResourceKey, ResourceErrors, ResourceState, ResourceStatus, StoreEnum, createKeyedResourceData, isAnyKeyLoading, isKeyedResourceData } from '@flurryx/core';
2
- export { BaseStore, Store } from '@flurryx/store';
2
+ export { BaseStore, CollectKeyedOptions, MirrorOptions, Store, collectKeyed, mirrorKey } from '@flurryx/store';
3
3
  export { ErrorNormalizer, Loading, SkipIfCached, SyncToKeyedStoreOptions, SyncToStoreOptions, defaultErrorNormalizer, syncToKeyedStore, syncToStore } from '@flurryx/rx';
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  CACHE_NO_TIMEOUT,
7
7
  DEFAULT_CACHE_TTL_MS
8
8
  } from "@flurryx/core";
9
- import { BaseStore, Store } from "@flurryx/store";
9
+ import { BaseStore, Store, mirrorKey, collectKeyed } from "@flurryx/store";
10
10
  import {
11
11
  syncToStore,
12
12
  syncToKeyedStore,
@@ -21,10 +21,12 @@ export {
21
21
  Loading,
22
22
  SkipIfCached,
23
23
  Store,
24
+ collectKeyed,
24
25
  createKeyedResourceData,
25
26
  defaultErrorNormalizer,
26
27
  isAnyKeyLoading,
27
28
  isKeyedResourceData,
29
+ mirrorKey,
28
30
  syncToKeyedStore,
29
31
  syncToStore
30
32
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @flurryx/core\nexport type {\n ResourceState,\n StoreEnum,\n KeyedResourceData,\n KeyedResourceKey,\n ResourceStatus,\n ResourceErrors,\n} from \"@flurryx/core\";\nexport {\n isKeyedResourceData,\n createKeyedResourceData,\n isAnyKeyLoading,\n CACHE_NO_TIMEOUT,\n DEFAULT_CACHE_TTL_MS,\n} from \"@flurryx/core\";\n\n// @flurryx/store\nexport { BaseStore, Store } from \"@flurryx/store\";\n\n// @flurryx/rx\nexport {\n syncToStore,\n syncToKeyedStore,\n SkipIfCached,\n Loading,\n defaultErrorNormalizer,\n} from \"@flurryx/rx\";\nexport type {\n SyncToStoreOptions,\n SyncToKeyedStoreOptions,\n ErrorNormalizer,\n} from \"@flurryx/rx\";\n"],"mappings":";AASA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,WAAW,aAAa;AAGjC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["// @flurryx/core\nexport type {\n ResourceState,\n StoreEnum,\n KeyedResourceData,\n KeyedResourceKey,\n ResourceStatus,\n ResourceErrors,\n} from \"@flurryx/core\";\nexport {\n isKeyedResourceData,\n createKeyedResourceData,\n isAnyKeyLoading,\n CACHE_NO_TIMEOUT,\n DEFAULT_CACHE_TTL_MS,\n} from \"@flurryx/core\";\n\n// @flurryx/store\nexport { BaseStore, Store, mirrorKey, collectKeyed } from \"@flurryx/store\";\nexport type { MirrorOptions, CollectKeyedOptions } from \"@flurryx/store\";\n\n// @flurryx/rx\nexport {\n syncToStore,\n syncToKeyedStore,\n SkipIfCached,\n Loading,\n defaultErrorNormalizer,\n} from \"@flurryx/rx\";\nexport type {\n SyncToStoreOptions,\n SyncToKeyedStoreOptions,\n ErrorNormalizer,\n} from \"@flurryx/rx\";\n"],"mappings":";AASA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAGP,SAAS,WAAW,OAAO,WAAW,oBAAoB;AAI1D;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flurryx",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
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.0",
42
- "@flurryx/store": "0.7.0",
43
- "@flurryx/rx": "0.7.0"
41
+ "@flurryx/core": "0.7.1",
42
+ "@flurryx/store": "0.7.1",
43
+ "@flurryx/rx": "0.7.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@angular/core": ">=17.0.0",