@asaidimu/utils-store 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
- # `@asaidimu/utils-store` - Reactive Data Store
1
+ # `@asaidimu/utils-store`
2
+
3
+ **A Reactive Data Store**
2
4
 
3
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.
4
6
 
5
7
  [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-store?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-store)
6
8
  [![License](https://img.shields.io/npm/l/@asaidimu/utils-store?style=flat-square)](LICENSE)
7
- [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat-square)](https://github.com/asaidimu/utils-actions?query=workflow%3ACI)
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)
8
10
 
9
11
  ---
10
12
 
@@ -17,7 +19,7 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
17
19
  - [Persistence Integration](#persistence-integration)
18
20
  - [Middleware System](#middleware-system)
19
21
  - [Transaction Support](#transaction-support)
20
- - [Store Observability](#store-observability)
22
+ - [Store Observer](#store-observability)
21
23
  - [Event System](#event-system)
22
24
  - [Project Architecture](#project-architecture)
23
25
  - [Development & Contributing](#development--contributing)
@@ -29,7 +31,7 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
29
31
 
30
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.
31
33
 
32
- Whether you're building a simple utility or a complex application, this library offers the 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 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.
33
35
 
34
36
  ### Key Features
35
37
 
@@ -40,8 +42,8 @@ Whether you're building a simple utility or a complex application, this library
40
42
  * **Blocking Middleware**: Implement custom validation or authorization logic to prevent invalid state changes from occurring. These middlewares return a boolean.
41
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.
42
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.
43
- * 🔍 **Deep Observability & Debugging**: An optional `StoreObservability` class provides unparalleled runtime introspection:
44
- * **Event History**: Keep a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, etc.).
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.).
45
47
  * **State Snapshots**: Maintain a configurable history of your state over time, allowing for easy inspection of changes between updates.
46
48
  * **Time-Travel Debugging**: Undo and redo state changes using the recorded state history, providing powerful capabilities for debugging complex scenarios.
47
49
  * **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times, and largest update size to identify bottlenecks.
@@ -60,6 +62,12 @@ Install the entire collection using your preferred package manager. You can then
60
62
  ```bash
61
63
  # Using Bun
62
64
  bun add @asaidimu/utils-store
65
+
66
+ # Using npm
67
+ npm install @asaidimu/utils-store
68
+
69
+ # Using yarn
70
+ yarn add @asaidimu/utils-store
63
71
  ```
64
72
 
65
73
  ### Prerequisites
@@ -82,18 +90,31 @@ interface MyState {
82
90
 
83
91
  const store = new ReactiveDataStore<MyState>({ count: 0, message: "Hello" });
84
92
 
93
+ // Subscribing to "count" will only log when 'count' path changes
85
94
  store.subscribe("count", (state) => {
86
95
  console.log(`Count changed to: ${state.count}`);
87
96
  });
88
97
 
89
- await store.set({ count: 1 });
90
- await store.set({ message: "World" }); // This won't trigger the 'count' listener
98
+ // Subscribing to "" (empty string) will log for any store update
99
+ store.subscribe("", (state) => {
100
+ console.log(`Store updated to: ${JSON.stringify(state)}`);
101
+ });
91
102
 
92
- console.log("Current state:", store.get());
103
+ console.log("Initial state:", store.get());
104
+ // Output: Initial state: { count: 0, message: "Hello" }
93
105
 
94
- // Expected Output:
106
+ await store.set({ count: 1 });
107
+ // Output:
95
108
  // Count changed to: 1
96
- // Current state: { count: 1, message: "World" }
109
+ // Store updated to: {"count":1,"message":"Hello"}
110
+
111
+ await store.set({ message: "World" });
112
+ // Output:
113
+ // Store updated to: {"count":1,"message":"World"}
114
+ // (The 'count' listener won't be triggered as only 'message' changed)
115
+
116
+ console.log("Current state:", store.get());
117
+ // Output: Current state: { count: 1, message: "World" }
97
118
  ```
98
119
 
99
120
  Run this file:
@@ -112,7 +133,7 @@ This section provides practical examples of how to use `@asaidimu/utils-store` t
112
133
  Creating a store, getting state, and setting updates.
113
134
 
114
135
  ```typescript
115
- import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
136
+ import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
116
137
 
117
138
  // 1. Define your state interface for type safety
118
139
  interface AppState {
@@ -182,8 +203,8 @@ await store.set((state) => ({
182
203
  lastUpdated: Date.now(),
183
204
  }));
184
205
 
185
- console.log('State after functional update:', store.get().products.length);
186
- // Output: State after functional update: 3
206
+ console.log('State after functional update, products count:', store.get().products.length);
207
+ // Output: State after functional update, products count: 3
187
208
 
188
209
  // 6. Subscribing to state changes
189
210
  // You can subscribe to the entire state or specific paths.
@@ -202,19 +223,19 @@ const unsubscribeMulti = store.subscribe(['user.name', 'products'], (state) => {
202
223
 
203
224
  // Subscribe to any change in the store
204
225
  const unsubscribeAll = store.subscribe('', (state) => {
205
- console.log('Store updated (any path changed).');
226
+ console.log('Store updated (any path changed). Current products count:', state.products.length);
206
227
  });
207
228
 
208
229
  await store.set({ user: { email: 'jane.smith@example.com' } });
209
230
  // Output:
210
231
  // User data changed: { id: '123', name: 'Jane Smith', email: 'jane.smith@example.com', isActive: false }
211
232
  // User name or products changed: Jane Smith 3
212
- // Store updated (any path changed).
233
+ // Store updated (any path changed). Current products count: 3
213
234
 
214
235
  await store.set({ settings: { notificationsEnabled: false } });
215
236
  // Output:
216
237
  // Notifications setting changed: false
217
- // Store updated (any path changed).
238
+ // Store updated (any path changed). Current products count: 3
218
239
 
219
240
  // 7. Unsubscribe from changes
220
241
  unsubscribeUser();
@@ -227,10 +248,11 @@ await store.set({ user: { isActive: true } });
227
248
 
228
249
  // 8. Deleting properties
229
250
  // Use Symbol.for("delete") to remove a property from the state.
230
- const deleteSymbol = Symbol.for("delete");
251
+ // Note: You might need a type cast for TypeScript if strict type checking is enabled.
252
+ const DELETE_ME = Symbol.for("delete");
231
253
  await store.set({
232
254
  user: {
233
- email: deleteSymbol as DeepPartial<string> // Cast is needed for type inference
255
+ email: DELETE_ME as DeepPartial<string> // Cast is needed for type inference
234
256
  }
235
257
  });
236
258
  console.log('User email after deletion:', store.get().user.email);
@@ -239,18 +261,24 @@ console.log('User email after deletion:', store.get().user.email);
239
261
 
240
262
  ### Persistence Integration
241
263
 
242
- 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.
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.
243
265
 
244
266
  ```typescript
245
- import { ReactiveDataStore, StoreEvent } from '@asaidimu/utils-store';
246
- import { SimplePersistence } from '@core/persistence'; // Your persistence implementation
267
+ 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';
247
270
 
248
271
  // Example: A simple in-memory persistence for demonstration
249
272
  // In a real app, this would interact with localStorage, IndexedDB, a backend, etc.
250
273
  class InMemoryPersistence<T> implements SimplePersistence<T> {
251
274
  private data: T | null = null;
275
+ // Using a map to simulate different instances subscribing to common data
252
276
  private subscribers: Map<string, (state: T) => void> = new Map();
253
277
 
278
+ constructor(initialData: T | null = null) {
279
+ this.data = initialData;
280
+ }
281
+
254
282
  async get(): Promise<T | null> {
255
283
  console.log('Persistence: Loading state...');
256
284
  return this.data;
@@ -261,8 +289,9 @@ class InMemoryPersistence<T> implements SimplePersistence<T> {
261
289
  this.data = state;
262
290
  // Simulate external change notification for other instances
263
291
  this.subscribers.forEach((callback, subId) => {
264
- if (subId !== instanceId) { // Don't notify the instance that just saved
265
- callback(this.data!);
292
+ // Only notify other instances, not the one that just saved
293
+ if (subId !== instanceId) {
294
+ callback(structuredClone(this.data!)); // Pass a clone to prevent mutation
266
295
  }
267
296
  });
268
297
  return true;
@@ -283,35 +312,42 @@ interface UserConfig {
283
312
  fontSize: number;
284
313
  }
285
314
 
286
- // Create a persistence instance
287
- const userConfigPersistence = new InMemoryPersistence<UserConfig>();
315
+ // Create a persistence instance, possibly with some pre-existing data
316
+ const userConfigPersistence = new InMemoryPersistence<UserConfig>({ theme: 'dark', fontSize: 18 });
288
317
 
289
- // Optionally, listen for persistence readiness
290
- const storeReady = new Promise<void>(resolve => {
318
+ // Initialize the store with persistence
319
+ const store = new ReactiveDataStore<UserConfig>(
320
+ { theme: 'light', fontSize: 16 }, // Initial state if no persisted data found or persistence is not used
321
+ userConfigPersistence // Pass your persistence implementation here
322
+ );
323
+
324
+ // Optionally, listen for persistence readiness (important for UIs that depend on loaded state)
325
+ const storeReadyPromise = new Promise<void>(resolve => {
291
326
  store.onStoreEvent('persistence:ready', () => {
292
327
  console.log('Store is ready and persistence is initialized!');
293
328
  resolve();
294
329
  });
295
330
  });
296
331
 
297
- // Initialize the store with persistence
298
- const store = new ReactiveDataStore<UserConfig>(
299
- { theme: 'light', fontSize: 16 }, // Initial state if no persisted data
300
- userConfigPersistence // Pass your persistence implementation here
301
- );
332
+ console.log('Store initial state (before persistence loads):', store.get());
302
333
 
303
- console.log('Store initial state:', store.get()); // May reflect initial state if persistence is empty
334
+ await storeReadyPromise; // Wait for persistence to load/initialize
304
335
 
305
- await storeReady; // Wait for persistence to load/initialize
336
+ // Now, store.get() will reflect the loaded state from persistence
337
+ console.log('Store state after persistence load:', store.get());
338
+ // Output: Store state after persistence load: { theme: 'dark', fontSize: 18 } (from InMemoryPersistence)
306
339
 
307
340
  // Now update the state, which will trigger persistence.set()
308
- await store.set({ theme: 'dark' });
341
+ await store.set({ theme: 'light' });
309
342
  console.log('Current theme:', store.get().theme);
343
+ // Output: Current theme: light
310
344
 
311
345
  // Simulate an external change (e.g., another tab updating the state)
312
- await userConfigPersistence.set('another-instance-id', { theme: 'light', fontSize: 18 });
313
- // Store will automatically update its state and notify its listeners.
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
348
+ // The store will automatically update its state and notify its listeners due to the internal subscription.
314
349
  console.log('Current theme after external update:', store.get().theme);
350
+ // Output: Current theme after external update: system
315
351
  ```
316
352
 
317
353
  ### Middleware System
@@ -323,36 +359,37 @@ Middleware functions allow you to intercept and modify state updates.
323
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.
324
360
 
325
361
  ```typescript
326
- import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
362
+ import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
327
363
 
328
364
  interface MyState {
329
365
  counter: number;
330
366
  logs: string[];
331
367
  lastAction: string | null;
368
+ version: number;
332
369
  }
333
370
 
334
371
  const store = new ReactiveDataStore<MyState>({
335
372
  counter: 0,
336
373
  logs: [],
337
374
  lastAction: null,
375
+ version: 0,
338
376
  });
339
377
 
340
378
  // Middleware 1: Logger
341
- // Logs the incoming update before it's processed
379
+ // Logs the incoming update before it's processed. Does not return anything (void).
342
380
  store.use({
343
381
  name: 'LoggerMiddleware',
344
382
  action: (state, update) => {
345
383
  console.log('Middleware: Incoming update:', update);
346
- // You don't need to return anything if you just want to observe or perform side effects
347
384
  },
348
385
  });
349
386
 
350
387
  // Middleware 2: Timestamp and Action Tracker
351
- // Modifies the update to add a timestamp and track the last action
388
+ // Modifies the update to add a timestamp and track the last action. Returns a partial state.
352
389
  store.use({
353
390
  name: 'TimestampActionMiddleware',
354
391
  action: (state, update) => {
355
- // This middleware transforms the update by adding `lastAction`
392
+ // This middleware transforms the update by adding `lastAction` and a log entry
356
393
  const actionDescription = JSON.stringify(update);
357
394
  return {
358
395
  lastAction: `Updated at ${new Date().toLocaleTimeString()} with ${actionDescription}`,
@@ -361,12 +398,22 @@ store.use({
361
398
  },
362
399
  });
363
400
 
364
- // Middleware 3: Counter Incrementor
365
- // Increments the counter whenever a specific property is updated
401
+ // Middleware 3: Version Incrementor
402
+ // Increments a version counter for every update.
403
+ store.use({
404
+ name: 'VersionIncrementMiddleware',
405
+ action: (state) => {
406
+ return { version: state.version + 1 };
407
+ },
408
+ });
409
+
410
+ // Middleware 4: Counter Incrementor
411
+ // Increments the counter by the incoming value if the update is a number.
366
412
  store.use({
367
413
  name: 'CounterIncrementMiddleware',
368
414
  action: (state, update) => {
369
- if (update.counter !== undefined && typeof update.counter === 'number') {
415
+ // Only apply if the incoming update is a number for 'counter'
416
+ if (typeof update.counter === 'number') {
370
417
  return { counter: state.counter + update.counter };
371
418
  }
372
419
  // Return original update or void if no transformation needed
@@ -375,23 +422,30 @@ store.use({
375
422
  });
376
423
 
377
424
  await store.set({ counter: 5 }); // Will increment counter by 5, not set to 5
378
- // Output:
379
- // Middleware: Incoming update: { counter: 5 }
425
+ // Expected console output from LoggerMiddleware: Middleware: Incoming update: { counter: 5 }
380
426
  console.log('State after counter set:', store.get());
381
- // Output: counter: 5 (initial) + 5 (update) = 10, lastAction, logs updated
382
-
383
- await store.set({ lastAction: 'Manual update' });
384
- // Output:
385
- // Middleware: Incoming update: { lastAction: 'Manual update' }
427
+ /* Output will show:
428
+ counter: 5 (initial) + 5 (update) = 10,
429
+ lastAction updated by TimestampActionMiddleware,
430
+ logs updated by TimestampActionMiddleware,
431
+ version: 1 (incremented by VersionIncrementMiddleware)
432
+ */
433
+
434
+ await store.set({ lastAction: 'Manual update from outside middleware' });
435
+ // Expected console output from LoggerMiddleware: Middleware: Incoming update: { lastAction: 'Manual update from outside middleware' }
386
436
  console.log('State after manual action:', store.get());
387
- // Output: lastAction will be overwritten by TimestampActionMiddleware logic
388
- // and a new log entry will be added.
437
+ /* Output will show:
438
+ lastAction will be overwritten by TimestampActionMiddleware logic,
439
+ a new log entry will be added,
440
+ version: 2
441
+ */
389
442
 
390
443
  // Unuse a middleware by its ID
391
- const loggerId = store.use({ name: 'AnotherLogger', action: (s, u) => console.log('Another logger', u) });
444
+ const temporaryLoggerId = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) });
392
445
  await store.set({ counter: 1 });
393
- store.unuse(loggerId);
394
- await store.set({ counter: 1 }); // AnotherLogger will not be called now
446
+ // Output: Temporary logger saw: { counter: 1 }
447
+ store.unuse(temporaryLoggerId);
448
+ await store.set({ counter: 1 }); // TemporaryLogger will not be called now
395
449
  ```
396
450
 
397
451
  #### Blocking Middleware
@@ -399,26 +453,28 @@ await store.set({ counter: 1 }); // AnotherLogger will not be called now
399
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.
400
454
 
401
455
  ```typescript
402
- import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
456
+ import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
403
457
 
404
458
  interface UserProfile {
405
459
  name: string;
406
460
  age: number;
407
461
  isAdmin: boolean;
462
+ isVerified: boolean;
408
463
  }
409
464
 
410
465
  const store = new ReactiveDataStore<UserProfile>({
411
466
  name: 'Guest',
412
467
  age: 0,
413
468
  isAdmin: false,
469
+ isVerified: false,
414
470
  });
415
471
 
416
- // Blocking middleware: Age validation
472
+ // Blocking middleware 1: Age validation
417
473
  store.use({
418
474
  block: true, // Mark as a blocking middleware
419
475
  name: 'AgeValidationMiddleware',
420
476
  action: (state, update) => {
421
- if (update.age !== undefined && update.age < 18) {
477
+ if (update.age !== undefined && typeof update.age === 'number' && update.age < 18) {
422
478
  console.warn('Blocking update: Age must be 18 or older.');
423
479
  return false; // Block the update
424
480
  }
@@ -426,15 +482,21 @@ store.use({
426
482
  },
427
483
  });
428
484
 
429
- // Blocking middleware: Admin check
485
+ // Blocking middleware 2: Admin check
430
486
  store.use({
431
487
  block: true,
432
488
  name: 'AdminRestrictionMiddleware',
433
489
  action: (state, update) => {
490
+ // Cannot become admin if under 21
434
491
  if (update.isAdmin === true && state.age < 21) {
435
492
  console.warn('Blocking update: User must be 21+ to become admin.');
436
493
  return false;
437
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.');
498
+ return false;
499
+ }
438
500
  return true;
439
501
  },
440
502
  });
@@ -447,12 +509,17 @@ console.log('User age after valid update:', store.get().age); // Output: 25
447
509
  await store.set({ age: 16 });
448
510
  console.log('User age after invalid update attempt (should be 25):', store.get().age); // Output: 25
449
511
 
450
- // Attempt to make user admin under age 21
451
- await store.set({ age: 20 });
512
+ // Attempt to make user admin while not verified (should fail)
452
513
  await store.set({ isAdmin: true });
453
514
  console.log('User admin status after failed attempt (should be false):', store.get().isAdmin); // Output: false
454
515
 
455
- // Now make user old enough and try again
516
+ // Verify user, then attempt to make admin again (should fail due to age)
517
+ await store.set({ isVerified: true });
518
+ await store.set({ age: 20 });
519
+ await store.set({ isAdmin: true });
520
+ console.log('User admin status after failed age attempt (should be false):', store.get().isAdmin); // Output: false
521
+
522
+ // Now make user old enough and verified, then try again (should succeed)
456
523
  await store.set({ age: 25 });
457
524
  await store.set({ isAdmin: true });
458
525
  console.log('User admin status after successful attempt (should be true):', store.get().isAdmin); // Output: true
@@ -466,95 +533,108 @@ Use `store.transaction()` to group multiple state updates into a single atomic o
466
533
  import { ReactiveDataStore } from '@asaidimu/utils-store';
467
534
 
468
535
  interface BankAccount {
536
+ name: string;
469
537
  balance: number;
470
538
  transactions: string[];
471
539
  }
472
540
 
473
- const store = new ReactiveDataStore<BankAccount>({
474
- balance: 1000,
475
- transactions: ['Initial deposit'],
476
- });
541
+ // Set up two bank accounts
542
+ const accountA = new ReactiveDataStore<BankAccount>({ name: 'Account A', balance: 500, transactions: [] });
543
+ const accountB = new ReactiveDataStore<BankAccount>({ name: 'Account B', balance: 200, transactions: [] });
477
544
 
545
+ // A function to transfer funds using transactions
478
546
  async function transferFunds(
479
547
  fromStore: ReactiveDataStore<BankAccount>,
480
548
  toStore: ReactiveDataStore<BankAccount>,
481
549
  amount: number,
482
550
  ) {
551
+ // All operations inside this transaction will be atomic
483
552
  await fromStore.transaction(async () => {
484
553
  console.log(`Starting transfer of ${amount}. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
485
554
 
486
555
  // Deduct from sender
487
556
  await fromStore.set((state) => {
488
557
  if (state.balance < amount) {
558
+ // Throwing an error here will cause the entire transaction to roll back
489
559
  throw new Error('Insufficient funds');
490
560
  }
491
561
  return {
492
562
  balance: state.balance - amount,
493
- transactions: [...state.transactions, `Debited ${amount}`],
563
+ transactions: [...state.transactions, `Debited ${amount} from ${state.name}`],
494
564
  };
495
565
  });
496
566
 
497
- // Simulate a delay or another async operation
567
+ // Simulate a network delay or another async operation that might fail
498
568
  await new Promise(resolve => setTimeout(resolve, 50));
499
569
 
500
570
  // Add to receiver
501
571
  await toStore.set((state) => ({
502
572
  balance: state.balance + amount,
503
- transactions: [...state.transactions, `Credited ${amount}`],
573
+ transactions: [...state.transactions, `Credited ${amount} to ${state.name}`],
504
574
  }));
505
575
 
506
- console.log(`Transfer complete. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
576
+ console.log(`Transfer in progress. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
507
577
  });
578
+ console.log(`Transfer successful. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
508
579
  }
509
580
 
510
- const accountA = new ReactiveDataStore<BankAccount>({ balance: 500, transactions: [] });
511
- const accountB = new ReactiveDataStore<BankAccount>({ balance: 200, transactions: [] });
581
+ console.log('--- Initial Balances ---');
582
+ console.log('Account A:', accountA.get().balance); // Expected: 500
583
+ console.log('Account B:', accountB.get().balance); // Expected: 200
512
584
 
513
- // Successful transfer
585
+ // --- Scenario 1: Successful transfer ---
586
+ console.log('\n--- Attempting successful transfer (100) ---');
514
587
  try {
515
588
  await transferFunds(accountA, accountB, 100);
516
- console.log('Transfer 1 successful:');
517
- console.log('Account A:', accountA.get()); // Expected: balance 400
518
- console.log('Account B:', accountB.get()); // Expected: balance 300
589
+ console.log('\nTransfer 1 successful:');
590
+ console.log('Account A:', accountA.get()); // Expected: balance 400, transactions: ['Debited 100 from Account A']
591
+ console.log('Account B:', accountB.get()); // Expected: balance 300, transactions: ['Credited 100 to Account B']
519
592
  } catch (error: any) {
520
593
  console.error('Transfer 1 failed:', error.message);
521
594
  }
522
595
 
523
- // Failed transfer (insufficient funds)
596
+ // --- Scenario 2: Failed transfer (insufficient funds) ---
597
+ console.log('\n--- Attempting failed transfer (1000) ---');
524
598
  try {
525
- await transferFunds(accountA, accountB, 1000); // Account A only has 400
599
+ // Account A only has 400 now, so this should fail
600
+ await transferFunds(accountA, accountB, 1000);
526
601
  } catch (error: any) {
527
- console.error('Transfer 2 failed:', error.message);
602
+ console.error('Transfer 2 failed as expected:', error.message);
528
603
  } finally {
529
604
  console.log('Transfer 2 attempt, state after rollback:');
530
- console.log('Account A:', accountA.get()); // Expected: balance 400 (rolled back to state before transfer attempt)
531
- console.log('Account B:', accountB.get()); // Expected: balance 300 (rolled back to state before transfer attempt)
605
+ // 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)
532
610
  }
533
611
  ```
534
612
 
535
- ### Store Observability
613
+ ### Store Observer
536
614
 
537
- The `StoreObservability` 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.
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.
538
616
 
539
617
  ```typescript
540
- import { ReactiveDataStore, StoreObservability } from '@asaidimu/utils-store';
618
+ import { ReactiveDataStore, StoreObserver, type StoreEvent, type DeepPartial } from '@asaidimu/utils-store';
541
619
 
542
620
  interface DebuggableState {
543
621
  user: { name: string; status: 'online' | 'offline' };
544
622
  messages: string[];
545
- settings: { debugMode: boolean };
623
+ settings: { debugMode: boolean; logLevel: string };
624
+ metrics: { updates: number };
546
625
  }
547
626
 
548
627
  const store = new ReactiveDataStore<DebuggableState>({
549
628
  user: { name: 'Debugger', status: 'online' },
550
629
  messages: [],
551
- settings: { debugMode: true },
630
+ settings: { debugMode: true, logLevel: 'info' },
631
+ metrics: { updates: 0 },
552
632
  });
553
633
 
554
634
  // Initialize observability for the store
555
- const observability = new StoreObservability(store, {
556
- maxEvents: 100, // Keep up to 100 events in history
557
- maxStateHistory: 10, // Keep up to 10 state snapshots for time-travel
635
+ const observer = new StoreObserver(store, {
636
+ maxEvents: 50, // Keep up to 50 events in history
637
+ maxStateHistory: 5, // Keep up to 5 state snapshots for time-travel
558
638
  enableConsoleLogging: true, // Log events to console for immediate feedback
559
639
  logEvents: {
560
640
  updates: true, // Log all update lifecycle events
@@ -567,70 +647,92 @@ const observability = new StoreObservability(store, {
567
647
  },
568
648
  });
569
649
 
650
+ // Add a simple middleware to demonstrate middleware logging
651
+ store.use({ name: 'SimpleMiddleware', action: async (state, update) => {
652
+ await new Promise(resolve => setTimeout(resolve, 10)); // Simulate work
653
+ return { metrics: { updates: state.metrics.updates + 1 } };
654
+ }});
655
+
570
656
  // Perform some state updates
571
657
  await store.set({ user: { status: 'offline' } });
572
658
  await store.set({ messages: ['Hello World!'] });
573
659
  await store.set({ settings: { debugMode: false } });
574
660
 
661
+ // Simulate a slow update
662
+ await new Promise(resolve => setTimeout(resolve, 60)); // Artificially delay
663
+ await store.set({ messages: ['Another message', 'And another'] });
664
+ // This last set will cause a console warning for "Slow update detected" if enableConsoleLogging is true.
665
+
575
666
  // 1. Get Event History
576
- console.log('\n--- Event History ---');
577
- const events = observability.getEventHistory();
667
+ console.log('\n--- Event History (Most Recent First) ---');
668
+ const events = observer.getEventHistory();
578
669
  // Events will include: update:start, update:complete (multiple times), middleware:start, middleware:complete, etc.
579
- events.forEach(event => console.log(`${event.type} at ${new Date(event.timestamp).toLocaleTimeString()}`));
670
+ events.slice(0, 5).forEach(event => console.log(`Type: ${event.type}, Data: ${JSON.stringify(event.data).substring(0, 70)}...`));
580
671
 
581
672
  // 2. Get State History
582
673
  console.log('\n--- State History (Most Recent First) ---');
583
- const stateSnapshots = observability.getStateHistory();
584
- stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}:`, snapshot));
674
+ const stateSnapshots = observer.getStateHistory();
675
+ stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}:`, snapshot.messages, snapshot.user.status));
585
676
 
586
677
  // 3. Get Recent Changes
587
- console.log('\n--- Recent State Changes (Diff) ---');
588
- const recentChanges = observability.getRecentChanges(3); // Show diffs for last 3 changes
678
+ console.log('\n--- Recent State Changes (Diffs) ---');
679
+ const recentChanges = observer.getRecentChanges(3); // Show diffs for last 3 changes
589
680
  recentChanges.forEach((change, index) => {
590
- console.log(`Change #${index}:`, {
591
- timestamp: new Date(change.timestamp).toLocaleTimeString(),
592
- changedPaths: change.changedPaths,
593
- from: change.from,
594
- to: change.to,
595
- });
681
+ console.log(`\nChange #${index}:`);
682
+ console.log(` Timestamp: ${new Date(change.timestamp).toLocaleTimeString()}`);
683
+ console.log(` Changed Paths: ${change.changedPaths.join(', ')}`);
684
+ console.log(` From:`, change.from);
685
+ console.log(` To:`, change.to);
596
686
  });
597
687
 
598
688
  // 4. Time-Travel Debugging
599
689
  console.log('\n--- Time-Travel ---');
600
- const timeTravel = observability.createTimeTravel();
690
+ const timeTravel = observer.createTimeTravel();
601
691
 
602
- await store.set({ user: { status: 'online' } }); // State 4
603
- await store.set({ messages: ['First message'] }); // State 3
604
- await store.set({ messages: ['Second message'] }); // State 2
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
605
696
 
606
- console.log('Current state (latest):', store.get().messages);
697
+ console.log('Current state (latest):', store.get().messages); // Output: ['Second message']
607
698
 
608
699
  if (timeTravel.canUndo()) {
609
- await timeTravel.undo(); // Go back to State 3
610
- console.log('After undo 1:', store.get().messages);
700
+ await timeTravel.undo(); // Go back to State B
701
+ console.log('After undo 1:', store.get().messages); // Output: ['First message']
611
702
  }
612
703
 
613
704
  if (timeTravel.canUndo()) {
614
- await timeTravel.undo(); // Go back to State 4
615
- console.log('After undo 2:', store.get().messages);
705
+ await timeTravel.undo(); // Go back to State A
706
+ console.log('After undo 2:', store.get().messages); // Output: [] (original initial state)
616
707
  }
617
708
 
618
709
  if (timeTravel.canRedo()) {
619
- await timeTravel.redo(); // Go forward to State 3
620
- console.log('After redo 1:', store.get().messages);
710
+ await timeTravel.redo(); // Go forward to State B
711
+ console.log('After redo 1:', store.get().messages); // Output: ['First message']
621
712
  }
622
713
 
623
- // 5. Custom Debugging Middleware
624
- const loggingMiddleware = observability.createLoggingMiddleware({
625
- logLevel: 'info',
714
+ console.log('Time-Travel history length:', timeTravel.getHistoryLength()); // Will be `maxStateHistory` (5 in this case) + any initial states.
715
+
716
+ // 5. Custom Debugging Middleware (provided by StoreObserver for convenience)
717
+ const loggingMiddleware = observer.createLoggingMiddleware({
718
+ logLevel: 'info', // 'debug', 'info', 'warn'
626
719
  logUpdates: true,
627
720
  });
628
- store.use({ name: 'DebugLogging', action: loggingMiddleware });
721
+ const loggingMiddlewareId = store.use({ name: 'DebugLogging', action: loggingMiddleware });
722
+
723
+ await store.set({ user: { name: 'New User Via Debug Logger' } }); // This update will be logged by the created middleware.
724
+ // Expected console output: "State Update: { user: { name: 'New User Via Debug Logger' } }"
629
725
 
630
- await store.set({ user: { name: 'New User' } }); // This update will be logged by the created middleware.
726
+ // 6. Clear history
727
+ observer.clearHistory();
728
+ console.log('\nHistory cleared. Events:', observer.getEventHistory().length, 'State snapshots:', observer.getStateHistory().length);
729
+ // Output: History cleared. Events: 0 State snapshots: 1 (keeps current state)
631
730
 
632
- // 6. Disconnect observability when no longer needed to prevent memory leaks
633
- observability.disconnect();
731
+ // 7. Disconnect observer when no longer needed to prevent memory leaks
732
+ observer.disconnect();
733
+ console.log('\nObserver disconnected. No more events or state changes will be tracked.');
734
+ // After disconnect, new updates won't be logged or tracked by Observer
735
+ await store.set({ messages: ['Final message after disconnect'] });
634
736
  ```
635
737
 
636
738
  ### Event System
@@ -642,75 +744,104 @@ import { ReactiveDataStore, StoreEvent } from '@asaidimu/utils-store';
642
744
 
643
745
  interface MyState {
644
746
  value: number;
747
+ status: string;
645
748
  }
646
749
 
647
- const store = new ReactiveDataStore<MyState>({ value: 0 });
750
+ const store = new ReactiveDataStore<MyState>({ value: 0, status: 'idle' });
648
751
 
649
752
  // Subscribe to 'update:start' event
650
753
  store.onStoreEvent('update:start', (data) => {
651
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update started.`);
754
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update started.`);
652
755
  });
653
756
 
654
757
  // Subscribe to 'update:complete' event
655
758
  store.onStoreEvent('update:complete', (data) => {
656
759
  if (data.blocked) {
657
- console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] Update blocked. Error:`, data.error?.message);
760
+ console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] Update blocked. Error:`, data.error?.message);
658
761
  } else {
659
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update complete. Changed paths: ${data.changedPaths?.join(', ')} (took ${data.duration?.toFixed(2)}ms)`);
762
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update complete. Changed paths: ${data.changedPaths?.join(', ')} (took ${data.duration?.toFixed(2)}ms)`);
660
763
  }
661
764
  });
662
765
 
663
766
  // Subscribe to middleware events
664
767
  store.onStoreEvent('middleware:start', (data) => {
665
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) started.`);
768
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) started.`);
666
769
  });
667
770
 
668
771
  store.onStoreEvent('middleware:complete', (data) => {
669
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) completed in ${data.duration?.toFixed(2)}ms.`);
772
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) completed in ${data.duration?.toFixed(2)}ms.`);
670
773
  });
671
774
 
672
775
  store.onStoreEvent('middleware:error', (data) => {
673
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" failed:`, data.error);
776
+ console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" failed:`, data.error);
777
+ });
778
+
779
+ store.onStoreEvent('middleware:blocked', (data) => {
780
+ console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] 🛑 Middleware "${data.name}" blocked an update.`);
781
+ });
782
+
783
+ store.onStoreEvent('middleware:executed', (data) => {
784
+ // This event captures detailed execution info for all middlewares
785
+ console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 📊 Middleware executed: "${data.name}" - Duration: ${data.duration?.toFixed(2)}ms, Blocked: ${data.blocked}`);
674
786
  });
