@asaidimu/utils-store 2.0.4 → 2.1.1
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 +272 -202
- package/index.d.mts +1 -1
- package/index.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
@@ -1,11 +1,11 @@
|
|
1
|
-
# `@asaidimu/utils-store`
|
1
|
+
# `@asaidimu/utils-store`
|
2
2
|
|
3
|
-
**A Reactive Data Store**
|
3
|
+
**A Reactive Data Store for TypeScript Applications**
|
4
4
|
|
5
|
-
A comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware, transactional updates, deep observability, and an optional persistence layer.
|
5
|
+
A comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware, transactional updates, deep observability, and an optional persistence layer. It simplifies complex state interactions by promoting immutability, explicit updates, and a modular design.
|
6
6
|
|
7
7
|
[](https://www.npmjs.com/package/@asaidimu/utils-store)
|
8
|
-
[](LICENSE)
|
8
|
+
[](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
|
9
9
|
[](https://github.com/asaidimu/erp-utils/actions?query=workflow%3ACI)
|
10
10
|
|
11
11
|
---
|
@@ -19,7 +19,7 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
|
|
19
19
|
- [Persistence Integration](#persistence-integration)
|
20
20
|
- [Middleware System](#middleware-system)
|
21
21
|
- [Transaction Support](#transaction-support)
|
22
|
-
- [Store Observer](#store-observability)
|
22
|
+
- [Store Observer (Debugging & Observability)](#store-observer-debugging--observability)
|
23
23
|
- [Event System](#event-system)
|
24
24
|
- [Project Architecture](#project-architecture)
|
25
25
|
- [Development & Contributing](#development--contributing)
|
@@ -31,33 +31,32 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
|
|
31
31
|
|
32
32
|
`@asaidimu/utils-store` is a powerful and flexible state management solution designed for modern TypeScript applications. It provides a highly performant and observable way to manage your application's data, ensuring type safety and predictability across complex state interactions. Built on principles of immutability and explicit updates, it makes state changes easy to track, debug, and extend.
|
33
33
|
|
34
|
-
This library offers tools to handle your state with confidence, enabling features like atomic transactions, a pluggable middleware pipeline, and deep runtime introspection for unparalleled debugging capabilities. It emphasizes a component-based design internally, allowing for clear separation of concerns for state management, middleware processing, persistence, and observability.
|
34
|
+
This library offers robust tools to handle your state with confidence, enabling features like atomic transactions, a pluggable middleware pipeline, and deep runtime introspection for unparalleled debugging capabilities. It emphasizes a component-based design internally, allowing for clear separation of concerns for core state management, middleware processing, persistence, and observability.
|
35
35
|
|
36
36
|
### Key Features
|
37
37
|
|
38
|
-
* 📊 **Type-safe State Management**: Full TypeScript support for defining and interacting with your application state, leveraging `DeepPartial<T>` for precise, structural updates.
|
39
|
-
* 🔄 **Reactive Updates**: Subscribe to granular changes at specific paths within your state or listen for any change, ensuring efficient re-renders or side effects.
|
38
|
+
* 📊 **Type-safe State Management**: Full TypeScript support for defining and interacting with your application state, leveraging `DeepPartial<T>` for precise, structural updates while maintaining strong type inference.
|
39
|
+
* 🔄 **Reactive Updates & Granular Subscriptions**: Subscribe to granular changes at specific paths within your state or listen for any change, ensuring efficient re-renders or side effects. The internal `diff` algorithm optimizes notifications by identifying only truly changed paths.
|
40
40
|
* 🧠 **Composable Middleware System**:
|
41
|
-
* **Transform Middleware**:
|
42
|
-
* **Blocking Middleware**: Implement custom validation or
|
43
|
-
* 📦 **Atomic Transaction Support**: Group multiple state updates into a single, atomic operation. If any update within the transaction fails, the entire transaction is rolled back to the state before the transaction began, guaranteeing data integrity.
|
44
|
-
* 💾 **Optional Persistence Layer**: Seamlessly integrate with any `SimplePersistence<T>` implementation (e.g., for local storage, IndexedDB, or backend
|
45
|
-
* 🔍 **Deep Observer & Debugging**: An optional
|
46
|
-
* **Event History**:
|
47
|
-
* **State Snapshots**:
|
48
|
-
* **Time-Travel Debugging**:
|
49
|
-
* **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times,
|
50
|
-
* **Console Logging**:
|
51
|
-
* **Pre-built Debugging Middlewares**: Includes
|
52
|
-
*
|
53
|
-
*
|
54
|
-
* ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in order.
|
41
|
+
* **Transform Middleware**: Intercept, modify, normalize, or enrich state updates before they are applied. These can return a `DeepPartial<T>` to apply further changes or `void` for side effects.
|
42
|
+
* **Blocking Middleware**: Implement custom validation, authorization, or other conditional logic to prevent invalid state changes from occurring. If a blocking middleware returns `false` or throws an error, the update is immediately halted and rolled back.
|
43
|
+
* 📦 **Atomic Transaction Support**: Group multiple state updates into a single, atomic operation. If any update within the transaction fails or an error is thrown, the entire transaction is rolled back to the state before the transaction began, guaranteeing data integrity. Supports both synchronous and asynchronous operations.
|
44
|
+
* 💾 **Optional Persistence Layer**: Seamlessly integrate with any `SimplePersistence<T>` implementation (e.g., for local storage, IndexedDB, or backend synchronization) to load an initial state and save subsequent changes. The store emits a `persistence:ready` event and listens for external updates.
|
45
|
+
* 🔍 **Deep Observer & Debugging (`StoreObserver`)**: An optional but highly recommended class for unparalleled runtime introspection and debugging:
|
46
|
+
* **Comprehensive Event History**: Captures a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, `middleware:executed`, etc.).
|
47
|
+
* **State Snapshots**: Maintains a configurable history of your application's state over time, allowing for easy inspection of changes between updates and post-mortem analysis.
|
48
|
+
* **Time-Travel Debugging**: Leverage the recorded state history to `undo` and `redo` state changes, providing powerful capabilities for debugging complex asynchronous flows and state transitions.
|
49
|
+
* **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times, largest update size, and slow operation warnings to identify bottlenecks.
|
50
|
+
* **Configurable Console Logging**: Provides human-readable, color-coded logging of store events directly to the browser console for immediate feedback during development.
|
51
|
+
* **Pre-built Debugging Middlewares**: Includes helper methods to easily create a generic logging middleware and a validation middleware for immediate use.
|
52
|
+
* 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using the global `Symbol.for("delete")`or a custom marker.
|
53
|
+
* ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in a predictable, sequential order.
|
55
54
|
|
56
55
|
---
|
57
56
|
|
58
57
|
## Installation & Setup
|
59
58
|
|
60
|
-
Install
|
59
|
+
Install `@asaidimu/utils-store` using your preferred package manager. This library is designed for browser and Node.js environments, providing both CommonJS and ES Module exports.
|
61
60
|
|
62
61
|
```bash
|
63
62
|
# Using Bun
|
@@ -72,12 +71,12 @@ yarn add @asaidimu/utils-store
|
|
72
71
|
|
73
72
|
### Prerequisites
|
74
73
|
|
75
|
-
* Node.js (LTS version recommended)
|
76
|
-
* TypeScript (for type-
|
74
|
+
* **Node.js**: (LTS version recommended) for development and compilation.
|
75
|
+
* **TypeScript**: (v4.0+ recommended) for full type-safety during development.
|
77
76
|
|
78
77
|
### Verification
|
79
78
|
|
80
|
-
To verify the
|
79
|
+
To verify that the library is installed and working correctly, create a small TypeScript file (e.g., `verify.ts`) and run it.
|
81
80
|
|
82
81
|
```typescript
|
83
82
|
// verify.ts
|
@@ -101,24 +100,25 @@ store.subscribe("", (state) => {
|
|
101
100
|
});
|
102
101
|
|
103
102
|
console.log("Initial state:", store.get());
|
104
|
-
// Output: Initial state: { count: 0, message: "Hello" }
|
105
103
|
|
106
104
|
await store.set({ count: 1 });
|
107
|
-
// Output:
|
105
|
+
// Expected Output:
|
108
106
|
// Count changed to: 1
|
109
107
|
// Store updated to: {"count":1,"message":"Hello"}
|
110
108
|
|
111
109
|
await store.set({ message: "World" });
|
112
|
-
// Output:
|
110
|
+
// Expected Output:
|
113
111
|
// Store updated to: {"count":1,"message":"World"}
|
114
112
|
// (The 'count' listener won't be triggered as only 'message' changed)
|
115
113
|
|
116
114
|
console.log("Current state:", store.get());
|
117
|
-
// Output: Current state: { count: 1, message: "World" }
|
115
|
+
// Expected Output: Current state: { count: 1, message: "World" }
|
118
116
|
```
|
119
117
|
|
120
|
-
Run this file:
|
118
|
+
Run this file using `ts-node` or compile it first:
|
119
|
+
|
121
120
|
```bash
|
121
|
+
# Install ts-node if you don't have it: npm install -g ts-node
|
122
122
|
npx ts-node verify.ts
|
123
123
|
```
|
124
124
|
|
@@ -126,11 +126,11 @@ npx ts-node verify.ts
|
|
126
126
|
|
127
127
|
## Usage Documentation
|
128
128
|
|
129
|
-
This section provides practical examples of how to use `@asaidimu/utils-store` to manage your application state.
|
129
|
+
This section provides practical examples and detailed explanations of how to use `@asaidimu/utils-store` to manage your application state effectively.
|
130
130
|
|
131
131
|
### Basic Usage
|
132
132
|
|
133
|
-
|
133
|
+
Learn how to create a store, read state, and update state with partial objects or functions.
|
134
134
|
|
135
135
|
```typescript
|
136
136
|
import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
|
@@ -173,25 +173,40 @@ const initialState: AppState = {
|
|
173
173
|
const store = new ReactiveDataStore<AppState>(initialState);
|
174
174
|
|
175
175
|
// 3. Get the current state
|
176
|
+
// `store.get()` returns a reference to the internal state.
|
177
|
+
// Use `store.get(true)` to get a deep clone, ensuring immutability if you modify it directly.
|
176
178
|
const currentState = store.get();
|
177
179
|
console.log('Initial state:', currentState);
|
178
|
-
|
180
|
+
/* Output:
|
181
|
+
Initial state: {
|
182
|
+
user: { id: '123', name: 'Jane Doe', email: 'jane@example.com', isActive: true },
|
183
|
+
products: [ { id: 'p1', name: 'Laptop', price: 1200 }, { id: 'p2', name: 'Mouse', price: 25 } ],
|
184
|
+
settings: { theme: 'light', notificationsEnabled: true },
|
185
|
+
lastUpdated: <timestamp>
|
186
|
+
}
|
187
|
+
*/
|
179
188
|
|
180
|
-
// 4. Update the state using a partial object
|
181
|
-
// You can update deeply nested properties without affecting siblings
|
189
|
+
// 4. Update the state using a partial object (`DeepPartial<T>`)
|
190
|
+
// You can update deeply nested properties without affecting siblings.
|
182
191
|
await store.set({
|
183
192
|
user: {
|
184
|
-
name: 'Jane Smith',
|
185
|
-
isActive: false,
|
193
|
+
name: 'Jane Smith', // Changes user's name
|
194
|
+
isActive: false, // Changes user's active status
|
186
195
|
},
|
187
196
|
settings: {
|
188
|
-
theme: 'dark',
|
197
|
+
theme: 'dark', // Changes theme
|
189
198
|
},
|
190
199
|
});
|
191
200
|
|
192
201
|
console.log('State after partial update:', store.get());
|
193
|
-
|
194
|
-
|
202
|
+
/* Output:
|
203
|
+
State after partial update: {
|
204
|
+
user: { id: '123', name: 'Jane Smith', email: 'jane@example.com', isActive: false },
|
205
|
+
products: [ { id: 'p1', name: 'Laptop', price: 1200 }, { id: 'p2', name: 'Mouse', price: 25 } ],
|
206
|
+
settings: { theme: 'dark', notificationsEnabled: true },
|
207
|
+
lastUpdated: <timestamp> // Email and products remain unchanged.
|
208
|
+
}
|
209
|
+
*/
|
195
210
|
|
196
211
|
// 5. Update the state using a function (StateUpdater)
|
197
212
|
// This is useful when the new state depends on the current state.
|
@@ -200,14 +215,14 @@ await store.set((state) => ({
|
|
200
215
|
...state.products, // Keep existing products
|
201
216
|
{ id: 'p3', name: 'Keyboard', price: 75 }, // Add a new product
|
202
217
|
],
|
203
|
-
lastUpdated: Date.now(),
|
218
|
+
lastUpdated: Date.now(), // Update timestamp
|
204
219
|
}));
|
205
220
|
|
206
221
|
console.log('State after functional update, products count:', store.get().products.length);
|
207
222
|
// Output: State after functional update, products count: 3
|
208
223
|
|
209
224
|
// 6. Subscribing to state changes
|
210
|
-
// You can subscribe to the entire state or specific paths.
|
225
|
+
// You can subscribe to the entire state (path: '') or specific paths (e.g., 'user.name', 'settings.notificationsEnabled').
|
211
226
|
const unsubscribeUser = store.subscribe('user', (state) => {
|
212
227
|
console.log('User data changed:', state.user);
|
213
228
|
});
|
@@ -216,26 +231,28 @@ const unsubscribeNotifications = store.subscribe('settings.notificationsEnabled'
|
|
216
231
|
console.log('Notifications setting changed:', state.settings.notificationsEnabled);
|
217
232
|
});
|
218
233
|
|
219
|
-
// Subscribe to multiple paths
|
234
|
+
// Subscribe to multiple paths at once
|
220
235
|
const unsubscribeMulti = store.subscribe(['user.name', 'products'], (state) => {
|
221
236
|
console.log('User name or products changed:', state.user.name, state.products.length);
|
222
237
|
});
|
223
238
|
|
224
|
-
// Subscribe to any change in the store
|
239
|
+
// Subscribe to any change in the store (root listener)
|
225
240
|
const unsubscribeAll = store.subscribe('', (state) => {
|
226
241
|
console.log('Store updated (any path changed). Current products count:', state.products.length);
|
227
242
|
});
|
228
243
|
|
229
244
|
await store.set({ user: { email: 'jane.smith@example.com' } });
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
245
|
+
/* Output (order may vary slightly depending on async operations):
|
246
|
+
User data changed: { id: '123', name: 'Jane Smith', email: 'jane.smith@example.com', isActive: false }
|
247
|
+
User name or products changed: Jane Smith 3
|
248
|
+
Store updated (any path changed). Current products count: 3
|
249
|
+
*/
|
234
250
|
|
235
251
|
await store.set({ settings: { notificationsEnabled: false } });
|
236
|
-
|
237
|
-
|
238
|
-
|
252
|
+
/* Output:
|
253
|
+
Notifications setting changed: false
|
254
|
+
Store updated (any path changed). Current products count: 3
|
255
|
+
*/
|
239
256
|
|
240
257
|
// 7. Unsubscribe from changes
|
241
258
|
unsubscribeUser();
|
@@ -247,8 +264,8 @@ await store.set({ user: { isActive: true } });
|
|
247
264
|
// No console output from the above listeners after unsubscribing.
|
248
265
|
|
249
266
|
// 8. Deleting properties
|
250
|
-
// Use Symbol.for("delete") to remove a property from the state.
|
251
|
-
// Note: You might need a type cast for TypeScript if strict type checking is enabled.
|
267
|
+
// Use `Symbol.for("delete")` to explicitly remove a property from the state.
|
268
|
+
// Note: You might need a type cast (e.g., `as DeepPartial<string>`) for TypeScript if strict type checking is enabled.
|
252
269
|
const DELETE_ME = Symbol.for("delete");
|
253
270
|
await store.set({
|
254
271
|
user: {
|
@@ -261,16 +278,22 @@ console.log('User email after deletion:', store.get().user.email);
|
|
261
278
|
|
262
279
|
### Persistence Integration
|
263
280
|
|
264
|
-
The `ReactiveDataStore` can integrate with any persistence layer that implements the `SimplePersistence<T>` interface. This allows you to load an initial state and save subsequent changes. The store emits a `persistence:ready` event once the persistence layer has loaded any initial state.
|
281
|
+
The `ReactiveDataStore` can integrate with any persistence layer that implements the `SimplePersistence<T>` interface. This allows you to load an initial state and automatically save subsequent changes. The store emits a `persistence:ready` event once the persistence layer has loaded any initial state.
|
265
282
|
|
266
283
|
```typescript
|
267
284
|
import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
|
268
|
-
|
269
|
-
|
285
|
+
|
286
|
+
// Define the SimplePersistence interface (from `@asaidimu/utils-persistence` or your own)
|
287
|
+
interface SimplePersistence<T extends object> {
|
288
|
+
get(): Promise<T | null>;
|
289
|
+
set(instanceId: string, state: T): Promise<boolean>;
|
290
|
+
subscribe(instanceId: string, listener: (state: T) => void): () => void;
|
291
|
+
// An unsubscribe method for the persistence layer is recommended but not enforced by this interface
|
292
|
+
}
|
270
293
|
|
271
294
|
// Example: A simple in-memory persistence for demonstration
|
272
|
-
// In a real
|
273
|
-
class InMemoryPersistence<T> implements SimplePersistence<T> {
|
295
|
+
// In a real application, this would interact with localStorage, IndexedDB, a backend API, etc.
|
296
|
+
class InMemoryPersistence<T extends object> implements SimplePersistence<T> {
|
274
297
|
private data: T | null = null;
|
275
298
|
// Using a map to simulate different instances subscribing to common data
|
276
299
|
private subscribers: Map<string, (state: T) => void> = new Map();
|
@@ -281,16 +304,16 @@ class InMemoryPersistence<T> implements SimplePersistence<T> {
|
|
281
304
|
|
282
305
|
async get(): Promise<T | null> {
|
283
306
|
console.log('Persistence: Loading state...');
|
284
|
-
return this.data;
|
307
|
+
return this.data ? structuredClone(this.data) : null;
|
285
308
|
}
|
286
309
|
|
287
310
|
async set(instanceId: string, state: T): Promise<boolean> {
|
288
311
|
console.log(`Persistence: Saving state for instance ${instanceId}...`);
|
289
|
-
this.data = state;
|
290
|
-
// Simulate external change notification for other instances
|
312
|
+
this.data = structuredClone(state); // Store a clone
|
313
|
+
// Simulate external change notification for *other* instances
|
291
314
|
this.subscribers.forEach((callback, subId) => {
|
292
315
|
// Only notify other instances, not the one that just saved
|
293
|
-
if (subId !== instanceId) {
|
316
|
+
if (subId !== instanceId) {
|
294
317
|
callback(structuredClone(this.data!)); // Pass a clone to prevent mutation
|
295
318
|
}
|
296
319
|
});
|
@@ -317,34 +340,37 @@ const userConfigPersistence = new InMemoryPersistence<UserConfig>({ theme: 'dark
|
|
317
340
|
|
318
341
|
// Initialize the store with persistence
|
319
342
|
const store = new ReactiveDataStore<UserConfig>(
|
320
|
-
{ theme: 'light', fontSize: 16 }, // Initial state if no persisted data found or persistence is not used
|
343
|
+
{ theme: 'light', fontSize: 16 }, // Initial state if no persisted data found (or if persistence is not used)
|
321
344
|
userConfigPersistence // Pass your persistence implementation here
|
322
345
|
);
|
323
346
|
|
324
347
|
// Optionally, listen for persistence readiness (important for UIs that depend on loaded state)
|
325
348
|
const storeReadyPromise = new Promise<void>(resolve => {
|
326
|
-
store.onStoreEvent('persistence:ready', () => {
|
327
|
-
console.log(
|
349
|
+
store.onStoreEvent('persistence:ready', (data) => {
|
350
|
+
console.log(`Store is ready and persistence is initialized! Timestamp: ${new Date(data.timestamp).toLocaleTimeString()}`);
|
328
351
|
resolve();
|
329
352
|
});
|
330
353
|
});
|
331
354
|
|
332
355
|
console.log('Store initial state (before persistence loads):', store.get());
|
356
|
+
// Output: Store initial state (before persistence loads): { theme: 'light', fontSize: 16 } (initial state provided to constructor)
|
333
357
|
|
334
358
|
await storeReadyPromise; // Wait for persistence to load/initialize
|
335
359
|
|
336
360
|
// Now, store.get() will reflect the loaded state from persistence
|
337
|
-
console.log('Store state after persistence load:', store.get());
|
361
|
+
console.log('Store state after persistence load:', store.get());
|
338
362
|
// Output: Store state after persistence load: { theme: 'dark', fontSize: 18 } (from InMemoryPersistence)
|
339
363
|
|
340
364
|
// Now update the state, which will trigger persistence.set()
|
341
365
|
await store.set({ theme: 'light' });
|
342
366
|
console.log('Current theme:', store.get().theme);
|
343
367
|
// Output: Current theme: light
|
368
|
+
// Persistence: Saving state for instance <uuid>...
|
344
369
|
|
345
|
-
// Simulate an external change (e.g., another tab updating the state)
|
346
|
-
// Note:
|
347
|
-
|
370
|
+
// Simulate an external change (e.g., another tab or process updating the state)
|
371
|
+
// Note: The `instanceId` here should be different from the store's `store.id()`
|
372
|
+
// to simulate an external change and trigger the store's internal persistence subscription.
|
373
|
+
await userConfigPersistence.set('another-instance-id-123', { theme: 'system', fontSize: 20 });
|
348
374
|
// The store will automatically update its state and notify its listeners due to the internal subscription.
|
349
375
|
console.log('Current theme after external update:', store.get().theme);
|
350
376
|
// Output: Current theme after external update: system
|
@@ -352,11 +378,11 @@ console.log('Current theme after external update:', store.get().theme);
|
|
352
378
|
|
353
379
|
### Middleware System
|
354
380
|
|
355
|
-
Middleware functions allow you to intercept and modify state updates.
|
381
|
+
Middleware functions allow you to intercept and modify state updates or prevent them from proceeding.
|
356
382
|
|
357
383
|
#### Transform Middleware
|
358
384
|
|
359
|
-
These middlewares can transform the `DeepPartial` update or perform side effects. They receive the current state and the incoming partial update, and can return a new partial state to be merged.
|
385
|
+
These middlewares can transform the `DeepPartial` update or perform side effects. They receive the current state and the incoming partial update, and can return a new partial state to be merged. If they don't return anything (`void`), the update proceeds as is.
|
360
386
|
|
361
387
|
```typescript
|
362
388
|
import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
|
@@ -376,7 +402,7 @@ const store = new ReactiveDataStore<MyState>({
|
|
376
402
|
});
|
377
403
|
|
378
404
|
// Middleware 1: Logger
|
379
|
-
// Logs the incoming update before it's processed. Does not return anything (void).
|
405
|
+
// Logs the incoming update before it's processed. Does not return anything (void), so it doesn't modify the update.
|
380
406
|
store.use({
|
381
407
|
name: 'LoggerMiddleware',
|
382
408
|
action: (state, update) => {
|
@@ -385,11 +411,10 @@ store.use({
|
|
385
411
|
});
|
386
412
|
|
387
413
|
// Middleware 2: Timestamp and Action Tracker
|
388
|
-
// Modifies the update to add a timestamp and track the last action. Returns a partial state.
|
414
|
+
// Modifies the update to add a timestamp and track the last action. Returns a partial state that gets merged.
|
389
415
|
store.use({
|
390
416
|
name: 'TimestampActionMiddleware',
|
391
417
|
action: (state, update) => {
|
392
|
-
// This middleware transforms the update by adding `lastAction` and a log entry
|
393
418
|
const actionDescription = JSON.stringify(update);
|
394
419
|
return {
|
395
420
|
lastAction: `Updated at ${new Date().toLocaleTimeString()} with ${actionDescription}`,
|
@@ -399,7 +424,7 @@ store.use({
|
|
399
424
|
});
|
400
425
|
|
401
426
|
// Middleware 3: Version Incrementor
|
402
|
-
// Increments a version counter for every update.
|
427
|
+
// Increments a version counter for every successful update.
|
403
428
|
store.use({
|
404
429
|
name: 'VersionIncrementMiddleware',
|
405
430
|
action: (state) => {
|
@@ -408,7 +433,8 @@ store.use({
|
|
408
433
|
});
|
409
434
|
|
410
435
|
// Middleware 4: Counter Incrementor
|
411
|
-
//
|
436
|
+
// This middleware intercepts updates to 'counter' and increments it by the value provided,
|
437
|
+
// instead of setting it directly.
|
412
438
|
store.use({
|
413
439
|
name: 'CounterIncrementMiddleware',
|
414
440
|
action: (state, update) => {
|
@@ -416,13 +442,15 @@ store.use({
|
|
416
442
|
if (typeof update.counter === 'number') {
|
417
443
|
return { counter: state.counter + update.counter };
|
418
444
|
}
|
419
|
-
// Return original update or void if no transformation needed
|
445
|
+
// Return the original update or void if no transformation is needed for other paths
|
420
446
|
return update;
|
421
447
|
},
|
422
448
|
});
|
423
449
|
|
424
450
|
await store.set({ counter: 5 }); // Will increment counter by 5, not set to 5
|
425
|
-
|
451
|
+
/* Expected console output from LoggerMiddleware:
|
452
|
+
Middleware: Incoming update: { counter: 5 }
|
453
|
+
*/
|
426
454
|
console.log('State after counter set:', store.get());
|
427
455
|
/* Output will show:
|
428
456
|
counter: 5 (initial) + 5 (update) = 10,
|
@@ -432,7 +460,9 @@ console.log('State after counter set:', store.get());
|
|
432
460
|
*/
|
433
461
|
|
434
462
|
await store.set({ lastAction: 'Manual update from outside middleware' });
|
435
|
-
|
463
|
+
/* Expected console output from LoggerMiddleware:
|
464
|
+
Middleware: Incoming update: { lastAction: 'Manual update from outside middleware' }
|
465
|
+
*/
|
436
466
|
console.log('State after manual action:', store.get());
|
437
467
|
/* Output will show:
|
438
468
|
lastAction will be overwritten by TimestampActionMiddleware logic,
|
@@ -444,13 +474,13 @@ console.log('State after manual action:', store.get());
|
|
444
474
|
const temporaryLoggerId = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) });
|
445
475
|
await store.set({ counter: 1 });
|
446
476
|
// Output: Temporary logger saw: { counter: 1 }
|
447
|
-
store.unuse(temporaryLoggerId);
|
477
|
+
store.unuse(temporaryLoggerId); // Remove the temporary logger
|
448
478
|
await store.set({ counter: 1 }); // TemporaryLogger will not be called now
|
449
479
|
```
|
450
480
|
|
451
481
|
#### Blocking Middleware
|
452
482
|
|
453
|
-
These middlewares can prevent an update from proceeding if certain conditions are not met. They return a boolean: `true` to allow, `false` to block. If a blocking middleware throws an error, the update is also blocked.
|
483
|
+
These middlewares can prevent an update from proceeding if certain conditions are not met. They return a boolean: `true` to allow, `false` to block. If a blocking middleware throws an error, the update is also blocked. When an update is blocked, the `update:complete` event will contain `blocked: true` and an `error` property.
|
454
484
|
|
455
485
|
```typescript
|
456
486
|
import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
|
@@ -487,17 +517,18 @@ store.use({
|
|
487
517
|
block: true,
|
488
518
|
name: 'AdminRestrictionMiddleware',
|
489
519
|
action: (state, update) => {
|
490
|
-
//
|
491
|
-
if (update.isAdmin === true
|
492
|
-
|
493
|
-
|
494
|
-
}
|
495
|
-
// Cannot become admin if not verified
|
496
|
-
if (update.isAdmin === true && !state.isVerified) {
|
497
|
-
console.warn('Blocking update: User must be verified to become admin.');
|
520
|
+
// If attempting to become admin, check conditions
|
521
|
+
if (update.isAdmin === true) {
|
522
|
+
if (state.age < 21) {
|
523
|
+
console.warn('Blocking update: User must be 21+ to become admin.');
|
498
524
|
return false;
|
525
|
+
}
|
526
|
+
if (!state.isVerified) {
|
527
|
+
console.warn('Blocking update: User must be verified to become admin.');
|
528
|
+
return false;
|
529
|
+
}
|
499
530
|
}
|
500
|
-
return true;
|
531
|
+
return true; // Allow the update
|
501
532
|
},
|
502
533
|
});
|
503
534
|
|
@@ -505,15 +536,15 @@ store.use({
|
|
505
536
|
await store.set({ age: 25 });
|
506
537
|
console.log('User age after valid update:', store.get().age); // Output: 25
|
507
538
|
|
508
|
-
// Attempt to set an invalid age
|
539
|
+
// Attempt to set an invalid age (will be blocked)
|
509
540
|
await store.set({ age: 16 });
|
510
541
|
console.log('User age after invalid update attempt (should be 25):', store.get().age); // Output: 25
|
511
542
|
|
512
|
-
// Attempt to make user admin while not verified (
|
543
|
+
// Attempt to make user admin while not verified (will be blocked)
|
513
544
|
await store.set({ isAdmin: true });
|
514
545
|
console.log('User admin status after failed attempt (should be false):', store.get().isAdmin); // Output: false
|
515
546
|
|
516
|
-
// Verify user, then attempt to make admin again (
|
547
|
+
// Verify user, then attempt to make admin again (will still be blocked due to age)
|
517
548
|
await store.set({ isVerified: true });
|
518
549
|
await store.set({ age: 20 });
|
519
550
|
await store.set({ isAdmin: true });
|
@@ -527,7 +558,7 @@ console.log('User admin status after successful attempt (should be true):', stor
|
|
527
558
|
|
528
559
|
### Transaction Support
|
529
560
|
|
530
|
-
Use `store.transaction()` to group multiple state updates into a single atomic operation. If an error occurs during the transaction, all changes made within that transaction will be rolled back.
|
561
|
+
Use `store.transaction()` to group multiple state updates into a single atomic operation. If an error occurs during the transaction (either thrown by your `operation` function or by an internal `store.set` call), all changes made within that transaction will be rolled back to the state *before* the transaction began. This guarantees data integrity for complex, multi-step operations.
|
531
562
|
|
532
563
|
```typescript
|
533
564
|
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
@@ -548,7 +579,8 @@ async function transferFunds(
|
|
548
579
|
toStore: ReactiveDataStore<BankAccount>,
|
549
580
|
amount: number,
|
550
581
|
) {
|
551
|
-
// All operations inside this transaction will be atomic
|
582
|
+
// All operations inside this transaction will be atomic.
|
583
|
+
// If `operation()` throws an error, the state will revert.
|
552
584
|
await fromStore.transaction(async () => {
|
553
585
|
console.log(`Starting transfer of ${amount}. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
|
554
586
|
|
@@ -565,6 +597,7 @@ async function transferFunds(
|
|
565
597
|
});
|
566
598
|
|
567
599
|
// Simulate a network delay or another async operation that might fail
|
600
|
+
// If an error happens here, the state will still roll back.
|
568
601
|
await new Promise(resolve => setTimeout(resolve, 50));
|
569
602
|
|
570
603
|
// Add to receiver
|
@@ -590,29 +623,29 @@ try {
|
|
590
623
|
console.log('Account A:', accountA.get()); // Expected: balance 400, transactions: ['Debited 100 from Account A']
|
591
624
|
console.log('Account B:', accountB.get()); // Expected: balance 300, transactions: ['Credited 100 to Account B']
|
592
625
|
} catch (error: any) {
|
593
|
-
console.error('Transfer 1 failed:', error.message);
|
626
|
+
console.error('Transfer 1 failed unexpectedly:', error.message);
|
594
627
|
}
|
595
628
|
|
596
629
|
// --- Scenario 2: Failed transfer (insufficient funds) ---
|
597
630
|
console.log('\n--- Attempting failed transfer (1000) ---');
|
598
631
|
try {
|
599
|
-
// Account A
|
600
|
-
await transferFunds(accountA, accountB, 1000);
|
632
|
+
// Account A now has 400, so this should fail
|
633
|
+
await transferFunds(accountA, accountB, 1000);
|
601
634
|
} catch (error: any) {
|
602
635
|
console.error('Transfer 2 failed as expected:', error.message);
|
603
636
|
} finally {
|
604
637
|
console.log('Transfer 2 attempt, state after rollback:');
|
605
638
|
// State should be rolled back to its state *before* the transaction attempt
|
606
|
-
console.log('Account A:', accountA.get());
|
607
|
-
// Expected: balance 400 (rolled back)
|
608
|
-
console.log('Account B:', accountB.get());
|
609
|
-
// Expected: balance 300 (rolled back)
|
639
|
+
console.log('Account A:', accountA.get());
|
640
|
+
// Expected: balance 400 (rolled back to state before this transaction)
|
641
|
+
console.log('Account B:', accountB.get());
|
642
|
+
// Expected: balance 300 (rolled back to state before this transaction)
|
610
643
|
}
|
611
644
|
```
|
612
645
|
|
613
|
-
### Store Observer
|
646
|
+
### Store Observer (Debugging & Observability)
|
614
647
|
|
615
|
-
The `StoreObserver` class provides advanced debugging and monitoring capabilities for any `ReactiveDataStore` instance. It allows you to inspect event history, state changes, and time-travel through your application's state.
|
648
|
+
The `StoreObserver` class provides advanced debugging and monitoring capabilities for any `ReactiveDataStore` instance. It allows you to inspect event history, state changes, and even time-travel through your application's state. It's an invaluable tool for understanding complex state flows.
|
616
649
|
|
617
650
|
```typescript
|
618
651
|
import { ReactiveDataStore, StoreObserver, type StoreEvent, type DeepPartial } from '@asaidimu/utils-store';
|
@@ -632,23 +665,24 @@ const store = new ReactiveDataStore<DebuggableState>({
|
|
632
665
|
});
|
633
666
|
|
634
667
|
// Initialize observability for the store
|
668
|
+
// Options allow granular control over what is tracked and logged.
|
635
669
|
const observer = new StoreObserver(store, {
|
636
|
-
maxEvents: 50, // Keep up to 50 events in history
|
670
|
+
maxEvents: 50, // Keep up to 50 internal store events in history
|
637
671
|
maxStateHistory: 5, // Keep up to 5 state snapshots for time-travel
|
638
|
-
enableConsoleLogging: true, // Log events to console for immediate feedback
|
672
|
+
enableConsoleLogging: true, // Log events to browser console for immediate feedback
|
639
673
|
logEvents: {
|
640
|
-
updates: true, // Log all update lifecycle events
|
641
|
-
middleware: true, // Log middleware start/complete/error
|
642
|
-
transactions: true, // Log transaction start/complete/error
|
674
|
+
updates: true, // Log all update lifecycle events (start/complete)
|
675
|
+
middleware: true, // Log middleware start/complete/error/blocked/executed events
|
676
|
+
transactions: true, // Log transaction start/complete/error events
|
643
677
|
},
|
644
678
|
performanceThresholds: {
|
645
|
-
updateTime: 50, // Warn if an update takes > 50ms
|
679
|
+
updateTime: 50, // Warn in console if an update takes > 50ms
|
646
680
|
middlewareTime: 20, // Warn if a middleware takes > 20ms
|
647
681
|
},
|
648
682
|
});
|
649
683
|
|
650
|
-
// Add a simple middleware to demonstrate middleware logging
|
651
|
-
store.use({ name: '
|
684
|
+
// Add a simple middleware to demonstrate middleware logging and metrics update
|
685
|
+
store.use({ name: 'UpdateMetricsMiddleware', action: async (state, update) => {
|
652
686
|
await new Promise(resolve => setTimeout(resolve, 10)); // Simulate work
|
653
687
|
return { metrics: { updates: state.metrics.updates + 1 } };
|
654
688
|
}});
|
@@ -658,7 +692,7 @@ await store.set({ user: { status: 'offline' } });
|
|
658
692
|
await store.set({ messages: ['Hello World!'] });
|
659
693
|
await store.set({ settings: { debugMode: false } });
|
660
694
|
|
661
|
-
// Simulate a slow update
|
695
|
+
// Simulate a slow update to trigger performance warning
|
662
696
|
await new Promise(resolve => setTimeout(resolve, 60)); // Artificially delay
|
663
697
|
await store.set({ messages: ['Another message', 'And another'] });
|
664
698
|
// This last set will cause a console warning for "Slow update detected" if enableConsoleLogging is true.
|
@@ -672,27 +706,27 @@ events.slice(0, 5).forEach(event => console.log(`Type: ${event.type}, Data: ${JS
|
|
672
706
|
// 2. Get State History
|
673
707
|
console.log('\n--- State History (Most Recent First) ---');
|
674
708
|
const stateSnapshots = observer.getStateHistory();
|
675
|
-
stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}
|
709
|
+
stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}: Messages: ${snapshot.messages.join(', ')}, User Status: ${snapshot.user.status}`));
|
676
710
|
|
677
|
-
// 3. Get Recent Changes
|
711
|
+
// 3. Get Recent Changes (Diffs)
|
678
712
|
console.log('\n--- Recent State Changes (Diffs) ---');
|
679
713
|
const recentChanges = observer.getRecentChanges(3); // Show diffs for last 3 changes
|
680
714
|
recentChanges.forEach((change, index) => {
|
681
715
|
console.log(`\nChange #${index}:`);
|
682
716
|
console.log(` Timestamp: ${new Date(change.timestamp).toLocaleTimeString()}`);
|
683
717
|
console.log(` Changed Paths: ${change.changedPaths.join(', ')}`);
|
684
|
-
console.log(` From:`, change.from);
|
685
|
-
console.log(` To:`, change.to);
|
718
|
+
console.log(` From (partial):`, change.from); // Only changed parts of the state
|
719
|
+
console.log(` To (partial):`, change.to); // Only changed parts of the state
|
686
720
|
});
|
687
721
|
|
688
722
|
// 4. Time-Travel Debugging
|
689
723
|
console.log('\n--- Time-Travel ---');
|
690
724
|
const timeTravel = observer.createTimeTravel();
|
691
725
|
|
692
|
-
// Add more states to the history for time-travel
|
693
|
-
await store.set({ user: { status: 'online' } }); // State A
|
694
|
-
await store.set({ messages: ['First message'] }); // State B
|
695
|
-
await store.set({ messages: ['Second message'] }); // State C
|
726
|
+
// Add more states to the history for time-travel demonstration
|
727
|
+
await store.set({ user: { status: 'online' } }); // Current State (A)
|
728
|
+
await store.set({ messages: ['First message'] }); // Previous State (B)
|
729
|
+
await store.set({ messages: ['Second message'] }); // Previous State (C)
|
696
730
|
|
697
731
|
console.log('Current state (latest):', store.get().messages); // Output: ['Second message']
|
698
732
|
|
@@ -703,7 +737,7 @@ if (timeTravel.canUndo()) {
|
|
703
737
|
|
704
738
|
if (timeTravel.canUndo()) {
|
705
739
|
await timeTravel.undo(); // Go back to State A
|
706
|
-
console.log('After undo 2:', store.get().messages); // Output: [] (
|
740
|
+
console.log('After undo 2:', store.get().messages); // Output: [] (the state before 'First message' was added)
|
707
741
|
}
|
708
742
|
|
709
743
|
if (timeTravel.canRedo()) {
|
@@ -711,18 +745,35 @@ if (timeTravel.canRedo()) {
|
|
711
745
|
console.log('After redo 1:', store.get().messages); // Output: ['First message']
|
712
746
|
}
|
713
747
|
|
714
|
-
console.log('Time-Travel history length:', timeTravel.getHistoryLength()); //
|
748
|
+
console.log('Time-Travel history length:', timeTravel.getHistoryLength()); // Reflects `maxStateHistory` + initial state
|
715
749
|
|
716
750
|
// 5. Custom Debugging Middleware (provided by StoreObserver for convenience)
|
751
|
+
// Example: A logging middleware that logs every update
|
717
752
|
const loggingMiddleware = observer.createLoggingMiddleware({
|
718
|
-
logLevel: 'info', // 'debug', 'info', 'warn'
|
719
|
-
logUpdates: true,
|
753
|
+
logLevel: 'info', // Can be 'debug', 'info', 'warn'
|
754
|
+
logUpdates: true, // Whether to log the update payload itself
|
720
755
|
});
|
721
756
|
const loggingMiddlewareId = store.use({ name: 'DebugLogging', action: loggingMiddleware });
|
722
757
|
|
723
758
|
await store.set({ user: { name: 'New User Via Debug Logger' } }); // This update will be logged by the created middleware.
|
724
759
|
// Expected console output: "State Update: { user: { name: 'New User Via Debug Logger' } }"
|
725
760
|
|
761
|
+
// Example: A validation middleware (blocking)
|
762
|
+
const validationMiddleware = observer.createValidationMiddleware((state, update) => {
|
763
|
+
if (update.messages && update.messages.length > 5) {
|
764
|
+
return { valid: false, reason: "Too many messages!" };
|
765
|
+
}
|
766
|
+
return true;
|
767
|
+
});
|
768
|
+
const validationMiddlewareId = store.use({ name: 'DebugValidation', block: true, action: validationMiddleware });
|
769
|
+
|
770
|
+
try {
|
771
|
+
await store.set({ messages: ['m1','m2','m3','m4','m5','m6'] }); // This will be blocked
|
772
|
+
} catch (e) {
|
773
|
+
console.warn(`Caught expected error from validation middleware: ${e.message}`);
|
774
|
+
}
|
775
|
+
console.log('Current messages after failed validation:', store.get().messages); // Should be the state before this set.
|
776
|
+
|
726
777
|
// 6. Clear history
|
727
778
|
observer.clearHistory();
|
728
779
|
console.log('\nHistory cleared. Events:', observer.getEventHistory().length, 'State snapshots:', observer.getStateHistory().length);
|
@@ -737,10 +788,10 @@ await store.set({ messages: ['Final message after disconnect'] });
|
|
737
788
|
|
738
789
|
### Event System
|
739
790
|
|
740
|
-
The store emits various events during its lifecycle, allowing for advanced monitoring, logging, and integration with external systems. You can subscribe to these events using `store.onStoreEvent()`.
|
791
|
+
The store emits various events during its lifecycle, allowing for advanced monitoring, logging, and integration with external systems. You can subscribe to these events using `store.onStoreEvent(eventName, listener)`. The `StoreObserver` leverages this event system internally to provide its rich debugging capabilities.
|
741
792
|
|
742
793
|
```typescript
|
743
|
-
import { ReactiveDataStore, StoreEvent } from '@asaidimu/utils-store';
|
794
|
+
import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
|
744
795
|
|
745
796
|
interface MyState {
|
746
797
|
value: number;
|
@@ -749,12 +800,12 @@ interface MyState {
|
|
749
800
|
|
750
801
|
const store = new ReactiveDataStore<MyState>({ value: 0, status: 'idle' });
|
751
802
|
|
752
|
-
// Subscribe to 'update:start' event
|
803
|
+
// Subscribe to 'update:start' event - triggered before an update begins processing.
|
753
804
|
store.onStoreEvent('update:start', (data) => {
|
754
805
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⚡ Update started.`);
|
755
806
|
});
|
756
807
|
|
757
|
-
// Subscribe to 'update:complete' event
|
808
|
+
// Subscribe to 'update:complete' event - triggered after an update is fully applied or blocked.
|
758
809
|
store.onStoreEvent('update:complete', (data) => {
|
759
810
|
if (data.blocked) {
|
760
811
|
console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] ✋ Update blocked. Error:`, data.error?.message);
|
@@ -763,7 +814,7 @@ store.onStoreEvent('update:complete', (data) => {
|
|
763
814
|
}
|
764
815
|
});
|
765
816
|
|
766
|
-
// Subscribe to middleware events
|
817
|
+
// Subscribe to middleware lifecycle events
|
767
818
|
store.onStoreEvent('middleware:start', (data) => {
|
768
819
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ▶ Middleware "${data.name}" (${data.type}) started.`);
|
769
820
|
});
|
@@ -781,11 +832,11 @@ store.onStoreEvent('middleware:blocked', (data) => {
|
|
781
832
|
});
|
782
833
|
|
783
834
|
store.onStoreEvent('middleware:executed', (data) => {
|
784
|
-
// This event captures detailed execution info for all middlewares
|
835
|
+
// This event captures detailed execution info for all middlewares, useful for aggregate metrics.
|
785
836
|
console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 📊 Middleware executed: "${data.name}" - Duration: ${data.duration?.toFixed(2)}ms, Blocked: ${data.blocked}`);
|
786
837
|
});
|
787
838
|
|
788
|
-
// Subscribe to transaction events
|
839
|
+
// Subscribe to transaction lifecycle events
|
789
840
|
store.onStoreEvent('transaction:start', (data) => {
|
790
841
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction started.`);
|
791
842
|
});
|
@@ -804,7 +855,7 @@ store.onStoreEvent('persistence:ready', (data) => {
|
|
804
855
|
});
|
805
856
|
|
806
857
|
|
807
|
-
// Add a transform middleware to demonstrate
|
858
|
+
// Add a transform middleware to demonstrate `middleware:start/complete/executed`
|
808
859
|
store.use({
|
809
860
|
name: 'ValueIncrementMiddleware',
|
810
861
|
action: (state, update) => {
|
@@ -812,7 +863,7 @@ store.use({
|
|
812
863
|
},
|
813
864
|
});
|
814
865
|
|
815
|
-
// Add a blocking middleware to demonstrate
|
866
|
+
// Add a blocking middleware to demonstrate `middleware:error` and `update:complete` (blocked)
|
816
867
|
store.use({
|
817
868
|
name: 'StatusValidationMiddleware',
|
818
869
|
block: true,
|
@@ -824,9 +875,9 @@ store.use({
|
|
824
875
|
},
|
825
876
|
});
|
826
877
|
|
827
|
-
// Perform operations
|
878
|
+
// Perform operations to trigger events
|
828
879
|
console.log('\n--- Perform Initial Update ---');
|
829
|
-
await store.set({ value: 5, status: 'active' }); // Will increment value by 5
|
880
|
+
await store.set({ value: 5, status: 'active' }); // Will increment value by 5 (due to middleware)
|
830
881
|
|
831
882
|
console.log('\n--- Perform Transactional Update (Success) ---');
|
832
883
|
await store.transaction(async () => {
|
@@ -836,7 +887,7 @@ await store.transaction(async () => {
|
|
836
887
|
|
837
888
|
console.log('\n--- Perform Update (Blocked by Middleware) ---');
|
838
889
|
try {
|
839
|
-
await store.set({ status: 'error' }); // This should be blocked by StatusValidationMiddleware (value is 8, which is < 10)
|
890
|
+
await store.set({ status: 'error' }); // This should be blocked by StatusValidationMiddleware (current value is 8, which is < 10)
|
840
891
|
} catch (e: any) {
|
841
892
|
console.log(`Caught expected error: ${e.message}`);
|
842
893
|
}
|
@@ -848,55 +899,71 @@ console.log('Final value:', store.get().value, 'Final status:', store.get().stat
|
|
848
899
|
|
849
900
|
## Project Architecture
|
850
901
|
|
851
|
-
The `@asaidimu/utils-store` library is built with a modular, component-based architecture to promote maintainability, testability, and extensibility.
|
902
|
+
The `@asaidimu/utils-store` library is built with a modular, component-based architecture to promote maintainability, testability, and extensibility. Each core concern is encapsulated within its own class, with `ReactiveDataStore` acting as the central coordinator.
|
903
|
+
|
904
|
+
```
|
905
|
+
src/store/
|
906
|
+
├── index.ts # Main entry point (exports all public APIs)
|
907
|
+
├── types.ts # TypeScript interfaces and types for the store
|
908
|
+
├── store.ts # `ReactiveDataStore` - The main orchestrator
|
909
|
+
├── state.ts # `CoreStateManager` - Manages the immutable state and diffing
|
910
|
+
├── merge.ts # `createMerge` - Deep merging utility with deletion support
|
911
|
+
├── diff.ts # `createDiff`, `createDerivePaths` - Change detection utilities
|
912
|
+
├── middleware.ts # `MiddlewareEngine` - Manages and executes middleware pipeline
|
913
|
+
├── transactions.ts # `TransactionManager` - Handles atomic state transactions
|
914
|
+
├── persistence.ts # `PersistenceHandler` - Integrates with `SimplePersistence` layer
|
915
|
+
├── metrics.ts # `MetricsCollector` - Gathers runtime performance metrics
|
916
|
+
├── observability.ts # `StoreObserver` - Debugging and introspection utilities
|
917
|
+
└── ... # (Other test files like *.test.ts, internal helpers)
|
918
|
+
```
|
852
919
|
|
853
920
|
### Core Components
|
854
921
|
|
855
|
-
* **`ReactiveDataStore<T>` (store.ts)**: The
|
856
|
-
* **`CoreStateManager<T>` (state.ts)**:
|
857
|
-
* **`MiddlewareEngine<T>` (middleware.ts)**: Manages the registration and execution of
|
858
|
-
* **`PersistenceHandler<T>` (persistence.ts)**:
|
859
|
-
* **`TransactionManager<T>` (transactions.ts)**: Provides atomic state operations. It creates a snapshot of the state before an `operation` and, if the operation fails,
|
860
|
-
* **`MetricsCollector` (metrics.ts)**: Observes the internal `eventBus` to gather and expose real-time performance metrics of the store, such as update counts, listener executions, and
|
861
|
-
* **`StoreObserver<T>` (observability.ts)**: An optional,
|
862
|
-
* **`createMerge` (merge.ts)**: A factory function that returns a configurable deep merging utility. This utility
|
863
|
-
* **`createDiff` / `createDerivePaths` (diff.ts)**: Factory functions returning utilities for efficient comparison between two objects (
|
922
|
+
* **`ReactiveDataStore<T>` (`store.ts`)**: The public API and primary entry point. It orchestrates interactions between all other internal components. It manages the update queue, ensures sequential processing of `set` calls, and exposes public methods like `get`, `set`, `subscribe`, `transaction`, `use`, `unuse`, and `onStoreEvent`.
|
923
|
+
* **`CoreStateManager<T>` (`state.ts`)**: Responsible for the direct management of the immutable state (`cache`). It applies incoming state changes, performs efficient object `diff`ing to identify modified paths, and notifies internal listeners (via an `updateBus`) about granular state changes.
|
924
|
+
* **`MiddlewareEngine<T>` (`middleware.ts`)**: Manages the registration and execution of both `blocking` and `transform` middleware functions. It ensures middleware execution order, handles potential errors, and emits detailed lifecycle events for observability.
|
925
|
+
* **`PersistenceHandler<T>` (`persistence.ts`)**: Handles integration with an external persistence layer via the `SimplePersistence` interface. It loads initial state, saves subsequent changes, and listens for external updates from the persistence layer to keep the in-memory state synchronized across multiple instances (e.g., browser tabs).
|
926
|
+
* **`TransactionManager<T>` (`transactions.ts`)**: Provides atomic state operations. It creates a snapshot of the state before an `operation` begins and, if the operation fails, ensures the state is reverted to this snapshot, guaranteeing data integrity. It integrates closely with the store's event system for tracking transaction status.
|
927
|
+
* **`MetricsCollector` (`metrics.ts`)**: Observes the internal `eventBus` to gather and expose real-time performance metrics of the store, such as update counts, listener executions, average update times, and the largest update size.
|
928
|
+
* **`StoreObserver<T>` (`observability.ts`)**: An optional, yet highly valuable, debugging companion. It taps into the `ReactiveDataStore`'s extensive event stream and state changes to build a comprehensive history of events and state snapshots, enabling powerful features like time-travel debugging, detailed console logging, and performance monitoring.
|
929
|
+
* **`createMerge` (`merge.ts`)**: A factory function that returns a configurable deep merging utility (`MergeFunction`). This utility is crucial for immutably applying partial updates and specifically handles `Symbol.for("delete")` for explicit property removal.
|
930
|
+
* **`createDiff` / `createDerivePaths` (`diff.ts`)**: Factory functions returning utilities for efficient comparison between two objects (`diff`) to identify changed paths, and for deriving all parent paths from a set of changes (`derivePaths`). These are fundamental for optimizing listener notifications and internal change detection.
|
864
931
|
|
865
932
|
### Data Flow
|
866
933
|
|
867
|
-
The `ReactiveDataStore` handles state updates in a robust, queued manner
|
934
|
+
The `ReactiveDataStore` handles state updates in a robust, queued, and event-driven manner:
|
868
935
|
|
869
936
|
1. **`store.set(update)` call**:
|
870
|
-
* If
|
871
|
-
* An `update:start` event is emitted.
|
937
|
+
* If another update is already in progress (`isUpdating`), the new `update` is queued in `pendingUpdates`. This ensures sequential processing and prevents race conditions, and the `store.state().pendingChanges` reflects the queue.
|
938
|
+
* An `update:start` event is immediately emitted.
|
872
939
|
2. **Middleware Execution**:
|
873
|
-
* The `MiddlewareEngine` first
|
874
|
-
* If not blocked, `transform` middlewares (registered via `store.use({ action: ... })`) are executed sequentially. Each transform middleware receives the current state and the incoming partial update, and can return a new
|
875
|
-
*
|
940
|
+
* The `MiddlewareEngine` first executes all `blocking` middlewares (registered via `store.use({ block: true, ... })`). If any blocking middleware returns `false` or throws an error, the update is immediately halted. An `update:complete` event with `blocked: true` is emitted, and the process stops, with the state remaining unchanged.
|
941
|
+
* If not blocked, `transform` middlewares (registered via `store.use({ action: ... })`) are executed sequentially. Each transform middleware receives the current state and the incoming partial update, and can return a new `DeepPartial<T>` that is then merged into the effective update payload.
|
942
|
+
* Detailed lifecycle events (`middleware:start`, `middleware:complete`, `middleware:error`, `middleware:blocked`, `middleware:executed`) are emitted during this phase, providing granular insight into middleware behavior.
|
876
943
|
3. **State Application**:
|
877
944
|
* The `CoreStateManager` receives the (potentially transformed) final `DeepPartial` update.
|
878
|
-
* It internally uses the `
|
879
|
-
* It then performs a `
|
880
|
-
* If changes are detected, the `CoreStateManager` updates its internal immutable `cache` to the `newState` and then emits an `update` event for each granular `changedPath` on its
|
945
|
+
* It internally uses the `createMerge` utility to immutably construct the new full state object.
|
946
|
+
* It then performs a `createDiff` comparison between the *previous* state and the *new* state to precisely identify all `changedPaths` (an array of strings).
|
947
|
+
* If changes are detected, the `CoreStateManager` updates its internal immutable `cache` to the `newState` and then emits an internal `update` event for each granular `changedPath` on its `updateBus`.
|
881
948
|
4. **Listener Notification**:
|
882
|
-
* Any external subscribers (registered with `store.subscribe()`) whose registered paths match or are parent paths of the `changedPaths` are notified with the latest state. The `MetricsCollector` tracks `listenerExecutions
|
949
|
+
* Any external subscribers (registered with `store.subscribe()`) whose registered paths match or are parent paths of the `changedPaths` are efficiently notified with the latest state. The `MetricsCollector` tracks `listenerExecutions` during this phase.
|
883
950
|
5. **Persistence Handling**:
|
884
951
|
* The `PersistenceHandler` receives the `changedPaths` and the new state. If a `SimplePersistence` implementation was configured during store initialization, it attempts to save the new state using `persistence.set()`.
|
885
952
|
* The `PersistenceHandler` also manages loading initial state and reacting to external state changes (e.g., from other browser tabs or processes) through `persistence.subscribe()`.
|
886
953
|
6. **Completion & Queue Processing**:
|
887
|
-
* An `update:complete` event is emitted, containing information about the update's duration,
|
888
|
-
* The `isUpdating` flag is reset
|
954
|
+
* An `update:complete` event is emitted, containing crucial information about the update's duration, the `changedPaths`, and any blocking errors.
|
955
|
+
* The `isUpdating` flag is reset. If there are any `pendingUpdates` in the queue, the next update is immediately pulled and processed, ensuring all queued updates are eventually applied in order.
|
889
956
|
|
890
957
|
### Extension Points
|
891
958
|
|
892
|
-
* **Custom Middleware**:
|
893
|
-
* **Custom Persistence**: The `SimplePersistence<T>` interface
|
959
|
+
* **Custom Middleware**: Developers can inject their own `Middleware` (for transformation) and `BlockingMiddleware` (for validation/prevention) functions using `store.use()`. This allows for highly customizable update logic, centralized logging, complex validation, authorization, or triggering specific side effects.
|
960
|
+
* **Custom Persistence**: The `SimplePersistence<T>` interface provides a clear contract for developers to integrate the store with any storage solution, whether it's local storage, IndexedDB, a backend API, or a WebSocket connection. This offers complete control over data durability and synchronization.
|
894
961
|
|
895
962
|
---
|
896
963
|
|
897
964
|
## Development & Contributing
|
898
965
|
|
899
|
-
Contributions are welcome! Follow these guidelines to get started.
|
966
|
+
Contributions are welcome! Follow these guidelines to get started with local development and contribute to the project.
|
900
967
|
|
901
968
|
### Development Setup
|
902
969
|
|
@@ -906,7 +973,7 @@ Contributions are welcome! Follow these guidelines to get started.
|
|
906
973
|
cd erp-utils
|
907
974
|
```
|
908
975
|
2. **Install dependencies:**
|
909
|
-
The `@asaidimu/utils-store` module is part of a monorepo managed with `pnpm` workspaces.
|
976
|
+
The `@asaidimu/utils-store` module is part of a monorepo managed with `pnpm` workspaces. Ensure you have `pnpm` installed globally.
|
910
977
|
```bash
|
911
978
|
pnpm install
|
912
979
|
# If you don't have pnpm installed globally: npm install -g pnpm
|
@@ -914,25 +981,27 @@ Contributions are welcome! Follow these guidelines to get started.
|
|
914
981
|
3. **Build the project:**
|
915
982
|
Navigate to the `store` package directory and run the build script, or build the entire monorepo from the root.
|
916
983
|
```bash
|
917
|
-
|
918
|
-
pnpm build #
|
919
|
-
|
920
|
-
#
|
984
|
+
# From the monorepo root:
|
985
|
+
pnpm build # Builds all packages in the monorepo
|
986
|
+
|
987
|
+
# Or, from the `src/store` directory:
|
988
|
+
cd src/store
|
989
|
+
pnpm build
|
921
990
|
```
|
922
991
|
|
923
992
|
### Scripts
|
924
993
|
|
925
|
-
From the `src/store` directory:
|
994
|
+
From the `src/store` directory, the following `pnpm` scripts are available:
|
926
995
|
|
927
|
-
* `pnpm test`: Runs all unit tests using Vitest.
|
996
|
+
* `pnpm test`: Runs all unit tests using [Vitest](https://vitest.dev/).
|
928
997
|
* `pnpm test:watch`: Runs tests in watch mode for continuous development.
|
929
|
-
* `pnpm lint`: Lints the codebase using ESLint.
|
930
|
-
* `pnpm format`: Formats the code using Prettier.
|
931
|
-
* `pnpm build`: Compiles TypeScript to JavaScript and generates declaration files.
|
998
|
+
* `pnpm lint`: Lints the codebase using [ESLint](https://eslint.org/).
|
999
|
+
* `pnpm format`: Formats the code using [Prettier](https://prettier.io/).
|
1000
|
+
* `pnpm build`: Compiles TypeScript to JavaScript (CommonJS and ES Modules) and generates declaration files (`.d.ts`).
|
932
1001
|
|
933
1002
|
### Testing
|
934
1003
|
|
935
|
-
All tests are written
|
1004
|
+
All tests are written using [Vitest](https://vitest.dev/), a fast unit test framework powered by Vite.
|
936
1005
|
|
937
1006
|
To run tests:
|
938
1007
|
|
@@ -948,21 +1017,21 @@ cd src/store
|
|
948
1017
|
pnpm test:watch
|
949
1018
|
```
|
950
1019
|
|
951
|
-
|
1020
|
+
Please ensure all new features have comprehensive test coverage and all existing tests pass before submitting a pull request.
|
952
1021
|
|
953
1022
|
### Contributing Guidelines
|
954
1023
|
|
955
1024
|
1. **Fork the repository** and create your branch from `main`.
|
956
|
-
2. **Ensure code quality**: Write clean, readable, and maintainable code. Adhere to existing coding styles.
|
957
|
-
3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all existing tests pass.
|
958
|
-
4. **Commit messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history (e.g., `feat: add new feature`, `fix: resolve bug`).
|
959
|
-
5. **Pull Requests**: Open a pull request to the `main` branch of the `erp-utils` repository.
|
1025
|
+
2. **Ensure code quality**: Write clean, readable, and maintainable code. Adhere to existing coding styles, which are enforced by ESLint and Prettier.
|
1026
|
+
3. **Tests**: Add comprehensive unit and integration tests for new features or bug fixes. Ensure all existing tests pass.
|
1027
|
+
4. **Commit messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history (e.g., `feat: add new feature`, `fix: resolve bug`, `chore: update dependencies`).
|
1028
|
+
5. **Pull Requests**: Open a pull request to the `main` branch of the `asaidimu/erp-utils` repository. Clearly describe your changes, provide context, and link to any relevant issues.
|
960
1029
|
|
961
1030
|
### Issue Reporting
|
962
1031
|
|
963
1032
|
If you find a bug or have a feature request, please open an issue on the [GitHub repository](https://github.com/asaidimu/erp-utils/issues).
|
964
|
-
* For
|
965
|
-
* For feature requests
|
1033
|
+
* For **bug reports**, include steps to reproduce, expected behavior, and actual behavior. Provide relevant code snippets or a minimal reproducible example.
|
1034
|
+
* For **feature requests**, describe the use case, the problem it solves, and your proposed solution or ideas.
|
966
1035
|
|
967
1036
|
---
|
968
1037
|
|
@@ -971,44 +1040,45 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
|
|
971
1040
|
### Troubleshooting
|
972
1041
|
|
973
1042
|
* **"Update not triggering listeners"**:
|
974
|
-
* Ensure you are subscribing to the correct path. `store.subscribe('user.name', ...)` will not trigger if you update `user.email` (unless you also subscribe to `user` or the root `''`).
|
1043
|
+
* Ensure you are subscribing to the correct path. `store.subscribe('user.name', ...)` will not trigger if you update `user.email` (unless you also subscribe to `user` or the root `''` path).
|
975
1044
|
* If the new value is strictly equal (`===`) to the old value, no change will be detected by the internal `diff` function, and listeners will not be notified.
|
976
1045
|
* Verify your `DeepPartial` update correctly targets the intended part of the state.
|
977
1046
|
* **"State not rolling back after transaction error"**:
|
978
|
-
* Ensure the error is thrown *within* the `transaction` callback. Errors caught and handled inside, or thrown outside, will not trigger the rollback mechanism.
|
979
|
-
* Promises within the transaction *must* be `await`ed so the
|
1047
|
+
* Ensure the error is thrown *within* the `transaction` callback function. Errors caught and handled inside the callback, or thrown outside of it, will not trigger the rollback mechanism.
|
1048
|
+
* Promises within the transaction *must* be `await`ed so the `TransactionManager` can capture potential rejections and manage the atomic operation correctly.
|
980
1049
|
* **"Middleware not being applied"**:
|
981
|
-
* Verify the middleware is registered with `store.use()` *before* the `set` operation it should affect.
|
982
|
-
* Check middleware `name` in console logs (if `StoreObserver` is enabled) to
|
1050
|
+
* Verify the middleware is registered with `store.use()` *before* the `set` operation it should affect. Middleware functions are applied in the order they are registered.
|
1051
|
+
* Check middleware `name` in console logs (if `StoreObserver` is enabled with `enableConsoleLogging: true`) to confirm it's being hit.
|
983
1052
|
* Ensure your `transform` middleware returns a `DeepPartial<T>` or `void`/`Promise<void>`, and `blocking` middleware returns `boolean`/`Promise<boolean>`.
|
984
1053
|
* **"Performance warnings in console"**:
|
985
|
-
* If `StoreObserver` is enabled with `enableConsoleLogging: true`, it will warn about updates or middlewares exceeding defined `performanceThresholds`. This is an informational warning, not an error, indicating a potentially slow operation that could be optimized.
|
1054
|
+
* If `StoreObserver` is enabled with `enableConsoleLogging: true`, it will warn about updates or middlewares exceeding defined `performanceThresholds`. This is an informational warning, not an error, indicating a potentially slow operation that could be optimized in your application.
|
986
1055
|
|
987
1056
|
### FAQ
|
988
1057
|
|
989
1058
|
**Q: How are arrays handled during updates?**
|
990
|
-
A: Arrays are treated as primitive values. When you provide an array in a `DeepPartial` update (e.g., `set({ items: [new Array] })`), the entire array at that path is replaced, not merged. This ensures predictable behavior and
|
1059
|
+
A: Arrays are treated as primitive values. When you provide an array in a `DeepPartial` update (e.g., `set({ items: [new Array] })`), the entire array at that path is replaced with the new array, not merged element-by-element. This ensures predictable behavior and avoids complex, potentially ambiguous partial array merging logic.
|
991
1060
|
|
992
1061
|
**Q: What is `Symbol.for("delete")`?**
|
993
|
-
A:
|
1062
|
+
A: `Symbol.for("delete")` is a special global symbol used to explicitly remove a property from the state during a `set` operation. If you pass `Symbol.for("delete")` as the value for a key in your `DeepPartial` update, that key will be removed from the resulting state object. This provides a clear semantic for deletion.
|
994
1063
|
|
995
1064
|
**Q: How do I debug my store's state changes?**
|
996
|
-
A: The `StoreObserver` class is your primary tool. Instantiate it with your `ReactiveDataStore` instance. It provides methods to get event history, state snapshots, and even time-travel debugging capabilities. Enabling `enableConsoleLogging: true` in `StoreObserver` options provides immediate, formatted console output during development.
|
1065
|
+
A: The `StoreObserver` class is your primary tool. Instantiate it with your `ReactiveDataStore` instance. It provides methods to get event history, state snapshots, and even time-travel debugging capabilities. Enabling `enableConsoleLogging: true` in `StoreObserver` options provides immediate, formatted console output during development, showing update lifecycles, middleware execution, and transaction events.
|
997
1066
|
|
998
1067
|
**Q: What is `SimplePersistence<T>`?**
|
999
|
-
A: It's a
|
1068
|
+
A: It's a minimal interface that defines the contract for any persistence layer you wish to integrate. It requires `get()`, `set(instanceId, state)`, and `subscribe(instanceId, listener)` methods. You need to provide an implementation of this interface to enable state saving and loading. An example `InMemoryPersistence` implementation is provided in the [Persistence Integration](#persistence-integration) section.
|
1000
1069
|
|
1001
|
-
**Q: Can I use this with React/Vue/Angular?**
|
1002
|
-
A: Yes. This is a framework-agnostic state management library. You would typically use the `subscribe` method within your framework's lifecycle hooks (e.g., `useEffect` in React, `onMounted` in Vue) to react to state changes and update your UI components.
|
1070
|
+
**Q: Can I use this with React/Vue/Angular or other UI frameworks?**
|
1071
|
+
A: Yes, absolutely. This is a framework-agnostic state management library. You would typically use the `subscribe` method within your framework's lifecycle hooks (e.g., `useEffect` in React, `onMounted` in Vue) to react to state changes and update your UI components. The library's reactivity model is independent of any specific framework.
|
1003
1072
|
|
1004
1073
|
### Changelog / Roadmap
|
1005
1074
|
|
1006
|
-
* **Changelog**: For detailed version history, please refer to the [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/src/store/CHANGELOG.md) file.
|
1007
|
-
* **Roadmap**: Future plans may include:
|
1008
|
-
* Official framework integrations (e.g., React hooks library).
|
1009
|
-
* More advanced query/selector capabilities with memoization.
|
1010
|
-
* Built-in serialization/deserialization options for persistence.
|
1011
|
-
*
|
1075
|
+
* **Changelog**: For detailed version history, including new features, bug fixes, and breaking changes, please refer to the project's [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/src/store/CHANGELOG.md) file.
|
1076
|
+
* **Roadmap**: Future plans for `@asaidimu/utils-store` may include:
|
1077
|
+
* Official framework-specific integrations (e.g., React hooks library for easier consumption).
|
1078
|
+
* More advanced query/selector capabilities with built-in memoization for derived state.
|
1079
|
+
* Built-in serialization/deserialization options for persistence, perhaps with schema validation.
|
1080
|
+
* Higher-order middlewares for common patterns (e.g., async data fetching, debouncing updates).
|
1081
|
+
* Further performance optimizations for very large states or high update frequencies.
|
1012
1082
|
|
1013
1083
|
### License
|
1014
1084
|
|
@@ -1016,6 +1086,6 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
|
|
1016
1086
|
|
1017
1087
|
### Acknowledgments
|
1018
1088
|
|
1019
|
-
* Inspired by modern state management patterns and
|
1020
|
-
*
|
1021
|
-
* Utilizes `uuid` for unique instance IDs.
|
1089
|
+
* Inspired by modern state management patterns such as Redux, Zustand, and Vuex, emphasizing immutability and explicit state changes.
|
1090
|
+
* Leverages the `@asaidimu/events` package for robust internal event bus capabilities.
|
1091
|
+
* Utilizes the `uuid` library for generating unique instance IDs.
|
package/index.d.mts
CHANGED
package/index.d.ts
CHANGED