flurryx 0.0.3 → 0.5.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 CHANGED
@@ -21,12 +21,18 @@
21
21
  flurryx bridges the gap between RxJS async operations and Angular signals. Define a store, pipe your HTTP calls through an operator, read signals in your templates. No actions, no reducers, no effects boilerplate.
22
22
 
23
23
  ```typescript
24
+ // Store — declare your slots in 3 lines
25
+ export const ProductStore = Store
26
+ .resource('LIST').as<Product[]>()
27
+ .resource('DETAIL').as<Product>()
28
+ .build();
29
+
24
30
  // Facade
25
- @SkipIfCached(StoreKey.LIST, (i) => i.store)
26
- @Loading(StoreKey.LIST, (i) => i.store)
31
+ @SkipIfCached('LIST', (i) => i.store)
32
+ @Loading('LIST', (i) => i.store)
27
33
  loadProducts() {
28
34
  this.http.get<Product[]>('/api/products')
29
- .pipe(syncToStore(this.store, StoreKey.LIST))
35
+ .pipe(syncToStore(this.store, 'LIST'))
30
36
  .subscribe();
31
37
  }
32
38
 
@@ -49,7 +55,7 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
49
55
  - [Getting Started](#getting-started)
50
56
  - [How to Use](#how-to-use)
51
57
  - [ResourceState](#resourcestate)
52
- - [BaseStore API](#basestore-api)
58
+ - [Store API](#store-api)
53
59
  - [syncToStore](#synctostore)
54
60
  - [syncToKeyedStore](#synctokeyedstore)
55
61
  - [@SkipIfCached](#skipifcached)
@@ -75,7 +81,7 @@ Angular signals are great for synchronous reactivity, but real applications stil
75
81
  | Duplicate requests | Manual `distinctUntilChanged`, inflight tracking | `@SkipIfCached` deduplicates while loading |
76
82
  | Keyed resources | Separate state per ID, boilerplate explosion | `KeyedResourceData` with per-key loading/error |
77
83
 
78
- flurryx gives you **one abstract class**, **two RxJS operators**, and **two decorators**. That's the entire API.
84
+ flurryx gives you **one fluent builder**, **two RxJS operators**, and **two decorators**. That's the entire API.
79
85
 
80
86
  ---
81
87
 
@@ -88,7 +94,7 @@ npm install flurryx
88
94
  That's it. The `flurryx` package re-exports everything from the three internal packages (`@flurryx/core`, `@flurryx/store`, `@flurryx/rx`), so every import comes from a single place:
89
95
 
90
96
  ```typescript
91
- import { BaseStore, syncToStore, SkipIfCached, Loading } from "flurryx";
97
+ import { Store, syncToStore, SkipIfCached, Loading } from "flurryx";
92
98
  import type { ResourceState, KeyedResourceData } from "flurryx";
93
99
  ```
94
100
 
@@ -137,33 +143,19 @@ npm install @flurryx/core @flurryx/store @flurryx/rx
137
143
 
138
144
  ### Step 1 — Define your store
139
145
 
140
- A store is an enum of named slots + a type map. Each slot holds a `ResourceState<T>`.
146
+ Use the `Store` fluent builder to declare named slots. Each slot holds a `ResourceState<T>`.
141
147
 
142
148
  ```typescript
143
- import { Injectable } from "@angular/core";
144
- import { BaseStore, type ResourceState } from "flurryx";
145
-
146
- enum ProductStoreEnum {
147
- LIST = "LIST",
148
- DETAIL = "DETAIL",
149
- }
149
+ import { Store } from "flurryx";
150
150
 
