flurryx 0.6.1 → 0.7.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 +93 -12
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -21,11 +21,12 @@
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();
24
+ // Store — declare your slots with a single interface
25
+ interface ProductStoreConfig {
26
+ LIST: Product[];
27
+ DETAIL: Product;
28
+ }
29
+ export const ProductStore = Store.for<ProductStoreConfig>().build();
29
30
 
30
31
  // Facade
31
32
  @SkipIfCached('LIST', (i) => i.store)
@@ -56,6 +57,7 @@ No `async` pipe. No `subscribe` in templates. No manual unsubscription.
56
57
  - [How to Use](#how-to-use)
57
58
  - [ResourceState](#resourcestate)
58
59
  - [Store API](#store-api)
60
+ - [Store Creation Styles](#store-creation-styles)
59
61
  - [syncToStore](#synctostore)
60
62
  - [syncToKeyedStore](#synctokeyedstore)
61
63
  - [@SkipIfCached](#skipifcached)
@@ -143,18 +145,20 @@ npm install @flurryx/core @flurryx/store @flurryx/rx
143
145
 
144
146
  ### Step 1 — Define your store
145
147
 
146
- Use the `Store` fluent builder to declare named slots. Each slot holds a `ResourceState<T>`.
148
+ Define a TypeScript interface mapping slot names to their data types, then pass it to the `Store` builder:
147
149
 
148
150
  ```typescript
149
151
  import { Store } from "flurryx";
150
152
 
151
- export const ProductStore = Store
152
- .resource('LIST').as<Product[]>()
153
- .resource('DETAIL').as<Product>()
154
- .build();
153
+ interface ProductStoreConfig {
154
+ LIST: Product[];
155
+ DETAIL: Product;
156
+ }
157
+
158
+ export const ProductStore = Store.for<ProductStoreConfig>().build();
155
159
  ```
156
160
 
157
- That's it. No enum, no interface, no class, no constructor. The builder returns an `InjectionToken` with `providedIn: 'root'`.
161
+ 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.
158
162
 
159
163
  ### Step 2 — Create a facade
160
164
 
@@ -252,13 +256,27 @@ A slot starts as `{ data: undefined, isLoading: false, status: undefined, errors
252
256
 
253
257
  ### Store API
254
258
 
255
- The `Store` builder creates a store backed by `WritableSignal<ResourceState>` per slot.
259
+ The `Store` builder creates a store backed by `WritableSignal<ResourceState>` per slot. Three creation styles are available:
256
260
 
257
261
  ```typescript
262
+ // 1. Interface-based (recommended) — type-safe with zero boilerplate
263
+ interface MyStoreConfig {
264
+ USERS: User[];
265
+ SELECTED: User;
266
+ }
267
+ export const MyStore = Store.for<MyStoreConfig>().build();
268
+
269
+ // 2. Fluent chaining — inline slot definitions
258
270
  export const MyStore = Store
259
271
  .resource('USERS').as<User[]>()
260
272
  .resource('SELECTED').as<User>()
261
273
  .build();
274
+
275
+ // 3. Enum-constrained — validates keys against a runtime enum
276
+ export const MyStore = Store.for(MyStoreEnum)
277
+ .resource('USERS').as<User[]>()
278
+ .resource('SELECTED').as<User>()
279
+ .build();
262
280
  ```
263
281
 
264
282
  Once injected, the store exposes these methods:
@@ -283,6 +301,69 @@ Once injected, the store exposes these methods:
283
301
 
284
302
  > Update hooks are stored in a `WeakMap` keyed by store instance, so garbage collection works naturally across multiple store lifetimes.
285
303
 
304
+ ### Store Creation Styles
305
+
306
+ #### Interface-based: `Store.for<Config>().build()`
307
+
308
+ The recommended approach. Define a TypeScript interface where keys are slot names and values are the data types:
309
+
310
+ ```typescript
311
+ import { Store } from "flurryx";
312
+
313
+ interface ChatStoreConfig {
314
+ SESSIONS: ChatSession[];
315
+ CURRENT_SESSION: ChatSession;
316
+ MESSAGES: ChatMessage[];
317
+ }
318
+
319
+ export const ChatStore = Store.for<ChatStoreConfig>().build();
320
+ ```
321
+
322
+ The generic argument is type-only — there is no runtime enum or config object. Under the hood, the store lazily creates signals on first access, so un-accessed keys have zero overhead.
323
+
324
+ Type safety is fully enforced:
325
+
326
+ ```typescript
327
+ const store = inject(ChatStore);
328
+
329
+ store.get('SESSIONS'); // WritableSignal<ResourceState<ChatSession[]>>
330
+ store.update('SESSIONS', { data: [session] }); // ✅ type-checked
331
+ store.update('SESSIONS', { data: 42 }); // ❌ TS error — number is not ChatSession[]
332
+ store.get('INVALID'); // ❌ TS error — key does not exist
333
+ ```
334
+
335
+ #### Fluent chaining: `Store.resource().as<T>().build()`
336
+
337
+ Define slots inline without a separate interface:
338
+
339
+ ```typescript
340
+ export const ChatStore = Store
341
+ .resource('SESSIONS').as<ChatSession[]>()
342
+ .resource('CURRENT_SESSION').as<ChatSession>()
343
+ .resource('MESSAGES').as<ChatMessage[]>()
344
+ .build();
345
+ ```
346
+
347
+ #### Enum-constrained: `Store.for(enum).resource().as<T>().build()`
348
+
349
+ When you have a runtime enum (e.g. shared with backend code), pass it to `.for()` to ensure every key is accounted for:
350
+
351
+ ```typescript
352
+ const ChatStoreEnum = {
353
+ SESSIONS: 'SESSIONS',
354
+ CURRENT_SESSION: 'CURRENT_SESSION',
355
+ MESSAGES: 'MESSAGES',
356
+ } as const;
357
+
358
+ export const ChatStore = Store.for(ChatStoreEnum)
359
+ .resource('SESSIONS').as<ChatSession[]>()
360
+ .resource('CURRENT_SESSION').as<ChatSession>()
361
+ .resource('MESSAGES').as<ChatMessage[]>()
362
+ .build();
363
+ ```
364
+
365
+ The builder only allows keys from the enum, and `.build()` is only available once all keys have been defined.
366
+
286
367
  ### syncToStore
287
368
 
288
369
  RxJS pipeable operator that bridges an `Observable` to a store slot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flurryx",
3
- "version": "0.6.1",
3
+ "version": "0.7.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.6.1",
42
- "@flurryx/store": "0.6.1",
43
- "@flurryx/rx": "0.6.1"
41
+ "@flurryx/core": "0.7.0",
42
+ "@flurryx/store": "0.7.0",
43
+ "@flurryx/rx": "0.7.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@angular/core": ">=17.0.0",