ngssm-store 21.0.0 → 21.1.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 +475 -63
- package/fesm2022/ngssm-store-caching-testing.mjs +4 -6
- package/fesm2022/ngssm-store-caching-testing.mjs.map +1 -1
- package/fesm2022/ngssm-store-caching.mjs +14 -11
- package/fesm2022/ngssm-store-caching.mjs.map +1 -1
- package/fesm2022/ngssm-store-testing.mjs +5 -3
- package/fesm2022/ngssm-store-testing.mjs.map +1 -1
- package/fesm2022/ngssm-store-visibility-testing.mjs +5 -7
- package/fesm2022/ngssm-store-visibility-testing.mjs.map +1 -1
- package/fesm2022/ngssm-store-visibility.mjs +44 -50
- package/fesm2022/ngssm-store-visibility.mjs.map +1 -1
- package/fesm2022/ngssm-store.mjs +59 -64
- package/fesm2022/ngssm-store.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,85 +1,497 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ngssm-store
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A lightweight, production-ready state management library for Angular applications based on the Redux pattern. Provides centralized state management with full support for reducers, effects, and modern Angular signals.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
graph TB;
|
|
7
|
-
B["Store (State manager)"]
|
|
8
|
-
C[/Actions queue/]
|
|
9
|
-
A["State Observers: <br/> <ul> <li>Components</li> <li>Directives</li> <li>Guards</li></ul>"]
|
|
10
|
-
|
|
11
|
-
subgraph G[Action processors]
|
|
12
|
-
D[<b>Reducers</b> <br/> Updates state synchronously taking state immutability into account]
|
|
13
|
-
E[<b>Effects</b> <br/> No update of the state. <br/> Call to remote services <br/> Actions dispatch...]
|
|
14
|
-
end
|
|
5
|
+
## Overview
|
|
15
6
|
|
|
16
|
-
|
|
17
|
-
A -- Dispatch actions --> B
|
|
18
|
-
B -- Publish state --> A
|
|
19
|
-
B -- Apply action on state --> D
|
|
20
|
-
B -- Process action --> E
|
|
21
|
-
D -- Updated state --> B
|
|
22
|
-
E -- Dispatch actions --> B
|
|
7
|
+
`ngssm-store` is a simple yet powerful custom implementation of the Redux pattern designed specifically for Angular. It leverages Angular's dependency injection, RxJS for reactive updates, and modern Angular signals for optimal reactivity.
|
|
23
8
|
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
### Key Features
|
|
10
|
+
|
|
11
|
+
- **Centralized State Management**: Single source of truth for application state
|
|
12
|
+
- **Redux Pattern**: Actions → Reducers → State → Effects → Actions
|
|
13
|
+
- **Dual Reactivity**: Both RxJS observables and Angular Signals support
|
|
14
|
+
- **Immutable State**: Uses `immutability-helper` to ensure state immutability
|
|
15
|
+
- **Effect System**: Side effects, async operations, and action chaining
|
|
16
|
+
- **Feature States**: Modular state management with feature-based organization
|
|
17
|
+
- **Action Queue**: Sequential action processing for predictable state updates
|
|
18
|
+
- **Logging & Debugging**: Built-in logging system for monitoring state changes
|
|
19
|
+
- **TypeScript Support**: Fully typed for better developer experience
|
|
20
|
+
- **Dependency Injection**: Leverages Angular's DI system
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
### Redux Flow
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
Component/Effect
|
|
28
|
+
↓ (dispatch)
|
|
29
|
+
Action → Store → Action Queue
|
|
30
|
+
↓
|
|
31
|
+
Process Next Action
|
|
32
|
+
↓
|
|
33
|
+
Apply to Reducers
|
|
34
|
+
↓
|
|
35
|
+
Update State Immutably
|
|
36
|
+
↓
|
|
37
|
+
Publish New State
|
|
38
|
+
↓
|
|
39
|
+
Process Effects
|
|
40
|
+
↓
|
|
41
|
+
(Can dispatch actions)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm install ngssm-store
|
|
26
48
|
```
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
### Peer Dependencies
|
|
51
|
+
|
|
52
|
+
- `@angular/core` >= 20.0.0
|
|
53
|
+
- `@angular/common` >= 20.0.0
|
|
54
|
+
- `immutability-helper` >= 3.1.1
|
|
55
|
+
|
|
56
|
+
## Setup
|
|
57
|
+
|
|
58
|
+
### Global Provider
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
Initialize the store in your application bootstrapping:
|
|
31
61
|
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
S->>E: add message to process next action
|
|
62
|
+
```typescript
|
|
63
|
+
import { provideNgssmStore } from 'ngssm-store';
|
|
64
|
+
|
|
65
|
+
bootstrapApplication(AppComponent, {
|
|
66
|
+
providers: [
|
|
67
|
+
provideNgssmStore(),
|
|
68
|
+
]
|
|
69
|
+
});
|
|
41
70
|
```
|
|
42
71
|
|
|
43
|
-
|
|
72
|
+
## Core Concepts
|
|
44
73
|
|
|
45
|
-
###
|
|
74
|
+
### Actions
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
sequenceDiagram
|
|
49
|
-
participant L as Event loop
|
|
50
|
-
participant S as Store
|
|
51
|
-
participant Q as Actions Queue
|
|
52
|
-
actor O as State Observer
|
|
53
|
-
participant R as Reducer
|
|
54
|
-
participant E as Effect
|
|
55
|
-
L->>S: doProcessNextAction
|
|
56
|
-
S->>Q: get next available action
|
|
57
|
-
alt There is an action to process
|
|
58
|
-
loop For all registered reducers for the current sction
|
|
59
|
-
S->>R: update state
|
|
60
|
-
end
|
|
76
|
+
Actions are plain objects that describe what happened. They must have a `type` property:
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
```typescript
|
|
79
|
+
export interface Action {
|
|
80
|
+
type: string;
|
|
81
|
+
}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
83
|
+
// Example action class
|
|
84
|
+
export class IncrementCounterAction implements Action {
|
|
85
|
+
readonly type = 'INCREMENT_COUNTER';
|
|
86
|
+
|
|
87
|
+
constructor(public readonly amount: number = 1) {}
|
|
88
|
+
}
|
|
67
89
|
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
export class LoadUsersAction implements Action {
|
|
91
|
+
readonly type = 'LOAD_USERS';
|
|
92
|
+
}
|
|
70
93
|
```
|
|
71
94
|
|
|
72
|
-
|
|
95
|
+
### Reducers
|
|
96
|
+
|
|
97
|
+
Reducers are pure functions that take the current state and an action, then return a new state. They must be immutable:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { Reducer, State, Action } from 'ngssm-store';
|
|
101
|
+
import update from 'immutability-helper';
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class CounterReducer implements Reducer {
|
|
105
|
+
processedActions = ['INCREMENT_COUNTER', 'DECREMENT_COUNTER'];
|
|
106
|
+
|
|
107
|
+
updateState(state: State, action: Action): State {
|
|
108
|
+
switch (action.type) {
|
|
109
|
+
case 'INCREMENT_COUNTER':
|
|
110
|
+
return update(state, {
|
|
111
|
+
counter: {
|
|
112
|
+
value: { $apply: (v: number) => v + (action as IncrementCounterAction).amount }
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
case 'DECREMENT_COUNTER':
|
|
116
|
+
return update(state, {
|
|
117
|
+
counter: { value: { $set: state.counter.value - 1 } }
|
|
118
|
+
});
|
|
119
|
+
default:
|
|
120
|
+
return state;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Effects
|
|
127
|
+
|
|
128
|
+
Effects handle side effects like API calls, logging, and dispatching new actions. They don't modify state:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
import { Effect, ActionDispatcher, State, Action } from 'ngssm-store';
|
|
132
|
+
|
|
133
|
+
@Injectable()
|
|
134
|
+
export class UserEffect implements Effect {
|
|
135
|
+
processedActions = ['LOAD_USERS'];
|
|
136
|
+
|
|
137
|
+
private readonly userService = inject(UserService);
|
|
138
|
+
|
|
139
|
+
constructor(private injector: EnvironmentInjector) {}
|
|
140
|
+
|
|
141
|
+
processAction(dispatcher: ActionDispatcher, state: State, action: Action): void {
|
|
142
|
+
if (action.type === 'LOAD_USERS') {
|
|
143
|
+
this.userService.getUsers().subscribe((users) => {
|
|
144
|
+
dispatcher.dispatchAction(new SetUsersAction(users));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### State
|
|
152
|
+
|
|
153
|
+
The state is a plain object containing all application data. Structure it hierarchically:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
export interface State {
|
|
157
|
+
counter?: {
|
|
158
|
+
value: number;
|
|
159
|
+
};
|
|
160
|
+
users?: {
|
|
161
|
+
list: User[];
|
|
162
|
+
loading: boolean;
|
|
163
|
+
error?: Error;
|
|
164
|
+
};
|
|
165
|
+
settings?: {
|
|
166
|
+
theme: string;
|
|
167
|
+
language: string;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Usage
|
|
173
|
+
|
|
174
|
+
### Dispatching Actions
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
import { Store } from 'ngssm-store';
|
|
178
|
+
|
|
179
|
+
@Component({
|
|
180
|
+
selector: 'app-counter',
|
|
181
|
+
template: `
|
|
182
|
+
<p>Count: {{ count() }}</p>
|
|
183
|
+
<button (click)="increment()">Increment</button>
|
|
184
|
+
<button (click)="decrement()">Decrement</button>
|
|
185
|
+
`
|
|
186
|
+
})
|
|
187
|
+
export class CounterComponent {
|
|
188
|
+
private store = inject(Store);
|
|
189
|
+
|
|
190
|
+
count = createSignal((state) => state.counter?.value ?? 0);
|
|
191
|
+
|
|
192
|
+
increment() {
|
|
193
|
+
this.store.dispatchAction(new IncrementCounterAction(1));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
decrement() {
|
|
197
|
+
this.store.dispatchAction(new DecrementCounterAction());
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Accessing State with Signals
|
|
203
|
+
|
|
204
|
+
Use the signal-based API for reactive components:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
import { createSignal } from 'ngssm-store';
|
|
208
|
+
|
|
209
|
+
@Component({
|
|
210
|
+
selector: 'app-dashboard',
|
|
211
|
+
template: `
|
|
212
|
+
<div>
|
|
213
|
+
<p>Total Users: {{ userCount() }}</p>
|
|
214
|
+
<div *ngIf="loading()">Loading...</div>
|
|
215
|
+
<ul>
|
|
216
|
+
<li *ngFor="let user of users()">{{ user.name }}</li>
|
|
217
|
+
</ul>
|
|
218
|
+
</div>`
|
|
219
|
+
})
|
|
220
|
+
export class DashboardComponent {
|
|
221
|
+
private store = inject(Store);
|
|
222
|
+
|
|
223
|
+
users = createSignal((state) => state.users?.list ?? []);
|
|
224
|
+
loading = createSignal((state) => state.users?.loading ?? false);
|
|
225
|
+
userCount = createSignal((state) => (state.users?.list ?? []).length);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Accessing State with RxJS
|
|
230
|
+
|
|
231
|
+
For components that need RxJS integration:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
@Component({
|
|
235
|
+
selector: 'app-user-list',
|
|
236
|
+
template: `
|
|
237
|
+
<ul>
|
|
238
|
+
<li *ngFor="let user of users$ | async">{{ user.name }}</li>
|
|
239
|
+
</ul>`
|
|
240
|
+
})
|
|
241
|
+
export class UserListComponent {
|
|
242
|
+
private store = inject(Store);
|
|
243
|
+
|
|
244
|
+
users$ = this.store.state$.pipe(
|
|
245
|
+
map((state) => state.users?.list ?? [])
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Direct State Access
|
|
251
|
+
|
|
252
|
+
Access the current state directly:
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@Component(...)
|
|
256
|
+
export class MyComponent {
|
|
257
|
+
private store = inject(Store);
|
|
258
|
+
|
|
259
|
+
getCurrentState() {
|
|
260
|
+
const currentState = this.store.state(); // Signal access
|
|
261
|
+
// or
|
|
262
|
+
const viaObservable = this.store.state$; // Observable access
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### Tracking Processed Actions
|
|
268
|
+
|
|
269
|
+
Monitor which action was last processed:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
@Component(...)
|
|
273
|
+
export class MyComponent {
|
|
274
|
+
private store = inject(Store);
|
|
275
|
+
|
|
276
|
+
lastAction = createSignal((state) => this.store.processedAction().type);
|
|
277
|
+
|
|
278
|
+
// Or with RxJS
|
|
279
|
+
lastAction$ = this.store.processedAction$.pipe(map(a => a.type));
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Registering Reducers and Effects
|
|
73
284
|
|
|
74
|
-
|
|
75
|
-
- [rxjs](https://rxjs.dev/): the publish/subscribe pattern is implemented with **rxjs**,
|
|
76
|
-
- [immutability-helper](https://github.com/kolodny/immutability-helper): used by the reducers to update safely the state
|
|
285
|
+
### Single Reducer/Effect
|
|
77
286
|
|
|
78
|
-
|
|
287
|
+
```typescript
|
|
288
|
+
import { provideReducer, provideEffect } from 'ngssm-store';
|
|
289
|
+
|
|
290
|
+
bootstrapApplication(AppComponent, {
|
|
291
|
+
providers: [
|
|
292
|
+
provideNgssmStore(),
|
|
293
|
+
provideReducer(CounterReducer),
|
|
294
|
+
provideEffect(UserEffect)
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Multiple Reducers/Effects
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { provideReducers, provideEffects } from 'ngssm-store';
|
|
303
|
+
|
|
304
|
+
bootstrapApplication(AppComponent, {
|
|
305
|
+
providers: [
|
|
306
|
+
provideNgssmStore(),
|
|
307
|
+
provideReducers(
|
|
308
|
+
CounterReducer,
|
|
309
|
+
UserReducer,
|
|
310
|
+
SettingsReducer
|
|
311
|
+
),
|
|
312
|
+
provideEffects(
|
|
313
|
+
UserEffect,
|
|
314
|
+
NotificationEffect
|
|
315
|
+
)
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Effect Functions
|
|
321
|
+
|
|
322
|
+
Effect functions are the modern replacement for the `Effect` interface. They are executed in an injection context, allowing you to use Angular's `inject` function for dependency injection:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { provideEffectFunc } from 'ngssm-store';
|
|
326
|
+
import { inject } from '@angular/core';
|
|
327
|
+
|
|
328
|
+
bootstrapApplication(AppComponent, {
|
|
329
|
+
providers: [
|
|
330
|
+
provideNgssmStore(),
|
|
331
|
+
provideEffectFunc('LOAD_USERS', (state, action) => {
|
|
332
|
+
const userService = inject(UserService);
|
|
333
|
+
const dispatcher = inject(ACTION_DISPATCHER);
|
|
334
|
+
|
|
335
|
+
userService.getUsers().subscribe((users) => {
|
|
336
|
+
dispatcher.dispatchAction(new SetUsersAction(users));
|
|
337
|
+
});
|
|
338
|
+
})
|
|
339
|
+
]
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Effect functions should be preferred over the legacy `Effect` interface implementation as they are more concise and leverage Angular's dependency injection system.
|
|
344
|
+
|
|
345
|
+
## Feature States
|
|
346
|
+
|
|
347
|
+
Organize state by feature for better modularity:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { NgSsmFeatureState } from 'ngssm-store';
|
|
351
|
+
|
|
352
|
+
@NgSsmFeatureState({
|
|
353
|
+
featureStateKey: 'products',
|
|
354
|
+
initialState: {
|
|
355
|
+
list: [],
|
|
356
|
+
loading: false,
|
|
357
|
+
selectedId: null
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
export class ProductFeatureState {}
|
|
361
|
+
|
|
362
|
+
// Access in components
|
|
363
|
+
products = createSignal((state) => state.products?.list ?? []);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## State Initializers
|
|
367
|
+
|
|
368
|
+
Initialize state with data from external sources:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { StateInitializer } from 'ngssm-store';
|
|
372
|
+
|
|
373
|
+
@Injectable()
|
|
374
|
+
export class AppInitializer implements StateInitializer {
|
|
375
|
+
private configService = inject(ConfigService);
|
|
376
|
+
|
|
377
|
+
initializeState(state: State): State {
|
|
378
|
+
const config = this.configService.getConfig();
|
|
379
|
+
return update(state, {
|
|
380
|
+
settings: { $set: config }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Provide it
|
|
386
|
+
bootstrapApplication(AppComponent, {
|
|
387
|
+
providers: [
|
|
388
|
+
provideNgssmStore(),
|
|
389
|
+
{ provide: NGSSM_STATE_INITIALIZER, useClass: AppInitializer }
|
|
390
|
+
]
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Action Processing
|
|
395
|
+
|
|
396
|
+
### Sequential Processing
|
|
397
|
+
|
|
398
|
+
Actions are processed sequentially using an action queue:
|
|
399
|
+
|
|
400
|
+
1. Action dispatched
|
|
401
|
+
2. Added to queue
|
|
402
|
+
3. Store schedules processing (via setTimeout by default)
|
|
403
|
+
4. Reducers update state
|
|
404
|
+
5. State published to all subscribers
|
|
405
|
+
6. Effects process action (can dispatch new actions)
|
|
406
|
+
7. Next action processed
|
|
407
|
+
|
|
408
|
+
### Macro Tasks vs Micro Tasks
|
|
409
|
+
|
|
410
|
+
By default, the store uses `setTimeout` (macro-tasks) for action processing. You can switch to micro-tasks (Promises) if needed:
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
@Injectable()
|
|
414
|
+
export class Store {
|
|
415
|
+
// Set to false for micro-tasks (Promise.resolve())
|
|
416
|
+
public useMacroTasks = true;
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Best Practices
|
|
421
|
+
|
|
422
|
+
1. **Keep Actions Simple**: Actions should be serializable plain objects
|
|
423
|
+
2. **Pure Reducers**: Never mutate state directly; always use `immutability-helper`
|
|
424
|
+
3. **Avoid Side Effects in Reducers**: Use Effects for side effects
|
|
425
|
+
4. **Type Your State**: Define clear State interfaces
|
|
426
|
+
5. **Use Signals for Performance**: Prefer `createSignal` over RxJS when possible in new code
|
|
427
|
+
6. **Single Responsibility**: One reducer per feature/domain
|
|
428
|
+
7. **Logging**: Use the Logger service for debugging
|
|
429
|
+
8. **Action Names**: Use clear, descriptive action type names (FEATURE_ACTION_NAME pattern)
|
|
430
|
+
9. **Immutability**: Never modify state objects in reducers
|
|
431
|
+
10. **Error Handling**: Handle errors in effects and dispatch error actions
|
|
432
|
+
|
|
433
|
+
## API Reference
|
|
434
|
+
|
|
435
|
+
### Store Class
|
|
436
|
+
|
|
437
|
+
- `state(): Signal<State>` - Get current state as a Signal
|
|
438
|
+
- `state$: Observable<State>` - Get state as an Observable
|
|
439
|
+
- `processedAction(): Signal<Action>` - Get last processed action as a Signal
|
|
440
|
+
- `processedAction$: Observable<Action>` - Get last processed action as Observable
|
|
441
|
+
- `dispatchAction(action: Action): void` - Dispatch an action
|
|
442
|
+
- `dispatchActionType(actionType: string): void` - Dispatch by action type string
|
|
443
|
+
|
|
444
|
+
### Helper Functions
|
|
445
|
+
|
|
446
|
+
- `createSignal<T>(selector: (state: State) => T): Signal<T>` - Create a derived signal from state
|
|
447
|
+
- `provideNgssmStore()` - Initialize the store
|
|
448
|
+
- `provideReducer(reducer)` - Register a single reducer
|
|
449
|
+
- `provideReducers(...reducers)` - Register multiple reducers
|
|
450
|
+
- `provideEffect(effect)` - Register a single effect
|
|
451
|
+
- `provideEffects(...effects)` - Register multiple effects
|
|
452
|
+
- `provideEffectFunc(actionType, func)` - Register an effect function
|
|
453
|
+
|
|
454
|
+
### Interfaces
|
|
455
|
+
|
|
456
|
+
- `Action` - Action interface with `type` property
|
|
457
|
+
- `Reducer` - Reducer interface with `processedActions` and `updateState()`
|
|
458
|
+
- `Effect` - Effect interface with `processedActions` and `processAction()`
|
|
459
|
+
- `State` - Base state type (empty object by default)
|
|
460
|
+
- `StateInitializer` - Interface for initializing state
|
|
461
|
+
- `ActionDispatcher` - Interface for dispatching actions
|
|
462
|
+
|
|
463
|
+
## Debugging
|
|
464
|
+
|
|
465
|
+
### Logging
|
|
466
|
+
|
|
467
|
+
Enable logging to monitor state changes and action processing:
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { Logger } from 'ngssm-store';
|
|
471
|
+
|
|
472
|
+
constructor(private logger: Logger) {
|
|
473
|
+
this.logger.information('Component initialized');
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### DevTools Integration
|
|
478
|
+
|
|
479
|
+
The store can be integrated with Redux DevTools for advanced debugging (requires additional setup).
|
|
480
|
+
|
|
481
|
+
## Performance Considerations
|
|
482
|
+
|
|
483
|
+
- **Signals**: Prefer signals over observables for better performance in modern Angular
|
|
484
|
+
- **Selectors**: Use `createSignal` with specific selectors to minimize re-renders
|
|
485
|
+
- **Memoization**: Consider memoizing expensive selector functions
|
|
486
|
+
- **Action Batching**: Dispatch related actions together to reduce re-renders
|
|
487
|
+
|
|
488
|
+
## Dependencies
|
|
79
489
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
490
|
+
- **Angular Core**: Dependency injection, signals, lifecycle management
|
|
491
|
+
- **RxJS**: Reactive state and action streams
|
|
492
|
+
- **immutability-helper**: Safe state immutability patterns
|
|
493
|
+
- **TypeScript**: Full type safety
|
|
82
494
|
|
|
83
|
-
##
|
|
495
|
+
## License
|
|
84
496
|
|
|
85
|
-
|
|
497
|
+
MIT
|
|
@@ -10,9 +10,7 @@ import { TestBed } from '@angular/core/testing';
|
|
|
10
10
|
* Provides methods to apply a SetCachedItemAction, set the status, or set the value of a cached item.
|
|
11
11
|
*/
|
|
12
12
|
class NgssmCachedItemSetter {
|
|
13
|
-
|
|
14
|
-
this.store = inject(Store);
|
|
15
|
-
}
|
|
13
|
+
store = inject(Store);
|
|
16
14
|
/**
|
|
17
15
|
* Applies a SetCachedItemAction to the StoreMock, updating the cache state.
|
|
18
16
|
* @param action The SetCachedItemAction to apply.
|
|
@@ -95,10 +93,10 @@ class NgssmCachedItemSetter {
|
|
|
95
93
|
}
|
|
96
94
|
return this;
|
|
97
95
|
}
|
|
98
|
-
static
|
|
99
|
-
static
|
|
96
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NgssmCachedItemSetter, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
97
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NgssmCachedItemSetter });
|
|
100
98
|
}
|
|
101
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
99
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: NgssmCachedItemSetter, decorators: [{
|
|
102
100
|
type: Injectable
|
|
103
101
|
}] });
|
|
104
102
|
const ngssmCachedItemSetter = () => TestBed.inject(NgssmCachedItemSetter);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ngssm-store-caching-testing.mjs","sources":["../../../projects/ngssm-store/caching/testing/src/ngssm-cached-item-setter.ts","../../../projects/ngssm-store/caching/testing/src/provide-ngssm-caching-testing.ts","../../../projects/ngssm-store/caching/testing/ngssm-store-caching-testing.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\n\nimport { Store } from 'ngssm-store';\nimport { CachedItemStatus, selectNgssmCachedItem, SetCachedItemAction, updateNgssmCachingState } from 'ngssm-store/caching';\nimport { StoreMock } from 'ngssm-store/testing';\n\n/**\n * Utility service for setting and updating cached items in the StoreMock during tests.\n * Provides methods to apply a SetCachedItemAction, set the status, or set the value of a cached item.\n */\n@Injectable()\nexport class NgssmCachedItemSetter {\n public readonly store = inject(Store) as unknown as StoreMock;\n\n /**\n * Applies a SetCachedItemAction to the StoreMock, updating the cache state.\n * @param action The SetCachedItemAction to apply.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public apply<T = unknown>(action: SetCachedItemAction<T>): NgssmCachedItemSetter {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [action.cachedItemKey]: {\n $set: {\n status: action.status,\n item: action.cachedItem,\n error: action.error\n }\n }\n }\n });\n\n return this;\n }\n\n /**\n * Sets the status of a cached item in the StoreMock.\n * If the key does not exist in cache, it is created with an undefined value.\n *\n * @param cachedItemKey The key of the cached item.\n * @param status The new status to set.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public setCachedItemStatus(cachedItemKey: string, status: CachedItemStatus): NgssmCachedItemSetter {\n if (selectNgssmCachedItem(this.store.stateValue, cachedItemKey)) {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n status: { $set: status }\n }\n }\n });\n } else {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n $set: {\n status\n }\n }\n }\n });\n }\n\n return this;\n }\n\n /**\n * Sets the value of a cached item in the StoreMock.\n * If the key does not exist in cache, it is created with the status CachedItemStatus.notSet.\n *\n * @param cachedItemKey The key of the cached item.\n * @param value The value to set.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public setCachedItemValue<T>(cachedItemKey: string, value?: T): NgssmCachedItemSetter {\n if (selectNgssmCachedItem(this.store.stateValue, cachedItemKey)) {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n item: { $set: value }\n }\n }\n });\n } else {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n $set: {\n status: CachedItemStatus.notSet,\n item: value\n }\n }\n }\n });\n }\n\n return this;\n }\n}\n\nexport const ngssmCachedItemSetter = () => TestBed.inject(NgssmCachedItemSetter);\n","import { EnvironmentProviders, inject, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';\n\nimport { Logger, Store } from 'ngssm-store';\nimport { StoreMock } from 'ngssm-store/testing';\nimport { NgssmCachingStateSpecification } from 'ngssm-store/caching';\n\nimport { NgssmCachedItemSetter } from './ngssm-cached-item-setter';\n\n/**\n * App initializer that sets up the NgssmCaching state in the StoreMock for testing purposes.\n * Throws an error if StoreMock is not registered.\n */\nexport const ngssmCachingStateInitializer = () => {\n const logger = inject(Logger);\n logger.information('[ngssm-caching-testing] Initialization of state');\n const store = inject(Store);\n if (!(store instanceof StoreMock)) {\n throw new Error('StoreMock is not registered.');\n }\n\n store.stateValue = {\n ...store.stateValue,\n [NgssmCachingStateSpecification.featureStateKey]: NgssmCachingStateSpecification.initialState\n };\n};\n\n/**\n * Provides environment providers for NgssmCaching testing, including the state initializer.\n */\nexport const provideNgssmCachingTesting = (): EnvironmentProviders => {\n return makeEnvironmentProviders([provideAppInitializer(ngssmCachingStateInitializer), NgssmCachedItemSetter]);\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":[],"mappings":";;;;;;;AAOA;;;AAGG;MAEU,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"ngssm-store-caching-testing.mjs","sources":["../../../projects/ngssm-store/caching/testing/src/ngssm-cached-item-setter.ts","../../../projects/ngssm-store/caching/testing/src/provide-ngssm-caching-testing.ts","../../../projects/ngssm-store/caching/testing/ngssm-store-caching-testing.ts"],"sourcesContent":["import { inject, Injectable } from '@angular/core';\nimport { TestBed } from '@angular/core/testing';\n\nimport { Store } from 'ngssm-store';\nimport { CachedItemStatus, selectNgssmCachedItem, SetCachedItemAction, updateNgssmCachingState } from 'ngssm-store/caching';\nimport { StoreMock } from 'ngssm-store/testing';\n\n/**\n * Utility service for setting and updating cached items in the StoreMock during tests.\n * Provides methods to apply a SetCachedItemAction, set the status, or set the value of a cached item.\n */\n@Injectable()\nexport class NgssmCachedItemSetter {\n public readonly store = inject(Store) as unknown as StoreMock;\n\n /**\n * Applies a SetCachedItemAction to the StoreMock, updating the cache state.\n * @param action The SetCachedItemAction to apply.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public apply<T = unknown>(action: SetCachedItemAction<T>): NgssmCachedItemSetter {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [action.cachedItemKey]: {\n $set: {\n status: action.status,\n item: action.cachedItem,\n error: action.error\n }\n }\n }\n });\n\n return this;\n }\n\n /**\n * Sets the status of a cached item in the StoreMock.\n * If the key does not exist in cache, it is created with an undefined value.\n *\n * @param cachedItemKey The key of the cached item.\n * @param status The new status to set.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public setCachedItemStatus(cachedItemKey: string, status: CachedItemStatus): NgssmCachedItemSetter {\n if (selectNgssmCachedItem(this.store.stateValue, cachedItemKey)) {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n status: { $set: status }\n }\n }\n });\n } else {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n $set: {\n status\n }\n }\n }\n });\n }\n\n return this;\n }\n\n /**\n * Sets the value of a cached item in the StoreMock.\n * If the key does not exist in cache, it is created with the status CachedItemStatus.notSet.\n *\n * @param cachedItemKey The key of the cached item.\n * @param value The value to set.\n * @returns The NgssmCachedItemSetter instance for chaining.\n */\n public setCachedItemValue<T>(cachedItemKey: string, value?: T): NgssmCachedItemSetter {\n if (selectNgssmCachedItem(this.store.stateValue, cachedItemKey)) {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n item: { $set: value }\n }\n }\n });\n } else {\n this.store.stateValue = updateNgssmCachingState(this.store.stateValue, {\n caches: {\n [cachedItemKey]: {\n $set: {\n status: CachedItemStatus.notSet,\n item: value\n }\n }\n }\n });\n }\n\n return this;\n }\n}\n\nexport const ngssmCachedItemSetter = () => TestBed.inject(NgssmCachedItemSetter);\n","import { EnvironmentProviders, inject, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';\n\nimport { Logger, Store } from 'ngssm-store';\nimport { StoreMock } from 'ngssm-store/testing';\nimport { NgssmCachingStateSpecification } from 'ngssm-store/caching';\n\nimport { NgssmCachedItemSetter } from './ngssm-cached-item-setter';\n\n/**\n * App initializer that sets up the NgssmCaching state in the StoreMock for testing purposes.\n * Throws an error if StoreMock is not registered.\n */\nexport const ngssmCachingStateInitializer = () => {\n const logger = inject(Logger);\n logger.information('[ngssm-caching-testing] Initialization of state');\n const store = inject(Store);\n if (!(store instanceof StoreMock)) {\n throw new Error('StoreMock is not registered.');\n }\n\n store.stateValue = {\n ...store.stateValue,\n [NgssmCachingStateSpecification.featureStateKey]: NgssmCachingStateSpecification.initialState\n };\n};\n\n/**\n * Provides environment providers for NgssmCaching testing, including the state initializer.\n */\nexport const provideNgssmCachingTesting = (): EnvironmentProviders => {\n return makeEnvironmentProviders([provideAppInitializer(ngssmCachingStateInitializer), NgssmCachedItemSetter]);\n};\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public_api';\n"],"names":[],"mappings":"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"}
|
|
@@ -33,19 +33,24 @@ var CachedItemStatus;
|
|
|
33
33
|
* This action is dispatched to update the cache with a new or modified item, along with its status and any error information.
|
|
34
34
|
*/
|
|
35
35
|
class SetCachedItemAction {
|
|
36
|
+
cachedItemKey;
|
|
37
|
+
cachedItem;
|
|
38
|
+
status;
|
|
39
|
+
error;
|
|
40
|
+
type = NgssmCachingActionType.setCachedItem;
|
|
36
41
|
constructor(cachedItemKey, cachedItem, status = CachedItemStatus.set, error) {
|
|
37
42
|
this.cachedItemKey = cachedItemKey;
|
|
38
43
|
this.cachedItem = cachedItem;
|
|
39
44
|
this.status = status;
|
|
40
45
|
this.error = error;
|
|
41
|
-
this.type = NgssmCachingActionType.setCachedItem;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
class UnsetCachedItemAction {
|
|
50
|
+
cachedItemKey;
|
|
51
|
+
type = NgssmCachingActionType.unsetCachedItem;
|
|
46
52
|
constructor(cachedItemKey) {
|
|
47
53
|
this.cachedItemKey = cachedItemKey;
|
|
48
|
-
this.type = NgssmCachingActionType.unsetCachedItem;
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -54,10 +59,10 @@ const updateNgssmCachingState = (state, command) => update(state, {
|
|
|
54
59
|
[NgssmCachingStateSpecification.featureStateKey]: command
|
|
55
60
|
});
|
|
56
61
|
let NgssmCachingStateSpecification = class NgssmCachingStateSpecification {
|
|
57
|
-
static
|
|
58
|
-
static
|
|
62
|
+
static featureStateKey = 'ngssm-caching-state';
|
|
63
|
+
static initialState = {
|
|
59
64
|
caches: {}
|
|
60
|
-
};
|
|
65
|
+
};
|
|
61
66
|
};
|
|
62
67
|
NgssmCachingStateSpecification = __decorate([
|
|
63
68
|
NgSsmFeatureState({
|
|
@@ -69,9 +74,7 @@ NgssmCachingStateSpecification = __decorate([
|
|
|
69
74
|
const selectNgssmCachedItem = (state, key) => selectNgssmCachingState(state).caches[key];
|
|
70
75
|
|
|
71
76
|
class CachedItemReducer {
|
|
72
|
-
|
|
73
|
-
this.processedActions = [NgssmCachingActionType.setCachedItem, NgssmCachingActionType.unsetCachedItem];
|
|
74
|
-
}
|
|
77
|
+
processedActions = [NgssmCachingActionType.setCachedItem, NgssmCachingActionType.unsetCachedItem];
|
|
75
78
|
updateState(state, action) {
|
|
76
79
|
switch (action.type) {
|
|
77
80
|
case NgssmCachingActionType.setCachedItem: {
|
|
@@ -100,10 +103,10 @@ class CachedItemReducer {
|
|
|
100
103
|
}
|
|
101
104
|
return state;
|
|
102
105
|
}
|
|
103
|
-
static
|
|
104
|
-
static
|
|
106
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CachedItemReducer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
107
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CachedItemReducer });
|
|
105
108
|
}
|
|
106
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.
|
|
109
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: CachedItemReducer, decorators: [{
|
|
107
110
|
type: Injectable
|
|
108
111
|
}] });
|
|
109
112
|
|