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 +59 -69
- package/dist/index.cjs +2 -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 +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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(
|
|
26
|
-
@Loading(
|
|
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,
|
|
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
|
-
- [
|
|
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
|
|
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 {
|
|
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
|
-
|
|
146
|
+
Use the `Store` fluent builder to declare named slots. Each slot holds a `ResourceState<T>`.
|
|
141
147
|
|
|
142
148
|
```typescript
|
|
143
|
-
import {
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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(
|
|
183
|
-
readonly detail = this.store.get(
|
|
174
|
+
readonly list = this.store.get('LIST');
|
|
175
|
+
readonly detail = this.store.get('DETAIL');
|
|
184
176
|
|
|
185
|
-
@SkipIfCached(
|
|
186
|
-
@Loading(
|
|
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,
|
|
182
|
+
.pipe(syncToStore(this.store, 'LIST'))
|
|
191
183
|
.subscribe();
|
|
192
184
|
}
|
|
193
185
|
|
|
194
|
-
@Loading(
|
|
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,
|
|
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
|
-
###
|
|
253
|
+
### Store API
|
|
262
254
|
|
|
263
|
-
`
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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
|
-
|
|
360
|
+
'LIST', // which store slot to check
|
|
360
361
|
(instance) => instance.store, // how to get the store from `this`
|
|
361
|
-
returnObservable?,
|
|
362
|
-
timeoutMs?
|
|
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(
|
|
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(
|
|
388
|
-
@Loading(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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(
|
|
483
|
+
readonly items = this.store.get('ITEMS');
|
|
494
484
|
|
|
495
|
-
@SkipIfCached(
|
|
496
|
-
@Loading(
|
|
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,
|
|
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,
|
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
|
|
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
|
|
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
|
+
"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
|
|
42
|
-
"@flurryx/store": "0.0
|
|
43
|
-
"@flurryx/rx": "0.0
|
|
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",
|