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 +142 -35
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/dist/index.cjs.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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASA,kBAMO;AAGP,
|
|
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,
|
|
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.
|
|
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.
|
|
42
|
-
"@flurryx/store": "0.
|
|
43
|
-
"@flurryx/rx": "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",
|