675
787
 
676
788
  // Subscribe to transaction events
677
789
  store.onStoreEvent('transaction:start', (data) => {
678
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction started.`);
790
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction started.`);
679
791
  });
680
792
 
681
793
  store.onStoreEvent('transaction:complete', (data) => {
682
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction complete.`);
794
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction complete.`);
683
795
  });
684
796
 
685
797
  store.onStoreEvent('transaction:error', (data) => {
686
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction failed:`, data.error);
798
+ console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction failed:`, data.error);
687
799
  });
688
800
 
689
801
  // Subscribe to persistence events
690
802
  store.onStoreEvent('persistence:ready', (data) => {
691
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Persistence layer is ready.`);
803
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 💾 Persistence layer is ready.`);
692
804
  });
693
805
 
694
806
 
695
- // Add a middleware to demonstrate
807
+ // Add a transform middleware to demonstrate
696
808
  store.use({
697
- name: 'SampleMiddleware',
809
+ name: 'ValueIncrementMiddleware',
698
810
  action: (state, update) => {
699
811
  return { value: state.value + (update.value || 0) };
700
812
  },
701
813
  });
702
814
 
815
+ // Add a blocking middleware to demonstrate
816
+ store.use({
817
+ name: 'StatusValidationMiddleware',
818
+ block: true,
819
+ action: (state, update) => {
820
+ if (update.status === 'error' && state.value < 10) {
821
+ throw new Error('Cannot set status to error if value is too low!');
822
+ }
823
+ return true;
824
+ },
825
+ });
826
+
703
827
  // Perform operations
