flurryx 0.6.2 → 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)
@@ -145,11 +148,7 @@ npm install @flurryx/core @flurryx/store @flurryx/rx
145
148
 
146
149
  ### Step 1 — Define your store
147
150
 
148
- Use the `Store` fluent builder to declare named slots. Each slot holds a `ResourceState<T>`.
149
-
150
- **Option A — Interface-based (recommended)**
151
-
152
- Define a TypeScript interface mapping keys to data types, pass it as a generic:
151
+ Define a TypeScript interface mapping slot names to their data types, then pass it to the `Store` builder:
153
152
 
154
153
  ```typescript
155
154
  import { Store } from "flurryx";
@@ -162,36 +161,7 @@ interface ProductStoreConfig {
162
161
  export const ProductStore = Store.for<ProductStoreConfig>().build();
163
162
  ```
164
163
 
165
- No enum, no chaining, no class. The interface is type-only — zero runtime cost. Every call to `store.get('LIST')` returns `WritableSignal<ResourceState<Product[]>>`, and invalid keys or mismatched types are caught at compile time.
166
-
167
- **Option B — Fluent chaining**
168
-
169
- Declare each slot inline:
170
-
171
- ```typescript
172
- export const ProductStore = Store
173
- .resource('LIST').as<Product[]>()
174
- .resource('DETAIL').as<Product>()
175
- .build();
176
- ```
177
-
178
- **Option C — Enum-constrained**
179
-
180
- Bind the builder to an enum for compile-time key validation:
181
-
182
- ```typescript
183
- const ProductStoreEnum = {
184
- LIST: 'LIST',
185
- DETAIL: 'DETAIL',
186
- } as const;
187
-
188
- export const ProductStore = Store.for(ProductStoreEnum)
189
- .resource('LIST').as<Product[]>()
190
- .resource('DETAIL').as<Product>()
191
- .build();
192
- ```
193
-
194
- All three options return an `InjectionToken` with `providedIn: 'root'`.
164
+ That's it. The interface is type-only — zero runtime cost. The builder returns an `InjectionToken` with `providedIn: 'root'`. Every call to `store.get('LIST')` returns `WritableSignal<ResourceState<Product[]>>`, and invalid keys or mismatched types are caught at compile time.
195
165
 
196
166
  ### Step 2 — Create a facade
197
167
 
@@ -625,6 +595,143 @@ import {
625
595
 
626
596
  ---
627
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
+
628
735
  ## Design Decisions
629
736
 
630
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.6.2",
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.6.2",
42
- "@flurryx/store": "0.6.2",
43
- "@flurryx/rx": "0.6.2"
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",