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.
Files changed (2) hide show
  1. package/README.md +97 -29
  2. 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
- @SkipIfCached('LIST', (i) => i.store)
33
- @Loading('LIST', (i) => i.store)
34
- loadProducts() {
35
- this.http.get<Product[]>('/api/products')
36
- .pipe(syncToStore(this.store, 'LIST'))
37
- .subscribe();
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 template just read the signal
41
- @if (facade.list().isLoading) { <spinner /> }
42
- @for (product of facade.list().data; track product.id) {
43
- <product-card [product]="product" />
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 `WritableSignal<ResourceState<Product[]>>`, and invalid keys or mismatched types are caught at compile time.
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({ providedIn: "root" })
197
+ @Injectable()
178
198
  export class ProductFacade {
179
199
  private readonly http = inject(HttpClient);
180
200
  readonly store = inject(ProductStore);
181
201
 
182
- // Expose signals for templates
183
- readonly list = this.store.get('LIST');
184
- readonly detail = this.store.get('DETAIL');
202
+ getProducts() {
203
+ return this.store.get("LIST");
204
+ }
205
+
206
+ getProductDetail() {
207
+ return this.store.get("DETAIL");
208
+ }
185
209
 
186
- @SkipIfCached('LIST', (i: ProductFacade) => i.store)
187
- @Loading('LIST', (i: ProductFacade) => i.store)
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, 'LIST'))
215
+ .pipe(syncToStore(this.store, "LIST"))
192
216
  .subscribe();
193
217
  }
194
218
 
195
- @Loading('DETAIL', (i: ProductFacade) => i.store)
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, 'DETAIL'))
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 (facade.list().isLoading) {
234
+ @if (productsState().isLoading) {
211
235
  <spinner />
212
- } @if (facade.list().status === 'Success') { @for (product of
213
- facade.list().data; track product.id) {
236
+ } @if (productsState().status === 'Success') { @for (product of
237
+ productsState().data; track product.id) {
214
238
  <product-card [product]="product" />
215
- } } @if (facade.list().status === 'Error') {
216
- <error-banner [errors]="facade.list().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 `WritableSignal<ResourceState>` per slot. Three creation styles are available:
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 `WritableSignal` for a slot |
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'); // WritableSignal<ResourceState<ChatSession[]>>
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.7.5",
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.7.5",
42
- "@flurryx/store": "0.7.5",
43
- "@flurryx/rx": "0.7.5"
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",