704
- await store.set({ value: 10 });
828
+ console.log('\n--- Perform Initial Update ---');
829
+ await store.set({ value: 5, status: 'active' }); // Will increment value by 5
705
830
 
831
+ console.log('\n--- Perform Transactional Update (Success) ---');
706
832
  await store.transaction(async () => {
707
- await store.set({ value: 5 }); // Inside transaction
708
- if (store.get().value > 15) {
709
- throw new Error('Value too high!');
710
- }
833
+ await store.set({ value: 3 }); // Inside transaction, value becomes 5 + 3 = 8
834
+ await store.set({ status: 'processing' });
711
835
  });
712
836
 
713
- console.log('Final value:', store.get().value);
837
+ console.log('\n--- Perform Update (Blocked by Middleware) ---');
838
+ try {
839
+ await store.set({ status: 'error' }); // This should be blocked by StatusValidationMiddleware (value is 8, which is < 10)
840
+ } catch (e: any) {
841
+ console.log(`Caught expected error: ${e.message}`);
842
+ }
843
+
844
+ console.log('Final value:', store.get().value, 'Final status:', store.get().status);
714
845
  ```
715
846
 
716
847
  ---
@@ -719,58 +850,47 @@ console.log('Final value:', store.get().value);
719
850
 
720
851
  The `@asaidimu/utils-store` library is built with a modular, component-based architecture to promote maintainability, testability, and extensibility.
721
852
 
722
- ```
723
- src/store/
724
- ├─── diff.ts # Efficient change detection and path derivation
725
- ├─── metrics.ts # Gathers performance metrics from store events
726
- ├─── middleware.ts # Manages and executes state transformation and blocking middlewares
727
- ├─── observability.ts # Optional module for deep debugging, state history, and time-travel
728
- ├─── persistence.ts # Handles integration with external SimplePersistence implementations
729
- ├─── state.ts # Core immutable state management and internal change notifications
730
- ├─── store.ts # The main ReactiveDataStore class, orchestrates all components
731
- ├─── transactions.ts # Provides atomic state update transactions with rollback
732
- ├─── types.ts # Central TypeScript type definitions for the entire store
733
- └─── index.ts # Export barrel file for the store module
734
- ```
735
-
736
853
  ### Core Components
737
854
 
738
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`.
739
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.
740
- * **`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.
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.
741
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.
742
- * **`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.
743
- * **`MetricsCollector` (metrics.ts)**: Observes the internal `eventBus` to gather and expose performance metrics of the store, such as update counts, listener executions, and average update times.
744
- * **`StoreObservability<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.
745
- * **`merge` (merge.ts)**: A utility function for deep merging objects, preserving immutability and handling `Symbol.for("delete")` for property removal.
746
- * **`diff` (diff.ts)**: A utility function that efficiently compares two objects (original and partial) and returns an array of paths that have changed. This is crucial for optimizing listener notifications.
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.
747
864
 
748
865
  ### Data Flow
749
866
 
750
867
  The `ReactiveDataStore` handles state updates in a robust, queued manner.
751
868
 
752
869
  1. **`store.set(update)` call**:
753
- * If an update is already in progress (`isUpdating`), the new `update` is queued in `pendingUpdates`.
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.
754
871
  * An `update:start` event is emitted.
755
872
  2. **Middleware Execution**:
756
- * The `MiddlewareEngine` first runs all `blocking` middlewares with the current state and incoming `DeepPartial` update. 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.
757
- * If not blocked, `transform` middlewares are executed sequentially. Each transform middleware can modify the update payload.
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.
758
876
  3. **State Application**:
759
- * The `CoreStateManager` receives the (potentially transformed) update.
760
- * It performs a `diff` comparison between the current state and the new state to identify all changed paths.
761
- * If changes are detected, the `CoreStateManager` updates its internal immutable state (`cache`) and then emits an `update` event for each changed path on its internal `updateBus`.
877
+ * 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`.
762
881
  4. **Listener Notification**:
