@asaidimu/utils-store 2.0.4 → 2.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.
Files changed (2) hide show
  1. package/README.md +272 -202
  2. 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
  [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-store?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-store)
8
- [![License](https://img.shields.io/npm/l/@asaidimu/utils-store?style=flat-square)](LICENSE)
8
+ [![License](https://img.shields.io/npm/l/@asaidimu/utils-store?style=flat-square)](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
9
9
  [![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/erp-utils/ci.yml?branch=main&style=flat-square)](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**: Modify, normalize, or enrich state updates before they are applied. Return a `DeepPartial<T>` to apply further changes.
42
- * **Blocking Middleware**: Implement custom validation or authorization logic to prevent invalid state changes from occurring. These middlewares return a boolean.
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 sync) to load and save state, ensuring data durability and synchronization across instances.
45
- * 🔍 **Deep Observer & Debugging**: An optional `StoreObserver` class provides unparalleled runtime introspection:
46
- * **Event History**: Keep a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, `middleware:executed`, etc.).
47
- * **State Snapshots**: Maintain a configurable history of your state over time, allowing for easy inspection of changes between updates.
48
- * **Time-Travel Debugging**: Undo and redo state changes using the recorded state history, providing powerful capabilities for debugging complex scenarios.
49
- * **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times, and largest update size to identify bottlenecks.
50
- * **Console Logging**: Configurable, human-readable logging of store events directly to the browser console for immediate feedback during development.
51
- * **Pre-built Debugging Middlewares**: Includes helpers to create a generic logging middleware and a validation middleware.
52
- * **Efficient Change Detection**: Utilizes a custom `diff` algorithm to identify only the truly changed paths (`string[]`), optimizing listener notifications and ensuring minimal overhead.
53
- * 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using `Symbol.for("delete")`.
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 the entire collection using your preferred package manager. You can then import specific utilities as needed. This library is designed for browser and Node.js environments.
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-safe development)
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 installation, you can run a simple test script:
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
- Creating a store, getting state, and setting updates.
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
- // Output: Initial state: { user: { ... }, products: [ ... ], ... }
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
- // Output: User name is now 'Jane Smith', isActive is false, theme is 'dark'.
194
- // Email and products remain unchanged.
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
- // Output:
231
- // User data changed: { id: '123', name: 'Jane Smith', email: 'jane.smith@example.com', isActive: false }
232
- // User name or products changed: Jane Smith 3
233
- // Store updated (any path changed). Current products count: 3
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
- // Output:
237
- // Notifications setting changed: false
238
- // Store updated (any path changed). Current products count: 3
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
- // Assuming @core/persistence or similar is available from your utility package
269
- import type { SimplePersistence } from '@core/persistence';
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 app, this would interact with localStorage, IndexedDB, a backend, etc.
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('Store is ready and persistence is initialized!');
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: This 'another-instance-id' ensures the current store instance gets the update via subscription.
347
- await userConfigPersistence.set('another-instance-id', { theme: 'system', fontSize: 20 } as UserConfig); // Simulating a state update from outside the store
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
- // Increments the counter by the incoming value if the update is a number.
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
- // Expected console output from LoggerMiddleware: Middleware: Incoming update: { counter: 5 }
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
- // Expected console output from LoggerMiddleware: Middleware: Incoming update: { lastAction: 'Manual update from outside middleware' }
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
- // Cannot become admin if under 21
491
- if (update.isAdmin === true && state.age < 21) {
492
- console.warn('Blocking update: User must be 21+ to become admin.');
493
- return false;
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 (should fail)
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 (should fail due to age)
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 only has 400 now, so this should fail
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: 'SimpleMiddleware', action: async (state, update) => {
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}:`, snapshot.messages, snapshot.user.status));
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 (latest)
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: [] (original initial state)
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()); // Will be `maxStateHistory` (5 in this case) + any initial states.
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 primary entry point and public API for the state management system. It acts as an orchestrator, delegating tasks to specialized internal components. It manages the update queue and public interfaces like `set`, `get`, `subscribe`, `transaction`, `use`, and `onStoreEvent`.
856
- * **`CoreStateManager<T>` (state.ts)**: Encapsulates the actual, immutable state (`cache`). It's responsible for applying incoming state changes, performing efficient `diff`ing to identify modified paths, and notifying internal listeners (via an `updateBus`) about concrete state changes.
857
- * **`MiddlewareEngine<T>` (middleware.ts)**: Manages the registration and execution of middleware functions. It differentiates between `blocking` middleware (which can halt an update) and `transform` middleware (which can modify the update payload), ensuring they run in the correct order. It also emits detailed lifecycle events for observability.
858
- * **`PersistenceHandler<T>` (persistence.ts)**: Deals with external data persistence. It loads initial state from a provided `SimplePersistence` implementation and saves subsequent state changes. It also listens for external updates from the persistence layer to keep the in-memory state synchronized.
859
- * **`TransactionManager<T>` (transactions.ts)**: Provides atomic state operations. It creates a snapshot of the state before an `operation` and, if the operation fails, it ensures the state is reverted to this snapshot, guaranteeing data integrity. It integrates closely with the store's event system for tracking.
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 average update times.
861
- * **`StoreObserver<T>` (observability.ts)**: An optional, but highly recommended, debugging companion. It taps into the `ReactiveDataStore`'s events and state changes to build a comprehensive history of events and state snapshots, enabling time-travel debugging, detailed console logging, and performance monitoring.
862
- * **`createMerge` (merge.ts)**: A factory function that returns a configurable deep merging utility. This utility preserves immutability and specifically handles `Symbol.for("delete")` for explicit property removal.
863
- * **`createDiff` / `createDerivePaths` (diff.ts)**: Factory functions returning utilities for efficient comparison between two objects (original vs. changes) to identify changed paths (`diff`) and for deriving all parent paths from a set of changes (`derivePaths`). This is crucial for optimizing listener notifications and change detection.
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 an update is already in progress (`isUpdating`), the new `update` is queued in `pendingUpdates` (available via `store.state().pendingChanges`). This ensures sequential processing and prevents race conditions.
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 runs all `blocking` middlewares (registered via `store.use({ block: true, ... })`). If any blocking middleware returns `false` or throws an error, the update is halted, an `update:complete` (blocked) event is emitted, and the process stops.
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 partial state that is merged into the effective update payload.
875
- * Lifecycle events (`middleware:start`, `middleware:complete`, `middleware:error`, `middleware:blocked`, `middleware:executed`) are emitted during this phase for observability.
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 `merge` function to create the new full state object immutably.
879
- * It then performs a `diff` comparison between the *previous* state and the *new* state to identify all changed paths (`string[]`).
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 internal `updateBus`.
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, changed paths, and any blocking errors.
888
- * The `isUpdating` flag is reset, and if there are `pendingUpdates` in the queue, the next update is immediately pulled and processed, ensuring sequential updates.
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**: Users can inject their own `Middleware` and `BlockingMiddleware` functions using `store.use()` to customize update logic, add logging, validation, authorization, or trigger side effects.
893
- * **Custom Persistence**: The `SimplePersistence<T>` interface allows developers to integrate the store with any storage solution, whether it's local storage, IndexedDB, a backend API, or a WebSocket connection, providing full control over data durability.
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
- cd src/store # (from the monorepo root)
918
- pnpm build # or npm run build
919
- # Or, from the monorepo root:
920
- # pnpm build
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 with [Vitest](https://vitest.dev/).
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
- Ensure all new features have comprehensive test coverage and existing tests pass.
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. Describe your changes clearly and link to any relevant issues.
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 bugs, include steps to reproduce, expected behavior, and actual behavior.
965
- * For feature requests, describe the use case and proposed solution.
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 transaction manager can capture potential rejections and manage the atomic operation.
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 ensure it's being hit.
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 prevents complex partial array merging logic.
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: It's a special global symbol (`Symbol.for("delete")` evaluates to the same symbol across realms) 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 state object.
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 simple interface (`{ get(): Promise<T | null>; set(instanceId: string, state: T): Promise<boolean>; subscribe(instanceId: string, listener: (state: T) => void): () => void; }`) that defines the contract for any persistence layer. You need to provide an implementation of this interface to enable state saving and loading. An example can be found in the `InMemoryPersistence` example above.
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
- * Middleware for common patterns (e.g., async data fetching).
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 principles of immutability.
1020
- * Uses `@asaidimu/events` for the internal event bus.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asaidimu/utils-store",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "A reactive data store",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",