flurryx 0.6.1 → 0.6.2

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 +121 -7
  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)
@@ -145,16 +147,51 @@ npm install @flurryx/core @flurryx/store @flurryx/rx
145
147
 
146
148
  Use the `Store` fluent builder to declare named slots. Each slot holds a `ResourceState<T>`.
147
149
 
150
+ **Option A — Interface-based (recommended)**
151
+
152
+ Define a TypeScript interface mapping keys to data types, pass it as a generic:
153
+
148
154
  ```typescript
149
155
  import { Store } from "flurryx";
150
156
 
157
+ interface ProductStoreConfig {
158
+ LIST: Product[];
159
+ DETAIL: Product;
160
+ }
161
+
162
+ export const ProductStore = Store.for<ProductStoreConfig>().build();
163
+ ```
164
+
165
+ No enum, no chaining, no class. The interface is type-only — zero runtime cost. Every call to `store.get('LIST')` returns `WritableSignal<ResourceState<Product[]>>`, and invalid keys or mismatched types are caught at compile time.
166
+
167
+ **Option B — Fluent chaining**
168
+
169
+ Declare each slot inline:
170
+
171
+ ```typescript
151
172
  export const ProductStore = Store
152
173
  .resource('LIST').as<Product[]>()
153
174
  .resource('DETAIL').as<Product>()
154
175
  .build();
155
176
  ```
156
177
 
157
- That's it. No enum, no interface, no class, no constructor. The builder returns an `InjectionToken` with `providedIn: 'root'`.
178
+ **Option C Enum-constrained**
179
+
180
+ Bind the builder to an enum for compile-time key validation:
181
+
182
+ ```typescript
183
+ const ProductStoreEnum = {
184
+ LIST: 'LIST',
185
+ DETAIL: 'DETAIL',
186
+ } as const;
187
+
188
+ export const ProductStore = Store.for(ProductStoreEnum)
189
+ .resource('LIST').as<Product[]>()
190
+ .resource('DETAIL').as<Product>()
191
+ .build();
192
+ ```
193
+
194
+ All three options return an `InjectionToken` with `providedIn: 'root'`.
158
195
 
159
196
  ### Step 2 — Create a facade
160
197
 
@@ -252,13 +289,27 @@ A slot starts as `{ data: undefined, isLoading: false, status: undefined, errors
252
289
 
253
290
  ### Store API
254
291
 
255
- The `Store` builder creates a store backed by `WritableSignal<ResourceState>` per slot.
292
+ The `Store` builder creates a store backed by `WritableSignal<ResourceState>` per slot. Three creation styles are available:
256
293
 
257
294
  ```typescript
295
+ // 1. Interface-based (recommended) — type-safe with zero boilerplate
296
+ interface MyStoreConfig {
297
+ USERS: User[];
298
+ SELECTED: User;
299
+ }
300
+ export const MyStore = Store.for<MyStoreConfig>().build();
301
+
302
+ // 2. Fluent chaining — inline slot definitions
258
303
  export const MyStore = Store
259
304
  .resource('USERS').as<User[]>()
260
305
  .resource('SELECTED').as<User>()
261
306
  .build();
307
+
308
+ // 3. Enum-constrained — validates keys against a runtime enum
309
+ export const MyStore = Store.for(MyStoreEnum)
310
+ .resource('USERS').as<User[]>()
311
+ .resource('SELECTED').as<User>()
312
+ .build();
262
313
  ```
263
314
 
264
315
  Once injected, the store exposes these methods:
@@ -283,6 +334,69 @@ Once injected, the store exposes these methods:
283
334
 
284
335
  > Update hooks are stored in a `WeakMap` keyed by store instance, so garbage collection works naturally across multiple store lifetimes.
285
336
 
337
+ ### Store Creation Styles
338
+
339
+ #### Interface-based: `Store.for<Config>().build()`
340
+
341
+ The recommended approach. Define a TypeScript interface where keys are slot names and values are the data types:
342
+
343
+ ```typescript
344
+ import { Store } from "flurryx";
345
+
346
+ interface ChatStoreConfig {
347
+ SESSIONS: ChatSession[];
348
+ CURRENT_SESSION: ChatSession;
349
+ MESSAGES: ChatMessage[];
350
+ }
351
+
352
+ export const ChatStore = Store.for<ChatStoreConfig>().build();
353
+ ```
354
+
355
+ 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.
356
+
357
+ Type safety is fully enforced:
358
+
359
+ ```typescript
360
+ const store = inject(ChatStore);
361
+
362
+ store.get('SESSIONS'); // WritableSignal<ResourceState<ChatSession[]>>
363
+ store.update('SESSIONS', { data: [session] }); // ✅ type-checked
364
+ store.update('SESSIONS', { data: 42 }); // ❌ TS error — number is not ChatSession[]
365
+ store.get('INVALID'); // ❌ TS error — key does not exist
366
+ ```
367
+
368
+ #### Fluent chaining: `Store.resource().as<T>().build()`
369
+
370
+ Define slots inline without a separate interface:
371
+
372
+ ```typescript
373
+ export const ChatStore = Store
374
+ .resource('SESSIONS').as<ChatSession[]>()
375
+ .resource('CURRENT_SESSION').as<ChatSession>()
376
+ .resource('MESSAGES').as<ChatMessage[]>()
377
+ .build();
378
+ ```
379
+
380
+ #### Enum-constrained: `Store.for(enum).resource().as<T>().build()`
381
+
382
+ When you have a runtime enum (e.g. shared with backend code), pass it to `.for()` to ensure every key is accounted for:
383
+
384
+ ```typescript
385
+ const ChatStoreEnum = {
386
+ SESSIONS: 'SESSIONS',
387
+ CURRENT_SESSION: 'CURRENT_SESSION',
388
+ MESSAGES: 'MESSAGES',
389
+ } as const;
390
+
391
+ export const ChatStore = Store.for(ChatStoreEnum)
392
+ .resource('SESSIONS').as<ChatSession[]>()
393
+ .resource('CURRENT_SESSION').as<ChatSession>()
394
+ .resource('MESSAGES').as<ChatMessage[]>()
395
+ .build();
396
+ ```
397
+
398
+ The builder only allows keys from the enum, and `.build()` is only available once all keys have been defined.
399
+
286
400
  ### syncToStore
287
401
 
288
402
  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.6.2",
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.6.2",
42
+ "@flurryx/store": "0.6.2",
43
+ "@flurryx/rx": "0.6.2"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@angular/core": ">=17.0.0",