763
- * Any external subscribers (from `store.subscribe()`) whose registered paths match or are parent paths of the `changedPaths` are notified with the latest state.
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`.
764
883
  5. **Persistence Handling**:
765
- * The `PersistenceHandler` receives the `changedPaths` and the new state. If a `SimplePersistence` implementation is configured, it attempts to save the new state.
884
+ * 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
+ * The `PersistenceHandler` also manages loading initial state and reacting to external state changes (e.g., from other browser tabs or processes) through `persistence.subscribe()`.
766
886
  6. **Completion & Queue Processing**:
767
887
  * An `update:complete` event is emitted, containing information about the update's duration, changed paths, and any blocking errors.
768
- * The `isUpdating` flag is reset, and if there are `pendingUpdates`, the next update in the queue is immediately processed.
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.
769
889
 
770
890
  ### Extension Points
771
891
 
772
- * **Custom Middleware**: Users can inject their own `Middleware` and `BlockingMiddleware` functions using `store.use()` to customize update logic, add logging, validation, or side effects.
773
- * **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.
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.
774
894
 
775
895
  ---
776
896
 
@@ -782,35 +902,33 @@ Contributions are welcome! Follow these guidelines to get started.
782
902
 
783
903
  1. **Clone the repository:**
784
904
  ```bash
