flurryx 0.7.5 → 0.8.0
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 +97 -29
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -29,18 +29,37 @@ interface ProductStoreConfig {
|
|
|
29
29
|
export const ProductStore = Store.for<ProductStoreConfig>().build();
|
|
30
30
|
|
|
31
31
|
// Facade
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class ProductFacade {
|
|
34
|
+
@SkipIfCached("LIST", (i) => i.store)
|
|
35
|
+
@Loading("LIST", (i) => i.store)
|
|
36
|
+
loadProducts() {
|
|
37
|
+
this.http
|
|
38
|
+
.get<Product[]>("/api/products")
|
|
39
|
+
.pipe(syncToStore(this.store, "LIST"))
|
|
40
|
+
.subscribe();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Read signals from the facade
|
|
44
|
+
getProducts() {
|
|
45
|
+
return this.store.get("LIST");
|
|
46
|
+
}
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
// Component
|
|
41
|
-
@
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
// Component — read the facade signal once, use it in the template
|
|
50
|
+
@Component({
|
|
51
|
+
selector: "app-product-list",
|
|
52
|
+
template: `
|
|
53
|
+
@if (productsState().isLoading) {
|
|
54
|
+
<spinner />
|
|
55
|
+
} @for (product of productsState().data; track product.id) {
|
|
56
|
+
<product-card [product]="product" />
|
|
57
|
+
}
|
|
58
|
+
`,
|
|
59
|
+
})
|
|
60
|
+
export class ProductListComponent {
|
|
61
|
+
private readonly facade = inject(ProductFacade);
|
|
62
|
+
readonly productsState = this.facade.getProducts();
|
|
44
63
|
}
|
|
45
64
|
```
|
|
46
65
|
|
|
@@ -67,6 +86,7 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
|
|
|
67
86
|
- [Keyed Resources](#keyed-resources)
|
|
68
87
|
- [Store Mirroring](#store-mirroring)
|
|
69
88
|
- [Builder .mirror()](#builder-mirror)
|
|
89
|
+
- [Builder .mirrorSelf()](#builder-mirrorself)
|
|
70
90
|
- [Builder .mirrorKeyed()](#builder-mirrorkeyed)
|
|
71
91
|
- [mirrorKey](#mirrorkey)
|
|
72
92
|
- [collectKeyed](#collectkeyed)
|
|
@@ -163,7 +183,7 @@ interface ProductStoreConfig {
|
|
|
163
183
|
export const ProductStore = Store.for<ProductStoreConfig>().build();
|
|
164
184
|
```
|
|
165
185
|
|
|
166
|
-
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 `
|
|
186
|
+
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 `Signal<ResourceState<Product[]>>`, and invalid keys or mismatched types are caught at compile time.
|
|
167
187
|
|
|
168
188
|
### Step 2 — Create a facade
|
|
169
189
|
|
|
@@ -174,29 +194,33 @@ import { Injectable, inject } from "@angular/core";
|
|
|
174
194
|
import { HttpClient } from "@angular/common/http";
|
|
175
195
|
import { syncToStore, SkipIfCached, Loading } from "flurryx";
|
|
176
196
|
|
|
177
|
-
@Injectable(
|
|
197
|
+
@Injectable()
|
|
178
198
|
export class ProductFacade {
|
|
179
199
|
private readonly http = inject(HttpClient);
|
|
180
200
|
readonly store = inject(ProductStore);
|
|
181
201
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
getProducts() {
|
|
203
|
+
return this.store.get("LIST");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getProductDetail() {
|
|
207
|
+
return this.store.get("DETAIL");
|
|
208
|
+
}
|
|
185
209
|
|
|
186
|
-
@SkipIfCached(
|
|
187
|
-
@Loading(
|
|
210
|
+
@SkipIfCached("LIST", (i: ProductFacade) => i.store)
|
|
211
|
+
@Loading("LIST", (i: ProductFacade) => i.store)
|
|
188
212
|
loadProducts() {
|
|
189
213
|
this.http
|
|
190
214
|
.get<Product[]>("/api/products")
|
|
191
|
-
.pipe(syncToStore(this.store,
|
|
215
|
+
.pipe(syncToStore(this.store, "LIST"))
|
|
192
216
|
.subscribe();
|
|
193
217
|
}
|
|
194
218
|
|
|
195
|
-
@Loading(
|
|
219
|
+
@Loading("DETAIL", (i: ProductFacade) => i.store)
|
|
196
220
|
loadProduct(id: string) {
|
|
197
221
|
this.http
|
|
198
222
|
.get<Product>(`/api/products/${id}`)
|
|
199
|
-
.pipe(syncToStore(this.store,
|
|
223
|
+
.pipe(syncToStore(this.store, "DETAIL"))
|
|
200
224
|
.subscribe();
|
|
201
225
|
}
|
|
202
226
|
}
|
|
@@ -207,18 +231,19 @@ export class ProductFacade {
|
|
|
207
231
|
```typescript
|
|
208
232
|
@Component({
|
|
209
233
|
template: `
|
|
210
|
-
@if (
|
|
234
|
+
@if (productsState().isLoading) {
|
|
211
235
|
<spinner />
|
|
212
|
-
} @if (
|
|
213
|
-
|
|
236
|
+
} @if (productsState().status === 'Success') { @for (product of
|
|
237
|
+
productsState().data; track product.id) {
|
|
214
238
|
<product-card [product]="product" />
|
|
215
|
-
} } @if (
|
|
216
|
-
<error-banner [errors]="
|
|
239
|
+
} } @if (productsState().status === 'Error') {
|
|
240
|
+
<error-banner [errors]="productsState().errors" />
|
|
217
241
|
}
|
|
218
242
|
`,
|
|
219
243
|
})
|
|
220
244
|
export class ProductListComponent {
|
|
221
|
-
readonly facade = inject(ProductFacade);
|
|
245
|
+
private readonly facade = inject(ProductFacade);
|
|
246
|
+
readonly productsState = this.facade.getProducts();
|
|
222
247
|
|
|
223
248
|
constructor() {
|
|
224
249
|
this.facade.loadProducts();
|
|
@@ -261,7 +286,7 @@ A slot starts as `{ data: undefined, isLoading: false, status: undefined, errors
|
|
|
261
286
|
|
|
262
287
|
### Store API
|
|
263
288
|
|
|
264
|
-
The `Store` builder creates a store backed by `
|
|
289
|
+
The `Store` builder creates a store backed by `Signal<ResourceState>` per slot. Three creation styles are available:
|
|
265
290
|
|
|
266
291
|
```typescript
|
|
267
292
|
// 1. Interface-based (recommended) — type-safe with zero boilerplate
|
|
@@ -288,7 +313,7 @@ Once injected, the store exposes these methods:
|
|
|
288
313
|
|
|
289
314
|
| Method | Description |
|
|
290
315
|
| ------------------------- | ------------------------------------------------------------------------------------- |
|
|
291
|
-
| `get(key)` | Returns the `
|
|
316
|
+
| `get(key)` | Returns the `Signal` for a slot |
|
|
292
317
|
| `update(key, partial)` | Merges partial state (immutable spread) |
|
|
293
318
|
| `clear(key)` | Resets a slot to its initial empty state |
|
|
294
319
|
| `clearAll()` | Resets every slot |
|
|
@@ -306,6 +331,10 @@ Once injected, the store exposes these methods:
|
|
|
306
331
|
|
|
307
332
|
> Update hooks are stored in a `WeakMap` keyed by store instance, so garbage collection works naturally across multiple store lifetimes.
|
|
308
333
|
|
|
334
|
+
#### Read-only signals
|
|
335
|
+
|
|
336
|
+
`get(key)` returns a **read-only `Signal`**, not a `WritableSignal`. Consumers can read state but cannot mutate it directly — all writes must go through the store's own methods (`update`, `clear`, `startLoading`, …). This enforces strict encapsulation: the store is the single owner of its state, and external code can only observe it.
|
|
337
|
+
|
|
309
338
|
### Store Creation Styles
|
|
310
339
|
|
|
311
340
|
#### Interface-based: `Store.for<Config>().build()`
|
|
@@ -331,7 +360,7 @@ Type safety is fully enforced:
|
|
|
331
360
|
```typescript
|
|
332
361
|
const store = inject(ChatStore);
|
|
333
362
|
|
|
334
|
-
store.get('SESSIONS'); //
|
|
363
|
+
store.get('SESSIONS'); // Signal<ResourceState<ChatSession[]>>
|
|
335
364
|
store.update('SESSIONS', { data: [session] }); // ✅ type-checked
|
|
336
365
|
store.update('SESSIONS', { data: 42 }); // ❌ TS error — number is not ChatSession[]
|
|
337
366
|
store.get('INVALID'); // ❌ TS error — key does not exist
|
|
@@ -692,6 +721,45 @@ export const SessionStore = Store.for<{ ARTICLES: Item[] }>()
|
|
|
692
721
|
|
|
693
722
|
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
723
|
|
|
724
|
+
### Builder .mirrorSelf()
|
|
725
|
+
|
|
726
|
+
Use `.mirrorSelf()` when one slot in a store should mirror another slot in the same store. It is useful for aliases, local snapshots, or secondary slots that should stay in sync with a primary slot without wiring `onUpdate` manually.
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
interface SessionStoreConfig {
|
|
730
|
+
CUSTOMER_DETAILS: Customer;
|
|
731
|
+
CUSTOMER_SNAPSHOT: Customer;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
735
|
+
.mirrorSelf('CUSTOMER_DETAILS', 'CUSTOMER_SNAPSHOT')
|
|
736
|
+
.build();
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
It mirrors the full resource state one way — `data`, `isLoading`, `status`, and `errors` all flow from the source key to the target key. The target key must be different from the source key.
|
|
740
|
+
|
|
741
|
+
Because it listens to updates on the built store itself, `.mirrorSelf()` also reacts when the source key is updated by another mirror:
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
interface CustomerStoreConfig {
|
|
745
|
+
CUSTOMERS: Customer[];
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
interface SessionStoreConfig {
|
|
749
|
+
CUSTOMERS: Customer[];
|
|
750
|
+
CUSTOMER_COPY: Customer[];
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export const CustomerStore = Store.for<CustomerStoreConfig>().build();
|
|
754
|
+
|
|
755
|
+
export const SessionStore = Store.for<SessionStoreConfig>()
|
|
756
|
+
.mirror(CustomerStore, 'CUSTOMERS')
|
|
757
|
+
.mirrorSelf('CUSTOMERS', 'CUSTOMER_COPY')
|
|
758
|
+
.build();
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`.mirrorSelf()` is available on all builder styles. For fluent builders, declare both slots first, then chain `.mirrorSelf(sourceKey, targetKey)` before `.build()`.
|
|
762
|
+
|
|
695
763
|
### Builder .mirrorKeyed()
|
|
696
764
|
|
|
697
765
|
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).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flurryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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.8.0",
|
|
42
|
+
"@flurryx/store": "0.8.0",
|
|
43
|
+
"@flurryx/rx": "0.8.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"@angular/core": ">=17.0.0",
|