@flurryx/store 0.7.3 → 0.7.5

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 ADDED
@@ -0,0 +1,141 @@
1
+ # @flurryx/store
2
+
3
+ Signal-based reactive store for Angular. Part of the [flurryx](../../README.md) monorepo.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @flurryx/store
9
+ ```
10
+
11
+ Or use the umbrella package which re-exports everything:
12
+
13
+ ```bash
14
+ npm install flurryx
15
+ ```
16
+
17
+ ## Store Creation
18
+
19
+ Three builder styles are available:
20
+
21
+ ```typescript
22
+ import { Store } from "@flurryx/store";
23
+
24
+ // 1. Interface-based (recommended)
25
+ interface MyStoreConfig {
26
+ USERS: User[];
27
+ SELECTED: User;
28
+ }
29
+ export const MyStore = Store.for<MyStoreConfig>().build();
30
+
31
+ // 2. Fluent chaining
32
+ export const MyStore = Store
33
+ .resource('USERS').as<User[]>()
34
+ .resource('SELECTED').as<User>()
35
+ .build();
36
+
37
+ // 3. Enum-constrained
38
+ const Enum = { USERS: 'USERS', SELECTED: 'SELECTED' } as const;
39
+ export const MyStore = Store.for(Enum)
40
+ .resource('USERS').as<User[]>()
41
+ .resource('SELECTED').as<User>()
42
+ .build();
43
+ ```
44
+
45
+ ## Store API
46
+
47
+ ### Basic operations
48
+
49
+ | Method | Description |
50
+ |---|---|
51
+ | `get(key)` | Returns the `WritableSignal` for a slot |
52
+ | `update(key, partial)` | Merges partial state (immutable spread) |
53
+ | `clear(key)` | Resets a slot to its initial empty state |
54
+ | `clearAll()` | Resets every slot |
55
+ | `startLoading(key)` | Sets `isLoading: true`, clears `status` and `errors` |
56
+ | `stopLoading(key)` | Sets `isLoading: false`, clears `status` and `errors` |
57
+ | `onUpdate(key, callback)` | Registers a listener fired after `update` or `clear`. Returns an unsubscribe function |
58
+
59
+ ### Keyed operations
60
+
61
+ For slots holding `KeyedResourceData<TKey, TValue>`:
62
+
63
+ | Method | Description |
64
+ |---|---|
65
+ | `updateKeyedOne(key, resourceKey, entity)` | Merges one entity into a keyed slot |
66
+ | `clearKeyedOne(key, resourceKey)` | Removes one entity from a keyed slot |
67
+ | `startKeyedLoading(key, resourceKey)` | Sets loading for a single resource key |
68
+
69
+ ## Clearing Store Data
70
+
71
+ ### Whole-slot clearing
72
+
73
+ Reset an entire store slot back to its initial empty state:
74
+
75
+ ```typescript
76
+ const store = inject(ProductStore);
77
+
78
+ // Clear a single slot
79
+ store.clear('LIST');
80
+
81
+ // Clear every slot in the store
82
+ store.clearAll();
83
+ ```
84
+
85
+ ### Per-key clearing for keyed resources
86
+
87
+ When a slot holds a `KeyedResourceData`, `clear('ITEMS')` wipes **every** cached entity. To invalidate a single entry, use `clearKeyedOne`:
88
+
89
+ ```typescript
90
+ import { Store } from "@flurryx/store";
91
+ import type { KeyedResourceData } from "@flurryx/core";
92
+
93
+ // Define a store with a keyed slot
94
+ interface InvoiceStoreConfig {
95
+ ITEMS: KeyedResourceData<string, Invoice>;
96
+ }
97
+ export const InvoiceStore = Store.for<InvoiceStoreConfig>().build();
98
+ ```
99
+
100
+ ```typescript
101
+ const store = inject(InvoiceStore);
102
+
103
+ // Remove only invoice "inv-42" from the cache.
104
+ // All other cached invoices remain untouched.
105
+ store.clearKeyedOne('ITEMS', 'inv-42');
106
+ ```
107
+
108
+ `clearKeyedOne` removes the entity, its loading flag, status, and errors for that key, then recalculates the top-level `isLoading` based on remaining keys.
109
+
110
+ **Facade example:**
111
+
112
+ ```typescript
113
+ @Injectable({ providedIn: 'root' })
114
+ export class InvoiceFacade {
115
+ private readonly http = inject(HttpClient);
116
+ readonly store = inject(InvoiceStore);
117
+
118
+ deleteInvoice(id: string) {
119
+ this.http.delete(`/api/invoices/${id}`).subscribe(() => {
120
+ // Evict only this invoice from the keyed cache
121
+ this.store.clearKeyedOne('ITEMS', id);
122
+ });
123
+ }
124
+ }
125
+ ```
126
+
127
+ **Comparison:**
128
+
129
+ | Method | Scope | Use when |
130
+ |---|---|---|
131
+ | `clear(key)` | Entire slot | Logging out, resetting a form, full refresh |
132
+ | `clearAll()` | Every slot | Session teardown |
133
+ | `clearKeyedOne(key, resourceKey)` | Single entity in a keyed slot | Deleting or invalidating one cached item |
134
+
135
+ ## Store Mirroring
136
+
137
+ The store builder supports `.mirror()` and `.mirrorKeyed()` for declarative cross-store synchronization. See the [root README](../../README.md#store-mirroring) for full documentation.
138
+
139
+ ## License
140
+
141
+ [MIT](../../LICENSE)
package/dist/index.cjs CHANGED
@@ -273,6 +273,7 @@ var BaseStore = class {
273
273
 
274
274
  // src/lazy-store.ts
275
275
  var import_core3 = require("@angular/core");
276
+ var import_core4 = require("@flurryx/core");
276
277
  function createDefaultState() {
277
278
  return {
278
279
  data: void 0,
@@ -336,6 +337,96 @@ var LazyStore = class {
336
337
  })
337
338
  );
338
339
  }
340
+ updateKeyedOne(key, resourceKey, entity) {
341
+ const sig = this.getOrCreate(key);
342
+ const state = sig();
343
+ const data = (0, import_core4.isKeyedResourceData)(state.data) ? state.data : (0, import_core4.createKeyedResourceData)();
344
+ const nextErrors = { ...data.errors };
345
+ delete nextErrors[resourceKey];
346
+ const nextData = {
347
+ ...data,
348
+ entities: { ...data.entities, [resourceKey]: entity },
349
+ isLoading: { ...data.isLoading, [resourceKey]: false },
350
+ status: { ...data.status, [resourceKey]: "Success" },
351
+ errors: nextErrors
352
+ };
353
+ this.update(key, {
354
+ data: nextData,
355
+ isLoading: (0, import_core4.isAnyKeyLoading)(nextData.isLoading),
356
+ status: void 0,
357
+ errors: void 0
358
+ });
359
+ }
360
+ clearKeyedOne(key, resourceKey) {
361
+ const sig = this.getOrCreate(key);
362
+ const state = sig();
363
+ if (!(0, import_core4.isKeyedResourceData)(state.data)) {
364
+ return;
365
+ }
366
+ const data = state.data;
367
+ const previousState = state;
368
+ const nextEntities = { ...data.entities };
369
+ delete nextEntities[resourceKey];
370
+ const nextIsLoading = { ...data.isLoading };
371
+ delete nextIsLoading[resourceKey];
372
+ const nextStatus = { ...data.status };
373
+ delete nextStatus[resourceKey];
374
+ const nextErrors = { ...data.errors };
375
+ delete nextErrors[resourceKey];
376
+ const nextData = {
377
+ ...data,
378
+ entities: nextEntities,
379
+ isLoading: nextIsLoading,
380
+ status: nextStatus,
381
+ errors: nextErrors
382
+ };
383
+ sig.update(
384
+ (prev) => ({
385
+ ...prev,
386
+ data: nextData,
387
+ status: void 0,
388
+ isLoading: (0, import_core4.isAnyKeyLoading)(nextIsLoading),
389
+ errors: void 0
390
+ })
391
+ );
392
+ const updatedState = sig();
393
+ this.notifyHooks(key, updatedState, previousState);
394
+ }
395
+ startKeyedLoading(key, resourceKey) {
396
+ const sig = this.getOrCreate(key);
397
+ const state = sig();
398
+ if (!(0, import_core4.isKeyedResourceData)(state.data)) {
399
+ this.startLoading(key);
400
+ return;
401
+ }
402
+ const previousState = state;
403
+ const data = state.data;
404
+ const nextIsLoading = {
405
+ ...data.isLoading,
406
+ [resourceKey]: true
407
+ };
408
+ const nextStatus = { ...data.status };
409
+ delete nextStatus[resourceKey];
410
+ const nextErrors = { ...data.errors };
411
+ delete nextErrors[resourceKey];
412
+ const nextData = {
413
+ ...data,
414
+ isLoading: nextIsLoading,
415
+ status: nextStatus,
416
+ errors: nextErrors
417
+ };
418
+ sig.update(
419
+ (previous) => ({
420
+ ...previous,
421
+ data: nextData,
422
+ status: void 0,
423
+ isLoading: (0, import_core4.isAnyKeyLoading)(nextIsLoading),
424
+ errors: void 0
425
+ })
426
+ );
427
+ const updatedState = sig();
428
+ this.notifyHooks(key, updatedState, previousState);
429
+ }
339
430
  onUpdate(key, callback) {
340
431
  if (!this.hooks.has(key)) {
341
432
  this.hooks.set(key, []);
@@ -368,7 +459,7 @@ var LazyStore = class {
368
459
  };
369
460
 
370
461
  // src/store-builder.ts
371
- var import_core4 = require("@angular/core");
462
+ var import_core6 = require("@angular/core");
372
463
 
373
464
  // src/dynamic-store.ts
374
465
  var DynamicStore = class extends BaseStore {
@@ -397,108 +488,6 @@ function mirrorKey(source, sourceKey, target, targetKeyOrOptions, options) {
397
488
  return cleanup;
398
489
  }
399
490
 
400
- // src/resource.ts
401
- function resource() {
402
- return {};
403
- }
404
-
405
- // src/store-builder.ts
406
- function wireMirrors(store, mirrors) {
407
- for (const def of mirrors) {
408
- const sourceStore = (0, import_core4.inject)(def.sourceToken);
409
- mirrorKey(sourceStore, def.sourceKey, store, def.targetKey);
410
- }
411
- return store;
412
- }
413
- function createBuilder(accum, mirrors = []) {
414
- return {
415
- resource(key) {
416
- return {
417
- as() {
418
- const nextAccum = {
419
- ...accum,
420
- [key]: resource()
421
- };
422
- return createBuilder(nextAccum, mirrors);
423
- }
424
- };
425
- },
426
- mirror(source, sourceKey, targetKey) {
427
- const def = {
428
- sourceToken: source,
429
- sourceKey,
430
- targetKey: targetKey ?? sourceKey
431
- };
432
- return createBuilder(accum, [...mirrors, def]);
433
- },
434
- build() {
435
- return new import_core4.InjectionToken("FlurryxStore", {
436
- providedIn: "root",
437
- factory: () => wireMirrors(new DynamicStore(accum), mirrors)
438
- });
439
- }
440
- };
441
- }
442
- function createConstrainedBuilder(_enumObj, accum, mirrors = []) {
443
- return {
444
- resource(key) {
445
- return {
446
- as() {
447
- const nextAccum = {
448
- ...accum,
449
- [key]: resource()
450
- };
451
- return createConstrainedBuilder(_enumObj, nextAccum, mirrors);
452
- }
453
- };
454
- },
455
- mirror(source, sourceKey, targetKey) {
456
- const def = {
457
- sourceToken: source,
458
- sourceKey,
459
- targetKey: targetKey ?? sourceKey
460
- };
461
- return createConstrainedBuilder(_enumObj, accum, [...mirrors, def]);
462
- },
463
- build() {
464
- return new import_core4.InjectionToken("FlurryxStore", {
465
- providedIn: "root",
466
- factory: () => wireMirrors(new DynamicStore(accum), mirrors)
467
- });
468
- }
469
- };
470
- }
471
- function createInterfaceBuilder(mirrors = []) {
472
- return {
473
- mirror(source, sourceKey, targetKey) {
474
- const def = {
475
- sourceToken: source,
476
- sourceKey,
477
- targetKey: targetKey ?? sourceKey
478
- };
479
- return createInterfaceBuilder([...mirrors, def]);
480
- },
481
- build() {
482
- return new import_core4.InjectionToken("FlurryxStore", {
483
- providedIn: "root",
484
- factory: () => wireMirrors(
485
- new LazyStore(),
486
- mirrors
487
- )
488
- });
489
- }
490
- };
491
- }
492
- var Store = {
493
- ...createBuilder({}),
494
- for(enumObj) {
495
- if (arguments.length === 0) {
496
- return createInterfaceBuilder();
497
- }
498
- return createConstrainedBuilder(enumObj, {});
499
- }
500
- };
501
-
502
491
  // src/collect-keyed.ts
503
492
  var import_core5 = require("@flurryx/core");
504
493
  function collectKeyed(source, sourceKey, target, targetKeyOrOptions, options) {
@@ -585,6 +574,175 @@ function collectKeyed(source, sourceKey, target, targetKeyOrOptions, options) {
585
574
  }
586
575
  return cleanup;
587
576
  }
577
+
578
+ // src/resource.ts
579
+ function resource() {
580
+ return {};
581
+ }
582
+
583
+ // src/store-builder.ts
584
+ function wireMirrors(store, mirrors) {
585
+ for (const def of mirrors) {
586
+ const sourceStore = (0, import_core6.inject)(def.sourceToken);
587
+ mirrorKey(sourceStore, def.sourceKey, store, def.targetKey);
588
+ }
589
+ return store;
590
+ }
591
+ function wireMirrorKeyed(store, defs) {
592
+ for (const def of defs) {
593
+ const sourceStore = (0, import_core6.inject)(def.sourceToken);
594
+ collectKeyed(sourceStore, def.sourceKey, store, def.targetKey, {
595
+ extractId: def.extractId
596
+ });
597
+ }
598
+ return store;
599
+ }
600
+ function createBuilder(accum, mirrors = [], mirrorKeyedDefs = []) {
601
+ return {
602
+ resource(key) {
603
+ return {
604
+ as() {
605
+ const nextAccum = {
606
+ ...accum,
607
+ [key]: resource()
608
+ };
609
+ return createBuilder(nextAccum, mirrors, mirrorKeyedDefs);
610
+ }
611
+ };
612
+ },
613
+ mirror(source, sourceKey, targetKey) {
614
+ const def = {
615
+ sourceToken: source,
616
+ sourceKey,
617
+ targetKey: targetKey ?? sourceKey
618
+ };
619
+ return createBuilder(accum, [...mirrors, def], mirrorKeyedDefs);
620
+ },
621
+ mirrorKeyed(source, sourceKey, options, targetKey) {
622
+ const def = {
623
+ sourceToken: source,
624
+ sourceKey,
625
+ targetKey: targetKey ?? sourceKey,
626
+ extractId: options.extractId
627
+ };
628
+ return createBuilder(accum, mirrors, [...mirrorKeyedDefs, def]);
629
+ },
630
+ build() {
631
+ return new import_core6.InjectionToken("FlurryxStore", {
632
+ providedIn: "root",
633
+ factory: () => {
634
+ const store = new DynamicStore(accum);
635
+ wireMirrors(store, mirrors);
636
+ wireMirrorKeyed(store, mirrorKeyedDefs);
637
+ return store;
638
+ }
639
+ });
640
+ }
641
+ };
642
+ }
643
+ function createConstrainedBuilder(_enumObj, accum, mirrors = [], mirrorKeyedDefs = []) {
644
+ return {
645
+ resource(key) {
646
+ return {
647
+ as() {
648
+ const nextAccum = {
649
+ ...accum,
650
+ [key]: resource()
651
+ };
652
+ return createConstrainedBuilder(
653
+ _enumObj,
654
+ nextAccum,
655
+ mirrors,
656
+ mirrorKeyedDefs
657
+ );
658
+ }
659
+ };
660
+ },
661
+ mirror(source, sourceKey, targetKey) {
662
+ const def = {
663
+ sourceToken: source,
664
+ sourceKey,
665
+ targetKey: targetKey ?? sourceKey
666
+ };
667
+ return createConstrainedBuilder(
668
+ _enumObj,
669
+ accum,
670
+ [...mirrors, def],
671
+ mirrorKeyedDefs
672
+ );
673
+ },
674
+ mirrorKeyed(source, sourceKey, options, targetKey) {
675
+ const def = {
676
+ sourceToken: source,
677
+ sourceKey,
678
+ targetKey: targetKey ?? sourceKey,
679
+ extractId: options.extractId
680
+ };
681
+ return createConstrainedBuilder(_enumObj, accum, mirrors, [
682
+ ...mirrorKeyedDefs,
683
+ def
684
+ ]);
685
+ },
686
+ build() {
687
+ return new import_core6.InjectionToken("FlurryxStore", {
688
+ providedIn: "root",
689
+ factory: () => {
690
+ const store = new DynamicStore(accum);
691
+ wireMirrors(store, mirrors);
692
+ wireMirrorKeyed(store, mirrorKeyedDefs);
693
+ return store;
694
+ }
695
+ });
696
+ }
697
+ };
698
+ }
699
+ function createInterfaceBuilder(mirrors = [], mirrorKeyedDefs = []) {
700
+ return {
701
+ mirror(source, sourceKey, targetKey) {
702
+ const def = {
703
+ sourceToken: source,
704
+ sourceKey,
705
+ targetKey: targetKey ?? sourceKey
706
+ };
707
+ return createInterfaceBuilder(
708
+ [...mirrors, def],
709
+ mirrorKeyedDefs
710
+ );
711
+ },
712
+ mirrorKeyed(source, sourceKey, options, targetKey) {
713
+ const def = {
714
+ sourceToken: source,
715
+ sourceKey,
716
+ targetKey: targetKey ?? sourceKey,
717
+ extractId: options.extractId
718
+ };
719
+ return createInterfaceBuilder(mirrors, [
720
+ ...mirrorKeyedDefs,
721
+ def
722
+ ]);
723
+ },
724
+ build() {
725
+ return new import_core6.InjectionToken("FlurryxStore", {
726
+ providedIn: "root",
727
+ factory: () => {
728
+ const store = new LazyStore();
729
+ wireMirrors(store, mirrors);
730
+ wireMirrorKeyed(store, mirrorKeyedDefs);
731
+ return store;
732
+ }
733
+ });
734
+ }
735
+ };
736
+ }
737
+ var Store = {
738
+ ...createBuilder({}),
739
+ for(enumObj) {
740
+ if (arguments.length === 0) {
741
+ return createInterfaceBuilder();
742
+ }
743
+ return createConstrainedBuilder(enumObj, {});
744
+ }
745
+ };
588
746
  // Annotate the CommonJS export names for ESM import in node:
589
747
  0 && (module.exports = {
590
748
  BaseStore,