785
- git clone https://github.com/asaidimu/utils.git
786
- cd utils
905
+ git clone https://github.com/asaidimu/erp-utils.git
906
+ cd erp-utils
787
907
  ```
788
908
  2. **Install dependencies:**
789
- The `@asaidimu/utils-store` module is part of a monorepo (likely `pnpm` workspaces).
909
+ The `@asaidimu/utils-store` module is part of a monorepo managed with `pnpm` workspaces.
790
910
  ```bash
791
911
  pnpm install
792
- # Or if you don't have pnpm:
793
- # npm install -g pnpm
794
- # pnpm install
912
+ # If you don't have pnpm installed globally: npm install -g pnpm
795
913
  ```
796
914
  3. **Build the project:**
797
- Navigate to the `store` package directory and run the build script.
915
+ Navigate to the `store` package directory and run the build script, or build the entire monorepo from the root.
798
916
  ```bash
799
- cd packages/store # (Assuming the store package is in 'packages/store')
917
+ cd src/store # (from the monorepo root)
800
918
  pnpm build # or npm run build
801
- ```
802
- Alternatively, you might be able to build the whole monorepo from the root:
803
- ```bash
804
- pnpm build # (from the monorepo root)
919
+ # Or, from the monorepo root:
920
+ # pnpm build
805
921
  ```
806
922
 
807
923
  ### Scripts
808
924
 
925
+ From the `src/store` directory:
926
+
809
927
  * `pnpm test`: Runs all unit tests using Vitest.
810
- * `pnpm test:watch`: Runs tests in watch mode.
928
+ * `pnpm test:watch`: Runs tests in watch mode for continuous development.
811
929
  * `pnpm lint`: Lints the codebase using ESLint.
812
930
  * `pnpm format`: Formats the code using Prettier.
813
- * `pnpm build`: Compiles TypeScript to JavaScript.
931
+ * `pnpm build`: Compiles TypeScript to JavaScript and generates declaration files.
814
932
 
815
933
  ### Testing
816
934
 
@@ -819,12 +937,14 @@ All tests are written with [Vitest](https://vitest.dev/).
819
937
  To run tests:
820
938
 
821
939
  ```bash