151
- interface ProductStoreData {
152
- [ProductStoreEnum.LIST]: ResourceState<Product[]>;
153
- [ProductStoreEnum.DETAIL]: ResourceState<Product>;
154
- }
155
-
156
- @Injectable({ providedIn: "root" })
157
- export class ProductStore extends BaseStore<
158
- typeof ProductStoreEnum,
159
- ProductStoreData
160
- > {
161
- constructor() {
162
- super(ProductStoreEnum);
163
- }
164
- }
151
+ export const ProductStore = Store
152
+ .resource('LIST').as<Product[]>()
153
+ .resource('DETAIL').as<Product>()
154
+ .build();
165
155
  ```
166
156
 
157
+ That's it. No enum, no interface, no class, no constructor. The builder returns an `InjectionToken` with `providedIn: 'root'`.
158
+
167
159
  ### Step 2 — Create a facade
168
160
 
169
161
  The facade owns the store and exposes signals + data-fetching methods.
@@ -179,23 +171,23 @@ export class ProductFacade {
179
171
  readonly store = inject(ProductStore);
180
172
 
181
173
  // Expose signals for templates
182
- readonly list = this.store.get(ProductStoreEnum.LIST);
183
- readonly detail = this.store.get(ProductStoreEnum.DETAIL);
174
+ readonly list = this.store.get('LIST');
175
+ readonly detail = this.store.get('DETAIL');
184
176
 
185
- @SkipIfCached(ProductStoreEnum.LIST, (i: ProductFacade) => i.store)
186
- @Loading(ProductStoreEnum.LIST, (i: ProductFacade) => i.store)
177
+ @SkipIfCached('LIST', (i: ProductFacade) => i.store)
178
+ @Loading('LIST', (i: ProductFacade) => i.store)
187
179
  loadProducts() {
188
180
  this.http
189
181
  .get<Product[]>("/api/products")
190
- .pipe(syncToStore(this.store, ProductStoreEnum.LIST))
182
+ .pipe(syncToStore(this.store, 'LIST'))
191
183
  .subscribe();
192
184
  }
193
185
 
194
- @Loading(ProductStoreEnum.DETAIL, (i: ProductFacade) => i.store)
186
+ @Loading('DETAIL', (i: ProductFacade) => i.store)
195
187
  loadProduct(id: string) {
196
188
  this.http
197
189
  .get<Product>(`/api/products/${id}`)
198
- .pipe(syncToStore(this.store, ProductStoreEnum.DETAIL))
190
+ .pipe(syncToStore(this.store, 'DETAIL'))
199
191
  .subscribe();
200
192
  }
201
193
  }
@@ -258,9 +250,18 @@ A slot starts as `{ data: undefined, isLoading: false, status: undefined, errors
258
250
  └───────────┘
259
251
  ```
260
252
 
261
- ### BaseStore API
253
+ ### Store API
262
254
 
263
- `BaseStore<TEnum, TData>` manages a `Map<keyof TEnum, WritableSignal<ResourceState>>`.
255
+ The `Store` builder creates a store backed by `WritableSignal<ResourceState>` per slot.
256
+
257
+ ```typescript
258
+ export const MyStore = Store
259
+ .resource('USERS').as<User[]>()
260
+ .resource('SELECTED').as<User>()
261
+ .build();
262
+ ```
263
+
264
+ Once injected, the store exposes these methods:
264
265
 
265
266
  | Method | Description |
266
267
  | ------------------------- | ------------------------------------------------------------------------------------- |
@@ -289,7 +290,7 @@ RxJS pipeable operator that bridges an `Observable` to a store slot.
289
290
  ```typescript
290
291
  this.http
291
292
  .get<Product[]>("/api/products")
292
- .pipe(syncToStore(this.store, ProductStoreEnum.LIST))
293
+ .pipe(syncToStore(this.store, 'LIST'))
293
294
  .subscribe();
294
295
  ```
295
296
 
@@ -316,7 +317,7 @@ Same pattern, but targets a specific resource key within a `KeyedResourceData` s
316
317
  ```typescript
317
318
  this.http
318
319
  .get<Invoice>(`/api/invoices/${id}`)
319
- .pipe(syncToKeyedStore(this.store, InvoiceStoreEnum.ITEMS, id))
320
+ .pipe(syncToKeyedStore(this.store, 'ITEMS', id))
320
321
  .subscribe();
321
322
  ```
322
323
 
@@ -325,7 +326,7 @@ Only the targeted resource key is updated. Other keys in the same slot are untou
325
326
  **`mapResponse`** — transform the API response before writing to the store:
326
327
 
327
328
  ```typescript
328
- syncToKeyedStore(this.store, StoreKey.ITEMS, id, {
329
+ syncToKeyedStore(this.store, 'ITEMS', id, {
329
330
  mapResponse: (response) => response.data,
330
331
  });
331
332
  ```
@@ -335,7 +336,7 @@ syncToKeyedStore(this.store, StoreKey.ITEMS, id, {
335
336
  Method decorator that skips execution when the store already has valid data.
336
337
 
337
338
  ```typescript
338
- @SkipIfCached(StoreKey.LIST, (i) => i.store)
339
+ @SkipIfCached('LIST', (i) => i.store)
339
340
  loadProducts() { /* only runs when cache is stale */ }
340
341
  ```
341
342
 
@@ -356,10 +357,10 @@ loadProducts() { /* only runs when cache is stale */ }
356
357
 
357
358
  ```typescript
358
359
  @SkipIfCached(
359
- storeKey, // which store slot to check
360
+ 'LIST', // which store slot to check
360
361
  (instance) => instance.store, // how to get the store from `this`
361
- returnObservable?, // false (default): void methods; true: returns Observable
362
- timeoutMs? // default: 300_000 (5 min). Use CACHE_NO_TIMEOUT for infinite
362
+ returnObservable?, // false (default): void methods; true: returns Observable
363
+ timeoutMs? // default: 300_000 (5 min). Use CACHE_NO_TIMEOUT for infinite
363
364
  )
364
365
  ```
365
366
 
@@ -375,7 +376,7 @@ loadProducts() { /* only runs when cache is stale */ }
375
376
  Method decorator that calls `store.startLoading(key)` before the original method executes.
376
377
 
377
378
  ```typescript
378
- @Loading(StoreKey.LIST, (i) => i.store)
379
+ @Loading('LIST', (i) => i.store)
379
380
  loadProducts() { /* store.isLoading is already true when this runs */ }
380
381
  ```
381
382
 
@@ -384,11 +385,11 @@ loadProducts() { /* store.isLoading is already true when this runs */ }
384
385
  **Compose both decorators** for the common pattern:
385
386
 
386
387
  ```typescript
387
- @SkipIfCached(StoreKey.LIST, (i) => i.store)
388
- @Loading(StoreKey.LIST, (i) => i.store)
388
+ @SkipIfCached('LIST', (i) => i.store)
389
+ @Loading('LIST', (i) => i.store)
389
390
  loadProducts() {
390
391
  this.http.get('/api/products')
391
- .pipe(syncToStore(this.store, StoreKey.LIST))
392
+ .pipe(syncToStore(this.store, 'LIST'))
392
393
  .subscribe();
393
394
  }
394
395
  ```
@@ -418,7 +419,7 @@ import { httpErrorNormalizer } from "flurryx/http";
418
419
  this.http
419
420
  .get("/api/data")
420
421
  .pipe(
421
- syncToStore(this.store, Key.DATA, {
422
+ syncToStore(this.store, 'DATA', {
422
423
  errorNormalizer: httpErrorNormalizer,
423
424
  }),
424
425
  )
@@ -467,37 +468,26 @@ Each resource key gets **independent** loading, status, and error tracking. The
467
468
 
468
469
  ```typescript
469
470
  // Store
470
- enum InvoiceStoreEnum {
471
- ITEMS = "ITEMS",
472
- }
473
-
474
- interface InvoiceStoreData {
475
- [InvoiceStoreEnum.ITEMS]: ResourceState<KeyedResourceData<string, Invoice>>;
476
- }
471
+ import { Store } from "flurryx";
472
+ import type { KeyedResourceData } from "flurryx";
477
473
 
478
- @Injectable({ providedIn: "root" })
479
- export class InvoiceStore extends BaseStore<
480
- typeof InvoiceStoreEnum,
481
- InvoiceStoreData
482
- > {
483
- constructor() {
484
- super(InvoiceStoreEnum);
485
- }
486
- }
474
+ export const InvoiceStore = Store
475
+ .resource('ITEMS').as<KeyedResourceData<string, Invoice>>()
476
+ .build();
487
477
 
488
478
  // Facade
489
479
  @Injectable({ providedIn: "root" })
490
480
  export class InvoiceFacade {
491
481
  private readonly http = inject(HttpClient);
492
482
  readonly store = inject(InvoiceStore);
493
- readonly items = this.store.get(InvoiceStoreEnum.ITEMS);
483
+ readonly items = this.store.get('ITEMS');
494
484
 
495
- @SkipIfCached(InvoiceStoreEnum.ITEMS, (i: InvoiceFacade) => i.store)
496
- @Loading(InvoiceStoreEnum.ITEMS, (i: InvoiceFacade) => i.store)
485
+ @SkipIfCached('ITEMS', (i: InvoiceFacade) => i.store)
486
+ @Loading('ITEMS', (i: InvoiceFacade) => i.store)
497
487
  loadInvoice(id: string) {
498
488
  this.http
499
489
  .get<Invoice>(`/api/invoices/${id}`)
500
- .pipe(syncToKeyedStore(this.store, InvoiceStoreEnum.ITEMS, id))
490
+ .pipe(syncToKeyedStore(this.store, 'ITEMS', id))
501
491
  .subscribe();
502
492
  }
503
493
  }
package/dist/index.cjs CHANGED
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  DEFAULT_CACHE_TTL_MS: () => import_core.DEFAULT_CACHE_TTL_MS,
26
26
  Loading: () => import_rx.Loading,
27
27
  SkipIfCached: () => import_rx.SkipIfCached,
28
+ Store: () => import_store.Store,
28
29
  createKeyedResourceData: () => import_core.createKeyedResourceData,
29
30
  defaultErrorNormalizer: () => import_rx.defaultErrorNormalizer,
30
31
  isAnyKeyLoading: () => import_core.isAnyKeyLoading,
@@ -43,6 +44,7 @@ var import_rx = require("@flurryx/rx");
43
44
  DEFAULT_CACHE_TTL_MS,
44
45
  Loading,
45
46
  SkipIfCached,
47
+ Store,
46
48
  createKeyedResourceData,
47
49
  defaultErrorNormalizer,
48
50
  isAnyKeyLoading,
@@ -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 } 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;AASA,kBAMO;AAGP,mBAA0B;AAG1B,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 } 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":[]}
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 } from '@flurryx/store';
2
+ export { BaseStore, Store } 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 } from '@flurryx/store';
2
+ export { BaseStore, Store } 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 } from "@flurryx/store";
9
+ import { BaseStore, Store } from "@flurryx/store";
10
10
  import {
11
11
  syncToStore,
12
12
  syncToKeyedStore,
@@ -20,6 +20,7 @@ export {
20
20
  DEFAULT_CACHE_TTL_MS,
21
21
  Loading,
22
22
  SkipIfCached,
23
+ Store,
23
24
  createKeyedResourceData,
24
25
  defaultErrorNormalizer,
25
26
  isAnyKeyLoading,
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 } 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,iBAAiB;AAG1B;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 } 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":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flurryx",
3
- "version": "0.0.3",
3
+ "version": "0.5.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.0.1",
42
- "@flurryx/store": "0.0.1",
43
- "@flurryx/rx": "0.0.1"
41
+ "@flurryx/core": "0.5.0",
42
+ "@flurryx/store": "0.5.0",
43
+ "@flurryx/rx": "0.5.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@angular/core": ">=17.0.0",