940
+ cd src/store
822
941
  pnpm test
823
942
  ```
824
943
 
825
944
  To run tests and watch for changes during development:
826
945
 
827
946
  ```bash
947
+ cd src/store
828
948
  pnpm test:watch
829
949
  ```
830
950
 
@@ -836,11 +956,11 @@ Ensure all new features have comprehensive test coverage and existing tests pass
836
956
  2. **Ensure code quality**: Write clean, readable, and maintainable code. Adhere to existing coding styles.
837
957
  3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all existing tests pass.
838
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`).
839
- 5. **Pull Requests**: Open a pull request to the `main` branch. Describe your changes clearly and link to any relevant issues.
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.
840
960
 
841
961
  ### Issue Reporting
842
962
 
843
- If you find a bug or have a feature request, please open an issue on the [GitHub repository](https://github.com/asaidimu/utils-issues).
963
+ 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).
844
964
  * For bugs, include steps to reproduce, expected behavior, and actual behavior.
845
965
  * For feature requests, describe the use case and proposed solution.
846
966
 
@@ -851,47 +971,48 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
851
971
  ### Troubleshooting
852
972
 
853
973
  * **"Update not triggering listeners"**:
854
- * Ensure you are subscribing to the correct path. `store.subscribe('user.name', ...)` will not trigger if you update `user.email`.
855
- * If the new value is strictly equal (`===`) to the old value, no change will be detected, and listeners will not be notified. The `diff` function effectively handles this.
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 `''`).
975
+ * 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.
856
976
  * Verify your `DeepPartial` update correctly targets the intended part of the state.
857
977
  * **"State not rolling back after transaction error"**:
858
- * Ensure the error is thrown *within* the `transaction` callback. Errors outside will not be caught by the transaction manager.
859
- * Promises within the transaction *must* be `await`ed so the transaction manager can capture potential rejections.
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.
860
980
  * **"Middleware not being applied"**:
861
981
  * Verify the middleware is registered with `store.use()` *before* the `set` operation it should affect.
862
- * Check middleware `name` if debugging.
863
- * Ensure your middleware returns `DeepPartial<T>` or `void`/`Promise<void>` for transform middleware, and `boolean`/`Promise<boolean>` for blocking middleware.
982
+ * Check middleware `name` in console logs (if `StoreObserver` is enabled) to ensure it's being hit.
983
+ * Ensure your `transform` middleware returns a `DeepPartial<T>` or `void`/`Promise<void>`, and `blocking` middleware returns `boolean`/`Promise<boolean>`.
864
984
  * **"Performance warnings in console"**:
865
- * If `StoreObservability` is enabled, it will warn about updates or middlewares exceeding defined `performanceThresholds`. This is a warning, not an error. Consider optimizing the specific updates or middlewares identified.
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.
866
986
 
867
987
  ### FAQ
868
988
 
869
989
  **Q: How are arrays handled during updates?**
870
- A: Arrays are treated as primitive values. When you provide an array in a `DeepPartial` update, the entire array at that path is replaced, not merged. This ensures predictable behavior and prevents complex partial array merging logic.
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.
871
991
 
872
992
  **Q: What is `Symbol.for("delete")`?**
873
- A: It's a special 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 state.
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.
874
994
 
875
995
  **Q: How do I debug my store's state changes?**
876
- A: The `StoreObservability` 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 `StoreObservability` options provides immediate, formatted console output.
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.
877
997
 
878
998
  **Q: What is `SimplePersistence<T>`?**
879
- 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.
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.
880
1000
 
881
1001
  **Q: Can I use this with React/Vue/Angular?**
882
- 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) to react to state changes and update your UI components.
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.
883
1003
 
884
1004
  ### Changelog / Roadmap
885
1005
 
886
- * **Changelog**: For detailed version history, please refer to the [CHANGELOG.md](CHANGELOG.md) file.
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.
887
1007
  * **Roadmap**: Future plans may include:
888
- * Plugins for common integrations (e.g., React hooks, Redux DevTools extension compatibility).
889
- * More advanced query/selector capabilities.
890
- * Built-in serialization options for persistence.
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).
891
1012
 
892
1013
  ### License
893
1014
 
894
- This project is licensed under the [MIT License](LICENSE).
1015
+ This project is licensed under the [MIT License](https://github.com/asaidimu/erp-utils/blob/main/LICENSE).
895
1016
 
896
1017
  ### Acknowledgments
897
1018