@asaidimu/utils-store 9.0.1 → 9.0.3

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
@@ -38,26 +38,26 @@ This library offers robust tools to handle your state with confidence, enabling
38
38
 
39
39
  ### Key Features
40
40
 
41
- * 📊 **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.
42
- * 🔄 **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.
43
- * 🚀 **Action System**: Dispatch named actions to encapsulate state updates, improving code organization and enabling detailed logging and observability for each logical operation, including debouncing capabilities.
44
- * 🔍 **Reactive Selectors with Memoization**: Efficiently derive computed state from your store using reactive selectors that re-evaluate and notify subscribers only when their accessed paths actually change, preventing unnecessary renders.
45
- * 🧠 **Composable Middleware System**:
46
- * **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.
47
- * **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.
48
- * 📦 **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.
49
- * 💾 **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, handling persistence queueing and retries.
50
- * 🧩 **Artifacts (Dependency Injection)**: A flexible dependency injection system to manage services, utilities, or complex objects that your actions and other artifacts depend on. Supports `Singleton` (re-evaluated on dependencies change) and `Transient` (new instance every time) scopes, and reactive resolution using `use(({select, resolve}) => ...)` context. Handles dependency graphs and circular dependency detection.
51
- * 👀 **Deep Observer & Debugging (`StoreObserver`)**: An optional but highly recommended class for unparalleled runtime introspection and debugging:
52
- * **Comprehensive Event History**: Captures a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, `middleware:executed`, `action:start`, `selector:accessed`, etc.).
53
- * **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.
54
- * **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.
55
- * **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.
56
- * **Configurable Console Logging**: Provides human-readable, color-coded logging of store events directly to the browser console for immediate feedback during development.
57
- * **Pre-built Debugging Middlewares**: Includes helper methods to easily create a generic logging middleware and a validation middleware for immediate use.
58
- * **Session Management**: Save and load observer sessions for offline analysis or sharing bug reproductions.
59
- * 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using the global `Symbol.for("delete")` or a custom marker.
60
- * ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in a predictable, sequential order.
41
+ - 📊 **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.
42
+ - 🔄 **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.
43
+ - 🚀 **Action System**: Dispatch named actions to encapsulate state updates, improving code organization and enabling detailed logging and observability for each logical operation, including debouncing capabilities.
44
+ - 🔍 **Reactive Selectors with Memoization**: Efficiently derive computed state from your store using reactive selectors that re-evaluate and notify subscribers only when their accessed paths actually change, preventing unnecessary renders.
45
+ - 🧠 **Composable Middleware System**:
46
+ - **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.
47
+ - **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.
48
+ - 📦 **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.
49
+ - 💾 **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, handling persistence queueing and retries.
50
+ - 🧩 **Artifacts (Dependency Injection)**: A flexible dependency injection system to manage services, utilities, or complex objects that your actions and other artifacts depend on. Supports `Singleton` (re-evaluated on dependencies change) and `Transient` (new instance every time) scopes, and reactive resolution using `use(({select, resolve}) => ...)` context. Handles dependency graphs and circular dependency detection.
51
+ - 👀 **Deep Observer & Debugging (`StoreObserver`)**: An optional but highly recommended class for unparalleled runtime introspection and debugging:
52
+ - **Comprehensive Event History**: Captures a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, `middleware:executed`, `action:start`, `selector:accessed`, etc.).
53
+ - **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.
54
+ - **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.
55
+ - **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.
56
+ - **Configurable Console Logging**: Provides human-readable, color-coded logging of store events directly to the browser console for immediate feedback during development.
57
+ - **Pre-built Debugging Middlewares**: Includes helper methods to easily create a generic logging middleware and a validation middleware for immediate use.
58
+ - **Session Management**: Save and load observer sessions for offline analysis or sharing bug reproductions.
59
+ - 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using the global `Symbol.for("delete")` or a custom marker.
60
+ - ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in a predictable, sequential order.
61
61
 
62
62
  ---
63
63
 
@@ -78,8 +78,8 @@ yarn add @asaidimu/utils-store
78
78
 
79
79
  ### Prerequisites
80
80
 
81
- * **Node.js**: (LTS version recommended) for development and compilation.
82
- * **TypeScript**: (v4.0+ recommended) for full type-safety during development. Modern TS features (ES2017+ for `async/await`, ES2020+ for `Symbol.for()` and `structuredClone()`) are utilized. `moduleResolution`: `Node16` or `Bundler` is recommended in `tsconfig.json`.
81
+ - **Node.js**: (LTS version recommended) for development and compilation.
82
+ - **TypeScript**: (v4.0+ recommended) for full type-safety during development. Modern TS features (ES2017+ for `async/await`, ES2020+ for `Symbol.for()` and `structuredClone()`) are utilized. `moduleResolution`: `Node16` or `Bundler` is recommended in `tsconfig.json`.
83
83
 
84
84
  ### Verification
85
85
 
@@ -87,7 +87,7 @@ To verify that the library is installed and working correctly, create a small Ty
87
87
 
88
88
  ```typescript
89
89
  // verify.ts
90
- import { ReactiveDataStore } from '@asaidimu/utils-store';
90
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
91
91
 
92
92
  interface MyState {
93
93
  count: number;
@@ -103,7 +103,7 @@ store.watch("count", (state) => {
103
103
 
104
104
  // Subscribing to "" (empty string) will log for any store update
105
105
  store.watch("", (state) => {
106
- console.log(`Store updated to: ${JSON.stringify(state)}`);
106
+ console.log(`Store updated to: ${JSON.stringify(state)}`);
107
107
  });
108
108
 
109
109
  console.log("Initial state:", store.get());
@@ -140,7 +140,11 @@ This section provides practical examples and detailed explanations of how to use
140
140
  Learn how to create a store, read state, and update state with partial objects or functions.
141
141
 
142
142
  ```typescript
143
- import { ReactiveDataStore, DELETE_SYMBOL, type DeepPartial } from '@asaidimu/utils-store';
143
+ import {
144
+ ReactiveDataStore,
145
+ DELETE_SYMBOL,
146
+ type DeepPartial,
147
+ } from "@asaidimu/utils-store";
144
148
 
145
149
  // 1. Define your state interface for type safety
146
150
  interface AppState {
@@ -152,7 +156,7 @@ interface AppState {
152
156
  };
153
157
  products: Array<{ id: string; name: string; price: number }>;
154
158
  settings: {
155
- theme: 'light' | 'dark';
159
+ theme: "light" | "dark";
156
160
  notificationsEnabled: boolean;
157
161
  };
158
162
  lastUpdated: number;
@@ -161,17 +165,17 @@ interface AppState {
161
165
  // 2. Initialize the store with an initial state
162
166
  const initialState: AppState = {
163
167
  user: {
164
- id: '123',
165
- name: 'Jane Doe',
166
- email: 'jane@example.com',
168
+ id: "123",
169
+ name: "Jane Doe",
170
+ email: "jane@example.com",
167
171
  isActive: true,
168
172
  },
169
173
  products: [
170
- { id: 'p1', name: 'Laptop', price: 1200 },
171
- { id: 'p2', name: 'Mouse', price: 25 },
174
+ { id: "p1", name: "Laptop", price: 1200 },
175
+ { id: "p2", name: "Mouse", price: 25 },
172
176
  ],
173
177
  settings: {
174
- theme: 'light',
178
+ theme: "light",
175
179
  notificationsEnabled: true,
176
180
  },
177
181
  lastUpdated: Date.now(),
@@ -183,7 +187,7 @@ const store = new ReactiveDataStore<AppState>(initialState);
183
187
  // `store.get()` returns a reference to the internal state.
184
188
  // Use `store.get(true)` to get a deep clone, ensuring immutability if you modify it directly.
185
189
  const currentState = store.get();
186
- console.log('Initial state:', currentState);
190
+ console.log("Initial state:", currentState);
187
191
  /* Output:
188
192
  Initial state: {
189
193
  user: { id: '123', name: 'Jane Doe', email: 'jane@example.com', isActive: true },
@@ -197,15 +201,15 @@ Initial state: {
197
201
  // You can update deeply nested properties without affecting siblings.
198
202
  await store.set({
199
203
  user: {
200
- name: 'Jane Smith', // Changes user's name
201
- isActive: false, // Changes user's active status
204
+ name: "Jane Smith", // Changes user's name
205
+ isActive: false, // Changes user's active status
202
206
  },
203
207
  settings: {
204
- theme: 'dark', // Changes theme
208
+ theme: "dark", // Changes theme
205
209
  },
206
210
  });
207
211
 
208
- console.log('State after partial update:', store.get());
212
+ console.log("State after partial update:", store.get());
209
213
  /* Output:
210
214
  State after partial update: {
211
215
  user: { id: '123', name: 'Jane Smith', email: 'jane@example.com', isActive: false },
@@ -220,35 +224,51 @@ State after partial update: {
220
224
  await store.set((state) => ({
221
225
  products: [
222
226
  ...state.products, // Keep existing products
223
- { id: 'p3', name: 'Keyboard', price: 75 }, // Add a new product
227
+ { id: "p3", name: "Keyboard", price: 75 }, // Add a new product
224
228
  ],
225
229
  lastUpdated: Date.now(), // Update timestamp
226
230
  }));
227
231
 
228
- console.log('State after functional update, products count:', store.get().products.length);
232
+ console.log(
233
+ "State after functional update, products count:",
234
+ store.get().products.length,
235
+ );
229
236
  // Output: State after functional update, products count: 3
230
237
 
231
238
  // 6. Subscribing to state changes
232
239
  // You can subscribe to the entire state (path: '') or specific paths (e.g., 'user.name', 'settings.notificationsEnabled').
233
- const unsubscribeUser = store.watch('user', (state) => {
234
- console.log('User data changed:', state.user);
240
+ const unsubscribeUser = store.watch("user", (state) => {
241
+ console.log("User data changed:", state.user);
235
242
  });
236
243
 
237
- const unsubscribeNotifications = store.watch('settings.notificationsEnabled', (state) => {
238
- console.log('Notifications setting changed:', state.settings.notificationsEnabled);
239
- });
244
+ const unsubscribeNotifications = store.watch(
245
+ "settings.notificationsEnabled",
246
+ (state) => {
247
+ console.log(
248
+ "Notifications setting changed:",
249
+ state.settings.notificationsEnabled,
250
+ );
251
+ },
252
+ );
240
253
 
241
254
  // Subscribe to multiple paths at once
242
- const unsubscribeMulti = store.watch(['user.name', 'products'], (state) => {
243
- console.log('User name or products changed:', state.user.name, state.products.length);
255
+ const unsubscribeMulti = store.watch(["user.name", "products"], (state) => {
256
+ console.log(
257
+ "User name or products changed:",
258
+ state.user.name,
259
+ state.products.length,
260
+ );
244
261
  });
245
262
 
246
263
  // Subscribe to any change in the store (root listener)
247
- const unsubscribeAll = store.watch('', (state) => {
248
- console.log('Store updated (any path changed). Current products count:', state.products.length);
264
+ const unsubscribeAll = store.watch("", (state) => {
265
+ console.log(
266
+ "Store updated (any path changed). Current products count:",
267
+ state.products.length,
268
+ );
249
269
  });
250
270
 
251
- await store.set({ user: { email: 'jane.smith@example.com' } });
271
+ await store.set({ user: { email: "jane.smith@example.com" } });
252
272
  /* Output (order may vary slightly depending on async operations):
253
273
  User data changed: { id: '123', name: 'Jane Smith', email: 'jane.smith@example.com', isActive: false }
254
274
  User name or products changed: Jane Smith 3
@@ -274,10 +294,10 @@ await store.set({ user: { isActive: true } });
274
294
  // Use `DELETE_SYMBOL` (exported from the library) to explicitly remove a property from the state.
275
295
  await store.set({
276
296
  user: {
277
- email: DELETE_SYMBOL as DeepPartial<string> // Type cast is needed for strict TypeScript environments
278
- }
297
+ email: DELETE_SYMBOL as DeepPartial<string>, // Type cast is needed for strict TypeScript environments
298
+ },
279
299
  });
280
- console.log('User email after deletion:', store.get().user.email);
300
+ console.log("User email after deletion:", store.get().user.email);
281
301
  // Output: User email after deletion: undefined
282
302
  ```
283
303
 
@@ -286,7 +306,7 @@ console.log('User email after deletion:', store.get().user.email);
286
306
  The `dispatch` method allows you to encapsulate state updates into named actions. This improves code organization, provides clear semantic meaning to updates, and enables detailed logging and observability through `StoreObserver` and the event system. Actions can also be debounced to prevent excessive updates.
287
307
 
288
308
  ```typescript
289
- import { ReactiveDataStore } from '@asaidimu/utils-store';
309
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
290
310
 
291
311
  interface CounterState {
292
312
  value: number;
@@ -297,34 +317,42 @@ const store = new ReactiveDataStore<CounterState>({ value: 0, history: [] });
297
317
 
298
318
  // 1. Register an action
299
319
  store.register({
300
- name: 'incrementCounter',
301
- fn: (state, amount: number) => ({ // Action function receives state and dispatched parameters
320
+ name: "incrementCounter",
321
+ fn: (state, amount: number) => ({
322
+ // Action function receives state and dispatched parameters
302
323
  value: state.value + amount,
303
- history: [...state.history, `Incremented by ${amount} at ${new Date().toLocaleTimeString()}`],
324
+ history: [
325
+ ...state.history,
326
+ `Incremented by ${amount} at ${new Date().toLocaleTimeString()}`,
327
+ ],
304
328
  }),
305
329
  });
306
330
 
307
331
  // 2. Register an action with debounce
308
332
  store.register({
309
- name: 'incrementCounterDebounced',
333
+ name: "incrementCounterDebounced",
310
334
  fn: (state, amount: number) => ({
311
335
  value: state.value + amount,
312
- history: [...state.history, `Debounced Increment by ${amount} at ${new Date().toLocaleTimeString()}`],
336
+ history: [
337
+ ...state.history,
338
+ `Debounced Increment by ${amount} at ${new Date().toLocaleTimeString()}`,
339
+ ],
313
340
  }),
314
341
  debounce: {
315
342
  delay: 100, // Debounce for 100ms
316
343
  // Optional: condition to decide whether to debounce.
317
344
  // Here, we always debounce if `amount` is different from previous.
318
- condition: (previousArgs, currentArgs) => previousArgs?.[0] !== currentArgs[0],
319
- }
345
+ condition: (previousArgs, currentArgs) =>
346
+ previousArgs?.[0] !== currentArgs[0],
347
+ },
320
348
  });
321
349
 
322
350
  // 3. Register an async action
323
351
  store.register({
324
- name: 'loadInitialValue',
352
+ name: "loadInitialValue",
325
353
  fn: async (state, userId: string) => {
326
354
  console.log(`Simulating loading initial value for user: ${userId}`);
327
- await new Promise(resolve => setTimeout(resolve, 200)); // Simulate API call
355
+ await new Promise((resolve) => setTimeout(resolve, 200)); // Simulate API call
328
356
  return {
329
357
  value: 100, // Fetched value
330
358
  history: [...state.history, `Loaded initial value for ${userId}`],
@@ -333,41 +361,52 @@ store.register({
333
361
  });
334
362
 
335
363
  // 4. Dispatch actions
336
- console.log('Initial value:', store.get().value); // 0
364
+ console.log("Initial value:", store.get().value); // 0
337
365
 
338
- await store.dispatch('incrementCounter', 5);
339
- console.log('Value after incrementCounter(5):', store.get().value); // 5
366
+ await store.dispatch("incrementCounter", 5);
367
+ console.log("Value after incrementCounter(5):", store.get().value); // 5
340
368
 
341
- await store.dispatch('incrementCounter', 10);
342
- console.log('Value after incrementCounter(10):', store.get().value); // 15
369
+ await store.dispatch("incrementCounter", 10);
370
+ console.log("Value after incrementCounter(10):", store.get().value); // 15
343
371
 
344
372
  // Dispatch debounced actions multiple times quickly
345
- store.dispatch('incrementCounterDebounced', 1);
346
- store.dispatch('incrementCounterDebounced', 2);
347
- store.dispatch('incrementCounterDebounced', 3);
348
- console.log('Value after immediate debounced dispatches (still 15, waiting for debounce):', store.get().value);
373
+ store.dispatch("incrementCounterDebounced", 1);
374
+ store.dispatch("incrementCounterDebounced", 2);
375
+ store.dispatch("incrementCounterDebounced", 3);
376
+ console.log(
377
+ "Value after immediate debounced dispatches (still 15, waiting for debounce):",
378
+ store.get().value,
379
+ );
349
380
 
350
381
  // Wait for the debounce to complete
351
- await new Promise(resolve => setTimeout(150, resolve));
352
- console.log('Value after debounced dispatches settled (only last one applied):', store.get().value); // 15 + 3 = 18
382
+ await new Promise((resolve) => setTimeout(150, resolve));
383
+ console.log(
384
+ "Value after debounced dispatches settled (only last one applied):",
385
+ store.get().value,
386
+ ); // 15 + 3 = 18
353
387
 
354
388
  // Dispatch an async action
355
- await store.dispatch('loadInitialValue', 'user-abc');
356
- console.log('Value after loadInitialValue:', store.get().value); // 100 (overwritten by fetched value)
357
- console.log('History:', store.get().history);
389
+ await store.dispatch("loadInitialValue", "user-abc");
390
+ console.log("Value after loadInitialValue:", store.get().value); // 100 (overwritten by fetched value)
391
+ console.log("History:", store.get().history);
358
392
 
359
393
  // 5. Deregister an action
360
394
  const deregisterRisky = store.register({
361
- name: 'riskyAction',
362
- fn: () => { throw new Error('Risky action!'); }
395
+ name: "riskyAction",
396
+ fn: () => {
397
+ throw new Error("Risky action!");
398
+ },
363
399
  });
364
400
 
365
401
  deregisterRisky(); // Action is now removed
366
402
 
367
403
  try {
368
- await store.dispatch('riskyAction');
404
+ await store.dispatch("riskyAction");
369
405
  } catch (error: any) {
370
- console.error('Expected error when dispatching deregistered action:', error.message);
406
+ console.error(
407
+ "Expected error when dispatching deregistered action:",
408
+ error.message,
409
+ );
371
410
  }
372
411
  ```
373
412
 
@@ -376,7 +415,7 @@ try {
376
415
  Reactive selectors provide an efficient way to derive computed data from your store. They automatically track their dependencies and will only re-evaluate and notify subscribers if the underlying data they access changes. This prevents unnecessary re-renders in UI frameworks.
377
416
 
378
417
  ```typescript
379
- import { ReactiveDataStore } from '@asaidimu/utils-store';
418
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
380
419
 
381
420
  interface UserProfile {
382
421
  firstName: string;
@@ -391,22 +430,26 @@ interface UserProfile {
391
430
  }
392
431
 
393
432
  const store = new ReactiveDataStore<UserProfile>({
394
- firstName: 'John',
395
- lastName: 'Doe',
396
- email: 'john.doe@example.com',
433
+ firstName: "John",
434
+ lastName: "Doe",
435
+ email: "john.doe@example.com",
397
436
  address: {
398
- city: 'New York',
399
- zipCode: '10001',
437
+ city: "New York",
438
+ zipCode: "10001",
400
439
  },
401
440
  isActive: true,
402
- friends: ['Alice', 'Bob'],
441
+ friends: ["Alice", "Bob"],
403
442
  });
404
443
 
405
444
  // Create a reactive selector for the full name
406
- const selectFullName = store.select((state) => `${state.firstName} ${state.lastName}`);
445
+ const selectFullName = store.select(
446
+ (state) => `${state.firstName} ${state.lastName}`,
447
+ );
407
448
 
408
449
  // Create a reactive selector for user's location
409
- const selectLocation = store.select((state) => `${state.address.city}, ${state.address.zipCode}`);
450
+ const selectLocation = store.select(
451
+ (state) => `${state.address.city}, ${state.address.zipCode}`,
452
+ );
410
453
 
411
454
  // Create a reactive selector for user's active status
412
455
  const selectIsActive = store.select((state) => state.isActive);
@@ -414,73 +457,71 @@ const selectIsActive = store.select((state) => state.isActive);
414
457
  // Create a reactive selector for the number of friends
415
458
  const selectFriendCount = store.select((state) => state.friends.length);
416
459
 
417
- let lastFullName = '';
418
- let lastLocation = '';
460
+ let lastFullName = "";
461
+ let lastLocation = "";
419
462
  let lastIsActive = false;
420
463
  let lastFriendCount = 0;
421
464
 
422
465
  // Subscribe to the full name selector
423
466
  const unsubscribeFullName = selectFullName.subscribe((fullName) => {
424
467
  lastFullName = fullName;
425
- console.log('Full Name Changed:', lastFullName);
468
+ console.log("Full Name Changed:", lastFullName);
426
469
  });
427
470
 
428
471
  // Subscribe to the location selector
429
472
  const unsubscribeLocation = selectLocation.subscribe((location) => {
430
473
  lastLocation = location;
431
- console.log('Location Changed:', lastLocation);
474
+ console.log("Location Changed:", lastLocation);
432
475
  });
433
476
 
434
477
  // Subscribe to the active status selector
435
478
  const unsubscribeIsActive = selectIsActive.subscribe((isActive) => {
436
479
  lastIsActive = isActive;
437
- console.log('Is Active Changed:', lastIsActive);
480
+ console.log("Is Active Changed:", lastIsActive);
438
481
  });
439
482
 
440
483
  // Subscribe to the friend count selector
441
484
  const unsubscribeFriendCount = selectFriendCount.subscribe((count) => {
442
485
  lastFriendCount = count;
443
- console.log('Friend Count Changed:', lastFriendCount);
486
+ console.log("Friend Count Changed:", lastFriendCount);
444
487
  });
445
488
 
446
-
447
489
  // Get initial values
448
490
  lastFullName = selectFullName.get();
449
491
  lastLocation = selectLocation.get();
450
492
  lastIsActive = selectIsActive.get();
451
493
  lastFriendCount = selectFriendCount.get();
452
- console.log('Initial Full Name:', lastFullName);
453
- console.log('Initial Location:', lastLocation);
454
- console.log('Initial Is Active:', lastIsActive);
455
- console.log('Initial Friend Count:', lastFriendCount);
456
-
494
+ console.log("Initial Full Name:", lastFullName);
495
+ console.log("Initial Location:", lastLocation);
496
+ console.log("Initial Is Active:", lastIsActive);
497
+ console.log("Initial Friend Count:", lastFriendCount);
457
498
 
458
- console.log('\n--- Performing Updates ---');
499
+ console.log("\n--- Performing Updates ---");
459
500
 
460
501
  // Update first name (should trigger selectFullName)
461
- await store.set({ firstName: 'Jane' });
502
+ await store.set({ firstName: "Jane" });
462
503
  // Output: Full Name Changed: Jane Doe
463
504
 
464
- await store.set({ lastName: 'Smith' }); // Should trigger selectFullName
505
+ await store.set({ lastName: "Smith" }); // Should trigger selectFullName
465
506
  // Output: Full Name Changed: Jane Smith
466
507
 
467
- await store.set({ address: { city: 'Los Angeles' } }); // Should trigger selectLocation
508
+ await store.set({ address: { city: "Los Angeles" } }); // Should trigger selectLocation
468
509
  // Output: Location Changed: Los Angeles, 10001
469
510
 
470
511
  await store.set({ isActive: false }); // Should trigger selectIsActive
471
512
  // Output: Is Active Changed: false
472
513
 
473
- await store.set({ email: 'jane.smith@example.com' }); // Should NOT trigger any of the above selectors directly
514
+ await store.set({ email: "jane.smith@example.com" }); // Should NOT trigger any of the above selectors directly
474
515
  // No output from subscribed selectors
475
516
 
476
- await store.set((state) => ({ friends: [...state.friends, 'Charlie'] })); // Should trigger selectFriendCount
517
+ await store.set((state) => ({ friends: [...state.friends, "Charlie"] })); // Should trigger selectFriendCount
477
518
  // Output: Friend Count Changed: 3
478
519
 
479
- console.log('\n--- Current Values ---');
480
- console.log('Current Full Name:', selectFullName.get());
481
- console.log('Current Location:', selectLocation.get());
482
- console.log('Current Is Active:', selectIsActive.get());
483
- console.log('Current Friend Count:', selectFriendCount.get());
520
+ console.log("\n--- Current Values ---");
521
+ console.log("Current Full Name:", selectFullName.get());
522
+ console.log("Current Location:", selectLocation.get());
523
+ console.log("Current Is Active:", selectIsActive.get());
524
+ console.log("Current Friend Count:", selectFriendCount.get());
484
525
 
485
526
  // Cleanup
486
527
  unsubscribeFullName();
@@ -494,8 +535,8 @@ unsubscribeFriendCount();
494
535
  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. You can use `@asaidimu/utils-persistence` for concrete implementations or provide your own.
495
536
 
496
537
  ```typescript
497
- import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
498
- import { v4 as uuidv4 } from 'uuid'; // For generating unique instance IDs
538
+ import { ReactiveDataStore, type StoreEvent } from "@asaidimu/utils-store";
539
+ import { v4 as uuidv4 } from "uuid"; // For generating unique instance IDs
499
540
 
500
541
  // Define the SimplePersistence interface (from `@asaidimu/utils-persistence` or your own)
501
542
  interface SimplePersistence<T extends object> {
@@ -514,17 +555,20 @@ class InMemoryPersistence<T extends object> implements SimplePersistence<T> {
514
555
  private uniqueStoreId: string; // Acts as the storageKey/store identifier
515
556
 
516
557
  constructor(uniqueStoreId: string, initialData: T | null = null) {
517
- this.uniqueStoreId = uniqueStoreId;
518
- this.data = initialData;
558
+ this.uniqueStoreId = uniqueStoreId;
559
+ this.data = initialData;
519
560
  }
520
561
 
521
- get(): T | null { // Synchronous get for simplicity
562
+ get(): T | null {
563
+ // Synchronous get for simplicity
522
564
  console.log(`Persistence [${this.uniqueStoreId}]: Loading state...`);
523
565
  return this.data ? structuredClone(this.data) : null;
524
566
  }
525
567
 
526
568
  async set(instanceId: string, state: T): Promise<boolean> {
527
- console.log(`Persistence [${this.uniqueStoreId}]: Saving state for instance ${instanceId}...`);
569
+ console.log(
570
+ `Persistence [${this.uniqueStoreId}]: Saving state for instance ${instanceId}...`,
571
+ );
528
572
  this.data = structuredClone(state); // Store a clone
529
573
  // Simulate external change notification for *other* instances
530
574
  this.subscribers.forEach((callback, subId) => {
@@ -538,62 +582,71 @@ class InMemoryPersistence<T extends object> implements SimplePersistence<T> {
538
582
  }
539
583
 
540
584
  subscribe(instanceId: string, callback: (state: T) => void): () => void {
541
- console.log(`Persistence [${this.uniqueStoreId}]: Subscribing to external changes for instance ${instanceId}`);
585
+ console.log(
586
+ `Persistence [${this.uniqueStoreId}]: Subscribing to external changes for instance ${instanceId}`,
587
+ );
542
588
  this.subscribers.set(instanceId, callback);
543
589
  return () => {
544
- console.log(`Persistence [${this.uniqueStoreId}]: Unsubscribing for instance ${instanceId}`);
590
+ console.log(
591
+ `Persistence [${this.uniqueStoreId}]: Unsubscribing for instance ${instanceId}`,
592
+ );
545
593
  this.subscribers.delete(instanceId);
546
594
  };
547
595
  }
548
596
  }
549
597
 
550
598
  interface UserConfig {
551
- theme: 'light' | 'dark' | 'system';
599
+ theme: "light" | "dark" | "system";
552
600
  fontSize: number;
553
601
  }
554
602
 
555
603
  // Create a persistence instance, possibly with some pre-existing data
556
604
  // The 'my-user-config' string acts as the unique identifier for this particular data set in persistence
557
- const userConfigPersistence = new InMemoryPersistence<UserConfig>('my-user-config', { theme: 'dark', fontSize: 18 });
605
+ const userConfigPersistence = new InMemoryPersistence<UserConfig>(
606
+ "my-user-config",
607
+ { theme: "dark", fontSize: 18 },
608
+ );
558
609
 
559
610
  // Initialize the store with persistence
560
611
  const store = new ReactiveDataStore<UserConfig>(
561
- { theme: 'light', fontSize: 16 }, // Initial state if no persisted data found (or if persistence is not used)
612
+ { theme: "light", fontSize: 16 }, // Initial state if no persisted data found (or if persistence is not used)
562
613
  userConfigPersistence, // Pass your persistence implementation here
563
614
  // You can also pass persistence options like retries and delay
564
615
  undefined, // Use default DELETE_SYMBOL
565
- { persistenceMaxRetries: 5, persistenceRetryDelay: 2000 }
616
+ { persistenceMaxRetries: 5, persistenceRetryDelay: 2000 },
566
617
  );
567
618
 
568
619
  // Optionally, listen for persistence readiness (important for UIs that depend on loaded state)
569
- const storeReadyPromise = new Promise<void>(resolve => {
570
- store.on('persistence:ready', (data) => {
571
- console.log(`Store is ready and persistence is initialized! Timestamp: ${new Date(data.timestamp).toLocaleTimeString()}`);
620
+ const storeReadyPromise = new Promise<void>((resolve) => {
621
+ store.on("persistence:ready", (data) => {
622
+ console.log(
623
+ `Store is ready and persistence is initialized! Timestamp: ${new Date(data.timestamp).toLocaleTimeString()}`,
624
+ );
572
625
  resolve();
573
626
  });
574
627
  });
575
628
 
576
- console.log('Store initial state (before persistence loads):', store.get());
629
+ console.log("Store initial state (before persistence loads):", store.get());
577
630
  // Output: Store initial state (before persistence loads): { theme: 'light', fontSize: 16 } (initial state provided to constructor)
578
631
 
579
632
  await storeReadyPromise; // Wait for persistence to load/initialize
580
633
 
581
634
  // Now, store.get() will reflect the loaded state from persistence
582
- console.log('Store state after persistence load:', store.get());
635
+ console.log("Store state after persistence load:", store.get());
583
636
  // Output: Store state after persistence load: { theme: 'dark', fontSize: 18 } (from InMemoryPersistence)
584
637
 
585
638
  // Now update the state, which will trigger persistence.set()
586
- await store.set({ theme: 'light' });
587
- console.log('Current theme:', store.get().theme);
639
+ await store.set({ theme: "light" });
640
+ console.log("Current theme:", store.get().theme);
588
641
  // Output: Current theme: light
589
642
  // Persistence: Saving state for instance <uuid>...
590
643
 
591
644
  // Simulate an external change (e.g., another tab or process updating the state)
592
645
  // Note: The `instanceId` here should be different from the store's `store.id()`
593
646
  // to simulate an external change and trigger the store's internal persistence subscription.
594
- await userConfigPersistence.set(uuidv4(), { theme: 'system', fontSize: 20 });
647
+ await userConfigPersistence.set(uuidv4(), { theme: "system", fontSize: 20 });
595
648
  // The store will automatically update its state and notify its listeners due to the internal subscription.
596
- console.log('Current theme after external update:', store.get().theme);
649
+ console.log("Current theme after external update:", store.get().theme);
597
650
  // Output: Current theme after external update: system
598
651
  ```
599
652
 
@@ -606,7 +659,7 @@ Middleware functions allow you to intercept and modify state updates or prevent
606
659
  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.
607
660
 
608
661
  ```typescript
609
- import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
662
+ import { ReactiveDataStore, type DeepPartial } from "@asaidimu/utils-store";
610
663
 
611
664
  interface MyState {
612
665
  counter: number;
@@ -625,16 +678,16 @@ const store = new ReactiveDataStore<MyState>({
625
678
  // Middleware 1: Logger
626
679
  // Logs the incoming update before it's processed. Does not return anything (void), so it doesn't modify the update.
627
680
  store.use({
628
- name: 'LoggerMiddleware',
681
+ name: "LoggerMiddleware",
629
682
  action: (state, update) => {
630
- console.log('Middleware: Incoming update:', update);
683
+ console.log("Middleware: Incoming update:", update);
631
684
  },
632
685
  });
633
686
 
634
687
  // Middleware 2: Timestamp and Action Tracker
635
688
  // Modifies the update to add a timestamp and track the last action. Returns a partial state that gets merged.
636
689
  store.use({
637
- name: 'TimestampActionMiddleware',
690
+ name: "TimestampActionMiddleware",
638
691
  action: (state, update) => {
639
692
  const actionDescription = JSON.stringify(update);
640
693
  return {
@@ -647,7 +700,7 @@ store.use({
647
700
  // Middleware 3: Version Incrementor
648
701
  // Increments a version counter for every successful update.
649
702
  store.use({
650
- name: 'VersionIncrementMiddleware',
703
+ name: "VersionIncrementMiddleware",
651
704
  action: (state) => {
652
705
  return { version: state.version + 1 };
653
706
  },
@@ -657,10 +710,10 @@ store.use({
657
710
  // This middleware intercepts updates to 'counter' and increments it by the value provided,
658
711
  // instead of setting it directly.
659
712
  store.use({
660
- name: 'CounterIncrementMiddleware',
713
+ name: "CounterIncrementMiddleware",
661
714
  action: (state, update) => {
662
715
  // Only apply if the incoming update is a number for 'counter'
663
- if (typeof update.counter === 'number') {
716
+ if (typeof update.counter === "number") {
664
717
  return { counter: state.counter + update.counter };
665
718
  }
666
719
  // Return the original update or undefined if no transformation is needed for other paths
@@ -672,7 +725,7 @@ await store.set({ counter: 5 }); // Will increment counter by 5, not set to 5
672
725
  /* Expected console output from LoggerMiddleware:
673
726
  Middleware: Incoming update: { counter: 5 }
674
727
  */
675
- console.log('State after counter set:', store.get());
728
+ console.log("State after counter set:", store.get());
676
729
  /* Output will show:
677
730
  counter: 0 (initial) + 5 (update) = 5 (due to CounterIncrementMiddleware)
678
731
  lastAction updated by TimestampActionMiddleware,
@@ -680,11 +733,11 @@ console.log('State after counter set:', store.get());
680
733
  version: 1 (incremented by VersionIncrementMiddleware)
681
734
  */
682
735
 
683
- await store.set({ lastAction: 'Manual update from outside middleware' });
736
+ await store.set({ lastAction: "Manual update from outside middleware" });
684
737
  /* Expected console output from LoggerMiddleware:
685
738
  Middleware: Incoming update: { lastAction: 'Manual update from outside middleware' }
686
739
  */
687
- console.log('State after manual action:', store.get());
740
+ console.log("State after manual action:", store.get());
688
741
  /* Output will show:
689
742
  lastAction will be overwritten by TimestampActionMiddleware logic,
690
743
  a new log entry will be added,
@@ -692,11 +745,14 @@ console.log('State after manual action:', store.get());
692
745
  */
693
746
 
694
747
  // Unuse a middleware by its ID
695
- const temporaryLoggerId = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) });
748
+ const temporaryLoggerId = store.use({
749
+ name: "TemporaryLogger",
750
+ action: (s, u) => console.log("Temporary logger saw:", u),
751
+ });
696
752
  await store.set({ counter: 1 });
697
753
  // Output: Temporary logger saw: { counter: 1 }
698
754
  const removed = temporaryLoggerId(); // Remove the temporary logger
699
- console.log('Middleware removed:', removed);
755
+ console.log("Middleware removed:", removed);
700
756
  await store.set({ counter: 1 }); // TemporaryLogger will not be called now
701
757
  ```
702
758
 
@@ -705,7 +761,7 @@ await store.set({ counter: 1 }); // TemporaryLogger will not be called now
705
761
  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.
706
762
 
707
763
  ```typescript
708
- import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
764
+ import { ReactiveDataStore, type DeepPartial } from "@asaidimu/utils-store";
709
765
 
710
766
  interface UserProfile {
711
767
  name: string;
@@ -715,7 +771,7 @@ interface UserProfile {
715
771
  }
716
772
 
717
773
  const store = new ReactiveDataStore<UserProfile>({
718
- name: 'Guest',
774
+ name: "Guest",
719
775
  age: 0,
720
776
  isAdmin: false,
721
777
  isVerified: false,
@@ -724,10 +780,14 @@ const store = new ReactiveDataStore<UserProfile>({
724
780
  // Blocking middleware 1: Age validation
725
781
  store.use({
726
782
  block: true, // Mark as a blocking middleware
727
- name: 'AgeValidationMiddleware',
783
+ name: "AgeValidationMiddleware",
728
784
  action: (state, update) => {
729
- if (update.age !== undefined && typeof update.age === 'number' && update.age < 18) {
730
- console.warn('Blocking update: Age must be 18 or older.');
785
+ if (
786
+ update.age !== undefined &&
787
+ typeof update.age === "number" &&
788
+ update.age < 18
789
+ ) {
790
+ console.warn("Blocking update: Age must be 18 or older.");
731
791
  return false; // Block the update
732
792
  }
733
793
  return true; // Allow the update
@@ -737,17 +797,17 @@ store.use({
737
797
  // Blocking middleware 2: Admin check
738
798
  store.use({
739
799
  block: true,
740
- name: 'AdminRestrictionMiddleware',
800
+ name: "AdminRestrictionMiddleware",
741
801
  action: (state, update) => {
742
802
  // If attempting to become admin, check conditions
743
803
  if (update.isAdmin === true) {
744
804
  if (state.age < 21) {
745
- console.warn('Blocking update: User must be 21+ to become admin.');
805
+ console.warn("Blocking update: User must be 21+ to become admin.");
746
806
  return false;
747
807
  }
748
808
  if (!state.isVerified) {
749
- console.warn('Blocking update: User must be verified to become admin.');
750
- return false;
809
+ console.warn("Blocking update: User must be verified to become admin.");
810
+ return false;
751
811
  }
752
812
  }
753
813
  return true; // Allow the update
@@ -756,34 +816,46 @@ store.use({
756
816
 
757
817
  // Attempt to set a valid age
758
818
  await store.set({ age: 25 });
759
- console.log('User age after valid update:', store.get().age); // Output: 25
819
+ console.log("User age after valid update:", store.get().age); // Output: 25
760
820
 
761
821
  // Attempt to set an invalid age (will be blocked)
762
822
  await store.set({ age: 16 });
763
- console.log('User age after invalid update attempt (should be 25):', store.get().age); // Output: 25
823
+ console.log(
824
+ "User age after invalid update attempt (should be 25):",
825
+ store.get().age,
826
+ ); // Output: 25
764
827
 
765
828
  // Attempt to make user admin while not verified (will be blocked)
766
829
  await store.set({ isAdmin: true });
767
- console.log('User admin status after failed attempt (should be false):', store.get().isAdmin); // Output: false
830
+ console.log(
831
+ "User admin status after failed attempt (should be false):",
832
+ store.get().isAdmin,
833
+ ); // Output: false
768
834
 
769
835
  // Verify user, then attempt to make admin again (will still be blocked due to age)
770
836
  await store.set({ isVerified: true });
771
837
  await store.set({ age: 20 });
772
838
  await store.set({ isAdmin: true });
773
- console.log('User admin status after failed age attempt (should be false):', store.get().isAdmin); // Output: false
839
+ console.log(
840
+ "User admin status after failed age attempt (should be false):",
841
+ store.get().isAdmin,
842
+ ); // Output: false
774
843
 
775
844
  // Now make user old enough and verified, then try again (should succeed)
776
845
  await store.set({ age: 25 });
777
846
  await store.set({ isAdmin: true });
778
- console.log('User admin status after successful attempt (should be true):', store.get().isAdmin); // Output: true
847
+ console.log(
848
+ "User admin status after successful attempt (should be true):",
849
+ store.get().isAdmin,
850
+ ); // Output: true
779
851
  ```
780
852
 
781
853
  ### Transaction Support
782
854
 
783
- 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.
855
+ 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.
784
856
 
785
857
  ```typescript
786
- import { ReactiveDataStore } from '@asaidimu/utils-store';
858
+ import { ReactiveDataStore } from "@asaidimu/utils-store";
787
859
 
788
860
  interface BankAccount {
789
861
  name: string;
@@ -792,8 +864,16 @@ interface BankAccount {
792
864
  }
793
865
 
794
866
  // Set up two bank accounts
795
- const accountA = new ReactiveDataStore<BankAccount>({ name: 'Account A', balance: 500, transactions: [] });
796
- const accountB = new ReactiveDataStore<BankAccount>({ name: 'Account B', balance: 200, transactions: [] });
867
+ const accountA = new ReactiveDataStore<BankAccount>({
868
+ name: "Account A",
869
+ balance: 500,
870
+ transactions: [],
871
+ });
872
+ const accountB = new ReactiveDataStore<BankAccount>({
873
+ name: "Account B",
874
+ balance: 200,
875
+ transactions: [],
876
+ });
797
877
 
798
878
  // A function to transfer funds using transactions
799
879
  async function transferFunds(
@@ -804,63 +884,75 @@ async function transferFunds(
804
884
  // All operations inside this transaction will be atomic.
805
885
  // If `operation()` throws an error, the state will revert.
806
886
  await fromStore.transaction(async () => {
807
- console.log(`Starting transfer of ${amount}. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
887
+ console.log(
888
+ `Starting transfer of ${amount}. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`,
889
+ );
808
890
 
809
891
  // Deduct from sender
810
892
  await fromStore.set((state) => {
811
893
  if (state.balance < amount) {
812
894
  // Throwing an error here will cause the entire transaction to roll back
813
- throw new Error('Insufficient funds');
895
+ throw new Error("Insufficient funds");
814
896
  }
815
897
  return {
816
898
  balance: state.balance - amount,
817
- transactions: [...state.transactions, `Debited ${amount} from ${state.name}`],
899
+ transactions: [
900
+ ...state.transactions,
901
+ `Debited ${amount} from ${state.name}`,
902
+ ],
818
903
  };
819
904
  });
820
905
 
821
906
  // Simulate a network delay or another async operation that might fail
822
907
  // If an error happens here, the state will still roll back.
823
- await new Promise(resolve => setTimeout(resolve, 50));
908
+ await new Promise((resolve) => setTimeout(resolve, 50));
824
909
 
825
910
  // Add to receiver
826
911
  await toStore.set((state) => ({
827
912
  balance: state.balance + amount,
828
- transactions: [...state.transactions, `Credited ${amount} to ${state.name}`],
913
+ transactions: [
914
+ ...state.transactions,
915
+ `Credited ${amount} to ${state.name}`,
916
+ ],
829
917
  }));
830
918
 
831
- console.log(`Transfer in progress. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
919
+ console.log(
920
+ `Transfer in progress. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`,
921
+ );
832
922
  });
833
- console.log(`Transfer successful. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
923
+ console.log(
924
+ `Transfer successful. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`,
925
+ );
834
926
  }
835
927
 
836
- console.log('--- Initial Balances ---');
837
- console.log('Account A:', accountA.get().balance); // Expected: 500
838
- console.log('Account B:', accountB.get().balance); // Expected: 200
928
+ console.log("--- Initial Balances ---");
929
+ console.log("Account A:", accountA.get().balance); // Expected: 500
930
+ console.log("Account B:", accountB.get().balance); // Expected: 200
839
931
 
840
932
  // --- Scenario 1: Successful transfer ---
841
- console.log('\n--- Attempting successful transfer (100) ---');
933
+ console.log("\n--- Attempting successful transfer (100) ---");
842
934
  try {
843
935
  await transferFunds(accountA, accountB, 100);
844
- console.log('\nTransfer 1 successful:');
845
- console.log('Account A:', accountA.get()); // Expected: balance 400, transactions: ['Debited 100 from Account A']
846
- console.log('Account B:', accountB.get()); // Expected: balance 300, transactions: ['Credited 100 to Account B']
936
+ console.log("\nTransfer 1 successful:");
937
+ console.log("Account A:", accountA.get()); // Expected: balance 400, transactions: ['Debited 100 from Account A']
938
+ console.log("Account B:", accountB.get()); // Expected: balance 300, transactions: ['Credited 100 to Account B']
847
939
  } catch (error: any) {
848
- console.error('Transfer 1 failed unexpectedly:', error.message);
940
+ console.error("Transfer 1 failed unexpectedly:", error.message);
849
941
  }
850
942
 
851
943
  // --- Scenario 2: Failed transfer (insufficient funds) ---
852
- console.log('\n--- Attempting failed transfer (1000) ---');
944
+ console.log("\n--- Attempting failed transfer (1000) ---");
853
945
  try {
854
946
  // Account A now has 400, so this should fail
855
947
  await transferFunds(accountA, accountB, 1000);
856
948
  } catch (error: any) {
857
- console.error('Transfer 2 failed as expected:', error.message);
949
+ console.error("Transfer 2 failed as expected:", error.message);
858
950
  } finally {
859
- console.log('Transfer 2 attempt, state after rollback:');
951
+ console.log("Transfer 2 attempt, state after rollback:");
860
952
  // State should be rolled back to its state *before* the transaction attempt
861
- console.log('Account A:', accountA.get());
953
+ console.log("Account A:", accountA.get());
862
954
  // Expected: balance 400 (rolled back to state before this transaction)
863
- console.log('Account B:', accountB.get());
955
+ console.log("Account B:", accountB.get());
864
956
  // Expected: balance 300 (rolled back to state before this transaction)
865
957
  }
866
958
  ```
@@ -870,17 +962,21 @@ try {
870
962
  The Artifact system provides a powerful way to manage external dependencies, services, or complex objects that your actions or other artifacts might need. It supports `Singleton` (created once, reactive to dependencies) and `Transient` (new instance every time) scopes, and allows artifacts to depend on state changes and other artifacts.
871
963
 
872
964
  ```typescript
873
- import { ReactiveDataStore, ArtifactContainer, ArtifactScope } from '@asaidimu/utils-store';
965
+ import {
966
+ ReactiveDataStore,
967
+ ArtifactContainer,
968
+ ArtifactScope,
969
+ } from "@asaidimu/utils-store";
874
970
 
875
971
  interface AppState {
876
- user: { id: string; name: string; };
877
- config: { apiUrl: string; logLevel: string; };
972
+ user: { id: string; name: string };
973
+ config: { apiUrl: string; logLevel: string };
878
974
  }
879
975
 
880
976
  // Mock DataStore interface for standalone ArtifactContainer usage
881
977
  const store = new ReactiveDataStore<AppState>({
882
- user: { id: 'user-1', name: 'Alice' },
883
- config: { apiUrl: '/api/v1', logLevel: 'info' }
978
+ user: { id: "user-1", name: "Alice" },
979
+ config: { apiUrl: "/api/v1", logLevel: "info" },
884
980
  });
885
981
 
886
982
  const container = new ArtifactContainer(store);
@@ -889,113 +985,137 @@ const container = new ArtifactContainer(store);
889
985
 
890
986
  // Artifact 1: Simple Logger (Transient) - new instance every time
891
987
  container.register({
892
- key: 'logger',
893
- scope: ArtifactScope.Transient,
894
- factory: () => {
895
- console.log('Logger artifact created (Transient)');
896
- return {
897
- log: (message: string) => console.log(`[LOG] ${message}`)
898
- };
899
- }
988
+ key: "logger",
989
+ scope: ArtifactScope.Transient,
990
+ factory: () => {
991
+ console.log("Logger artifact created (Transient)");
992
+ return {
993
+ log: (message: string) => console.log(`[LOG] ${message}`),
994
+ };
995
+ },
900
996
  });
901
997
 
902
998
  // Artifact 2: API Client (Singleton) - depends on state.config.apiUrl
903
- const apiClientCleanup = () => console.log('API Client connection closed.');
999
+ const apiClientCleanup = () => console.log("API Client connection closed.");
904
1000
  container.register({
905
- key: 'apiClient',
906
- scope: ArtifactScope.Singleton,
907
- factory: async ({ use, onCleanup, current }) => {
908
- const apiUrl = await use(({ select }) => select((state: AppState) => state.config.apiUrl));
909
- console.log(`API Client created/re-created for URL: ${apiUrl}`);
910
- if (current) {
911
- console.log('Re-creating API client. Old instance:', current);
912
- }
913
- onCleanup(apiClientCleanup); // Register cleanup for this instance
914
- return {
915
- fetchUser: (id: string) => `Fetching ${id} from ${apiUrl}`,
916
- sendData: (data: any) => `Sending ${JSON.stringify(data)} to ${apiUrl}`
917
- };
1001
+ key: "apiClient",
1002
+ scope: ArtifactScope.Singleton,
1003
+ factory: async ({ use, onCleanup, current }) => {
1004
+ const apiUrl = await use(({ select }) =>
1005
+ select((state: AppState) => state.config.apiUrl),
1006
+ );
1007
+ console.log(`API Client created/re-created for URL: ${apiUrl}`);
1008
+ if (current) {
1009
+ console.log("Re-creating API client. Old instance:", current);
918
1010
  }
1011
+ onCleanup(apiClientCleanup); // Register cleanup for this instance
1012
+ return {
1013
+ fetchUser: (id: string) => `Fetching ${id} from ${apiUrl}`,
1014
+ sendData: (data: any) => `Sending ${JSON.stringify(data)} to ${apiUrl}`,
1015
+ };
1016
+ },
919
1017
  });
920
1018
 
921
1019
  // Artifact 3: User Service (Singleton) - depends on 'apiClient' and state.user.name
922
- const userServiceCleanup = () => console.log('User Service resources released.');
1020
+ const userServiceCleanup = () =>
1021
+ console.log("User Service resources released.");
923
1022
  container.register({
924
- key: 'userService',
925
- scope: ArtifactScope.Singleton,
926
- factory: async ({ use, onCleanup, current }) => {
927
- const apiClient = await use(({ resolve }) => resolve('apiClient'));
928
- const userName = await use(({ select }) => select((state: AppState) => state.user.name));
929
- console.log(`User Service created/re-created for user: ${userName}`);
930
- if (current) {
931
- console.log('Re-creating User Service. Old instance:', current);
932
- }
933
- onCleanup(userServiceCleanup); // Register cleanup for this instance
934
- return {
935
- getUserProfile: () => apiClient.instance!.fetchUser(store.get().user.id),
936
- updateUserName: (newName: string) => `Updating user name to ${newName} via API. Current: ${userName}`
937
- };
1023
+ key: "userService",
1024
+ scope: ArtifactScope.Singleton,
1025
+ factory: async ({ use, onCleanup, current }) => {
1026
+ const apiClient = await use(({ resolve }) => resolve("apiClient"));
1027
+ const userName = await use(({ select }) =>
1028
+ select((state: AppState) => state.user.name),
1029
+ );
1030
+ console.log(`User Service created/re-created for user: ${userName}`);
1031
+ if (current) {
1032
+ console.log("Re-creating User Service. Old instance:", current);
938
1033
  }
1034
+ onCleanup(userServiceCleanup); // Register cleanup for this instance
1035
+ return {
1036
+ getUserProfile: () => apiClient.instance!.fetchUser(store.get().user.id),
1037
+ updateUserName: (newName: string) =>
1038
+ `Updating user name to ${newName} via API. Current: ${userName}`,
1039
+ };
1040
+ },
939
1041
  });
940
1042
 
941
1043
  // --- Usage ---
942
1044
 
943
1045
  async function runDemo() {
944
- console.log('\n--- Initial Artifact Resolution ---');
945
- const logger1 = await container.resolve('logger');
946
- logger1.instance!.log('Application started.');
947
-
948
- const apiClient1 = await container.resolve('apiClient');
949
- console.log(apiClient1.instance!.fetchUser('123'));
950
-
951
- const userService1 = await container.resolve('userService');
952
- console.log(userService1.instance!.getUserProfile());
953
-
954
- // Transient artifact resolves a new instance
955
- const logger2 = await container.resolve('logger');
956
- console.log('Logger instances are different:', logger1.instance !== logger2.instance);
957
-
958
- // Singleton artifacts resolve the same instance initially
959
- const apiClient2 = await container.resolve('apiClient');
960
- console.log('API Client instances are same:', apiClient1.instance === apiClient2.instance);
961
-
962
- console.log('\n--- Simulate State Change (config.apiUrl) ---');
963
- await store.set({ config:{ apiUrl: '/api/v2'}})
964
- // This state change invalidates 'apiClient' which depends on 'config.apiUrl'
965
- // and then invalidates 'userService' which depends on 'apiClient'.
966
-
967
- // After config.apiUrl changes, apiClient (and userService) should be re-created
968
- const apiClient3 = await container.resolve('apiClient'); // This will trigger re-creation
969
- console.log(apiClient3.instance!.fetchUser('123'));
970
- console.log('API Client instances are different:', apiClient3.instance !== apiClient1.instance); // Should be a new instance
971
-
972
- // userService should also be re-created because apiClient, its dependency, was re-created
973
- const userService2 = await container.resolve('userService');
974
- console.log(userService2.instance!.getUserProfile());
975
- console.log('User Service instances are different:', userService2.instance !== userService1.instance); // Should be a new instance
976
-
977
- console.log('\n--- Simulate State Change (user.name) ---');
978
- await store.set({ user: { name: 'Bob' } });
979
-
980
- // Only userService (which depends on user.name) should be re-created this time, not apiClient
981
- const apiClient4 = await container.resolve('apiClient');
982
- console.log('API Client instances are same:', apiClient4.instance === apiClient3.instance); // Still the same API client instance
983
-
984
- const userService3 = await container.resolve('userService');
985
- console.log(userService3.instance!.getUserProfile());
986
- console.log('User Service instances are different:', userService3.instance !== userService2.instance); // New user service instance
987
-
988
- console.log('\n--- Unregistering Artifacts ---');
989
- await container.unregister('userService');
990
- // Output: User Service resources released. (cleanup called)
991
- await container.unregister('apiClient');
992
- // Output: API Client connection closed. (cleanup called)
993
-
994
- try {
995
- await container.resolve('userService');
996
- } catch (e: any) {
997
- console.error('Expected error:', e.message); // Artifact not found
998
- }
1046
+ console.log("\n--- Initial Artifact Resolution ---");
1047
+ const logger1 = await container.resolve("logger");
1048
+ logger1.instance!.log("Application started.");
1049
+
1050
+ const apiClient1 = await container.resolve("apiClient");
1051
+ console.log(apiClient1.instance!.fetchUser("123"));
1052
+
1053
+ const userService1 = await container.resolve("userService");
1054
+ console.log(userService1.instance!.getUserProfile());
1055
+
1056
+ // Transient artifact resolves a new instance
1057
+ const logger2 = await container.resolve("logger");
1058
+ console.log(
1059
+ "Logger instances are different:",
1060
+ logger1.instance !== logger2.instance,
1061
+ );
1062
+
1063
+ // Singleton artifacts resolve the same instance initially
1064
+ const apiClient2 = await container.resolve("apiClient");
1065
+ console.log(
1066
+ "API Client instances are same:",
1067
+ apiClient1.instance === apiClient2.instance,
1068
+ );
1069
+
1070
+ console.log("\n--- Simulate State Change (config.apiUrl) ---");
1071
+ await store.set({ config: { apiUrl: "/api/v2" } });
1072
+ // This state change invalidates 'apiClient' which depends on 'config.apiUrl'
1073
+ // and then invalidates 'userService' which depends on 'apiClient'.
1074
+
1075
+ // After config.apiUrl changes, apiClient (and userService) should be re-created
1076
+ const apiClient3 = await container.resolve("apiClient"); // This will trigger re-creation
1077
+ console.log(apiClient3.instance!.fetchUser("123"));
1078
+ console.log(
1079
+ "API Client instances are different:",
1080
+ apiClient3.instance !== apiClient1.instance,
1081
+ ); // Should be a new instance
1082
+
1083
+ // userService should also be re-created because apiClient, its dependency, was re-created
1084
+ const userService2 = await container.resolve("userService");
1085
+ console.log(userService2.instance!.getUserProfile());
1086
+ console.log(
1087
+ "User Service instances are different:",
1088
+ userService2.instance !== userService1.instance,
1089
+ ); // Should be a new instance
1090
+
1091
+ console.log("\n--- Simulate State Change (user.name) ---");
1092
+ await store.set({ user: { name: "Bob" } });
1093
+
1094
+ // Only userService (which depends on user.name) should be re-created this time, not apiClient
1095
+ const apiClient4 = await container.resolve("apiClient");
1096
+ console.log(
1097
+ "API Client instances are same:",
1098
+ apiClient4.instance === apiClient3.instance,
1099
+ ); // Still the same API client instance
1100
+
1101
+ const userService3 = await container.resolve("userService");
1102
+ console.log(userService3.instance!.getUserProfile());
1103
+ console.log(
1104
+ "User Service instances are different:",
1105
+ userService3.instance !== userService2.instance,
1106
+ ); // New user service instance
1107
+
1108
+ console.log("\n--- Unregistering Artifacts ---");
1109
+ await container.unregister("userService");
1110
+ // Output: User Service resources released. (cleanup called)
1111
+ await container.unregister("apiClient");
1112
+ // Output: API Client connection closed. (cleanup called)
1113
+
1114
+ try {
1115
+ await container.resolve("userService");
1116
+ } catch (e: any) {
1117
+ console.error("Expected error:", e.message); // Artifact not found
1118
+ }
999
1119
  }
1000
1120
 
1001
1121
  runDemo();
@@ -1006,50 +1126,58 @@ runDemo();
1006
1126
  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.
1007
1127
 
1008
1128
  ```typescript
1009
- import { ReactiveDataStore, StoreObserver, type StoreEvent, type DeepPartial } from '@asaidimu/utils-store';
1129
+ import {
1130
+ ReactiveDataStore,
1131
+ StoreObserver,
1132
+ type StoreEvent,
1133
+ type DeepPartial,
1134
+ } from "@asaidimu/utils-store";
1010
1135
 
1011
1136
  interface DebuggableState {
1012
- user: { name: string; status: 'online' | 'offline' };
1137
+ user: { name: string; status: "online" | "offline" };
1013
1138
  messages: string[];
1014
1139
  settings: { debugMode: boolean; logLevel: string };
1015
1140
  metrics: { updates: number };
1016
1141
  }
1017
1142
 
1018
1143
  const store = new ReactiveDataStore<DebuggableState>({
1019
- user: { name: 'Debugger', status: 'online' },
1144
+ user: { name: "Debugger", status: "online" },
1020
1145
  messages: [],
1021
- settings: { debugMode: true, logLevel: 'info' },
1146
+ settings: { debugMode: true, logLevel: "info" },
1022
1147
  metrics: { updates: 0 },
1023
1148
  });
1024
1149
 
1025
1150
  // Initialize observability for the store
1026
1151
  // Options allow granular control over what is tracked and logged.
1027
1152
  const observer = new StoreObserver(store, {
1028
- maxEvents: 50, // Keep up to 50 internal store events in history
1029
- maxStateHistory: 5, // Keep up to 5 state snapshots for time-travel
1153
+ maxEvents: 50, // Keep up to 50 internal store events in history
1154
+ maxStateHistory: 5, // Keep up to 5 state snapshots for time-travel
1030
1155
  enableConsoleLogging: true, // Log events to browser console for immediate feedback
1031
1156
  logEvents: {
1032
- updates: true, // Log all update lifecycle events (start/complete)
1033
- middleware: true, // Log middleware start/complete/error/blocked/executed events
1034
- transactions: true, // Log transaction start/complete/error events
1035
- actions: true, // Log action start/complete/error events
1036
- selectors: true, // Log selector accessed/changed events
1157
+ updates: true, // Log all update lifecycle events (start/complete)
1158
+ middleware: true, // Log middleware start/complete/error/blocked/executed events
1159
+ transactions: true, // Log transaction start/complete/error events
1160
+ actions: true, // Log action start/complete/error events
1161
+ selectors: true, // Log selector accessed/changed events
1037
1162
  },
1038
1163
  performanceThresholds: {
1039
- updateTime: 50, // Warn in console if an update takes > 50ms
1040
- middlewareTime: 20, // Warn if a middleware takes > 20ms
1164
+ updateTime: 50, // Warn in console if an update takes > 50ms
1165
+ middlewareTime: 20, // Warn if a middleware takes > 20ms
1041
1166
  },
1042
1167
  });
1043
1168
 
1044
1169
  // Add a simple middleware to demonstrate middleware logging and metrics update
1045
- store.use({ name: 'UpdateMetricsMiddleware', action: async (state, update) => {
1046
- await new Promise(resolve => setTimeout(resolve, 10)); // Simulate work
1047
- return { metrics: { updates: state.metrics.updates + 1 } };
1048
- }});
1170
+ store.use({
1171
+ name: "UpdateMetricsMiddleware",
1172
+ action: async (state, update) => {
1173
+ await new Promise((resolve) => setTimeout(resolve, 10)); // Simulate work
1174
+ return { metrics: { updates: state.metrics.updates + 1 } };
1175
+ },
1176
+ });
1049
1177
 
1050
1178
  // Perform some state updates
1051
- await store.set({ user: { status: 'offline' } });
1052
- await store.set({ messages: ['Hello World!'] });
1179
+ await store.set({ user: { status: "offline" } });
1180
+ await store.set({ messages: ["Hello World!"] });
1053
1181
  await store.set({ settings: { debugMode: false } });
1054
1182
 
1055
1183
  // Simulate a slow update to trigger performance warning
@@ -1058,92 +1186,122 @@ await store.set({ settings: { debugMode: false } });
1058
1186
  // This last set will cause a console warning for "Slow update detected" if enableConsoleLogging is true.
1059
1187
 
1060
1188
  // 1. Get Event History
1061
- console.log('\n--- Event History (Most Recent First) ---');
1189
+ console.log("\n--- Event History (Most Recent First) ---");
1062
1190
  const events = observer.getEventHistory();
1063
1191
  // Events will include: update:start, update:complete (multiple times), middleware:start, middleware:complete, etc.
1064
- events.slice(0, 5).forEach(event => console.log(`Type: ${event.type}, Data: ${JSON.stringify(event.data).substring(0, 70)}...`));
1192
+ events
1193
+ .slice(0, 5)
1194
+ .forEach((event) =>
1195
+ console.log(
1196
+ `Type: ${event.type}, Data: ${JSON.stringify(event.data).substring(0, 70)}...`,
1197
+ ),
1198
+ );
1065
1199
 
1066
1200
  // 2. Get State History
1067
- console.log('\n--- State History (Most Recent First) ---');
1201
+ console.log("\n--- State History (Most Recent First) ---");
1068
1202
  const stateSnapshots = observer.getStateHistory();
1069
- stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}: Messages: ${snapshot.state.messages.join(', ')}, User Status: ${snapshot.state.user.status}`));
1203
+ stateSnapshots.forEach((snapshot, index) =>
1204
+ console.log(
1205
+ `State #${index}: Messages: ${snapshot.state.messages.join(", ")}, User Status: ${snapshot.state.user.status}`,
1206
+ ),
1207
+ );
1070
1208
 
1071
1209
  // 3. Get Recent Changes (Diffs)
1072
- console.log('\n--- Recent State Changes (Diffs) ---');
1210
+ console.log("\n--- Recent State Changes (Diffs) ---");
1073
1211
  const recentChanges = observer.getRecentChanges(3); // Show diffs for last 3 changes
1074
1212
  recentChanges.forEach((change, index) => {
1075
1213
  console.log(`\nChange #${index}:`);
1076
- console.log(` Timestamp: ${new Date(change.timestamp).toLocaleTimeString()}`);
1077
- console.log(` Changed Paths: ${change.changedPaths.join(', ')}`);
1214
+ console.log(
1215
+ ` Timestamp: ${new Date(change.timestamp).toLocaleTimeString()}`,
1216
+ );
1217
+ console.log(` Changed Paths: ${change.changedPaths.join(", ")}`);
1078
1218
  console.log(` From (partial):`, change.from); // Only changed parts of the state
1079
- console.log(` To (partial):`, change.to); // Only changed parts of the state
1219
+ console.log(` To (partial):`, change.to); // Only changed parts of the state
1080
1220
  });
1081
1221
 
1082
1222
  // 4. Time-Travel Debugging
1083
- console.log('\n--- Time-Travel ---');
1223
+ console.log("\n--- Time-Travel ---");
1084
1224
  const timeTravel = observer.createTimeTravel();
1085
1225
 
1086
1226
  // Add more states to the history for time-travel demonstration
1087
- await store.set({ user: { status: 'online' } }); // Current State (A)
1088
- await store.set({ messages: ['First message'] }); // Previous State (B)
1089
- await store.set({ messages: ['Second message'] }); // Previous State (C)
1227
+ await store.set({ user: { status: "online" } }); // Current State (A)
1228
+ await store.set({ messages: ["First message"] }); // Previous State (B)
1229
+ await store.set({ messages: ["Second message"] }); // Previous State (C)
1090
1230
 
1091
- console.log('Current state (latest):', store.get().messages); // Output: ['Second message']
1231
+ console.log("Current state (latest):", store.get().messages); // Output: ['Second message']
1092
1232
 
1093
1233
  if (timeTravel.canUndo()) {
1094
1234
  await timeTravel.undo(); // Go back to State B
1095
- console.log('After undo 1:', store.get().messages); // Output: ['First message']
1235
+ console.log("After undo 1:", store.get().messages); // Output: ['First message']
1096
1236
  }
1097
1237
 
1098
1238
  if (timeTravel.canUndo()) {
1099
1239
  await timeTravel.undo(); // Go back to State A
1100
- console.log('After undo 2:', store.get().messages); // Output: [] (the state before 'First message' was added)
1240
+ console.log("After undo 2:", store.get().messages); // Output: [] (the state before 'First message' was added)
1101
1241
  }
1102
1242
 
1103
1243
  if (timeTravel.canRedo()) {
1104
1244
  await timeTravel.redo(); // Go forward to State B
1105
- console.log('After redo 1:', store.get().messages); // Output: ['First message']
1245
+ console.log("After redo 1:", store.get().messages); // Output: ['First message']
1106
1246
  }
1107
1247
 
1108
- console.log('Time-Travel history length:', timeTravel.length()); // Reflects `maxStateHistory` + initial state
1248
+ console.log("Time-Travel history length:", timeTravel.length()); // Reflects `maxStateHistory` + initial state
1109
1249
 
1110
1250
  // 5. Custom Debugging Middleware (provided by StoreObserver for convenience)
1111
1251
  // Example: A logging middleware that logs every update
1112
1252
  const loggingMiddleware = observer.createLoggingMiddleware({
1113
- logLevel: 'info', // Can be 'debug', 'info', 'warn'
1253
+ logLevel: "info", // Can be 'debug', 'info', 'warn'
1114
1254
  logUpdates: true, // Whether to log the update payload itself
1115
1255
  });
1116
- const loggingMiddlewareId = store.use({ name: 'DebugLogging', action: loggingMiddleware });
1256
+ const loggingMiddlewareId = store.use({
1257
+ name: "DebugLogging",
1258
+ action: loggingMiddleware,
1259
+ });
1117
1260
 
1118
- await store.set({ user: { name: 'New User Via Debug Logger' } }); // This update will be logged by the created middleware.
1261
+ await store.set({ user: { name: "New User Via Debug Logger" } }); // This update will be logged by the created middleware.
1119
1262
  // Expected console output: "State Update: { user: { name: 'New User Via Debug Logger' } }"
1120
1263
 
1121
1264
  // Example: A validation middleware (blocking)
1122
- const validationMiddleware = observer.createValidationMiddleware((state, update) => {
1265
+ const validationMiddleware = observer.createValidationMiddleware(
1266
+ (state, update) => {
1123
1267
  if (update.messages && update.messages.length > 5) {
1124
- return { valid: false, reason: "Too many messages!" };
1268
+ return { valid: false, reason: "Too many messages!" };
1125
1269
  }
1126
1270
  return true;
1271
+ },
1272
+ );
1273
+ const validationMiddlewareId = store.use({
1274
+ name: "DebugValidation",
1275
+ block: true,
1276
+ action: validationMiddleware,
1127
1277
  });
1128
- const validationMiddlewareId = store.use({ name: 'DebugValidation', block: true, action: validationMiddleware });
1129
1278
 
1130
1279
  try {
1131
- await store.set({ messages: ['m1','m2','m3','m4','m5','m6'] }); // This will be blocked
1280
+ await store.set({ messages: ["m1", "m2", "m3", "m4", "m5", "m6"] }); // This will be blocked
1132
1281
  } catch (e: any) {
1133
- console.warn(`Caught expected error from validation middleware: ${e.message}`);
1282
+ console.warn(
1283
+ `Caught expected error from validation middleware: ${e.message}`,
1284
+ );
1134
1285
  }
1135
- console.log('Current messages after failed validation:', store.get().messages); // Should be the state before this set.
1286
+ console.log("Current messages after failed validation:", store.get().messages); // Should be the state before this set.
1136
1287
 
1137
1288
  // 6. Clear history
1138
1289
  observer.clearHistory();
1139
- console.log('\nHistory cleared. Events:', observer.getEventHistory().length, 'State snapshots:', observer.getStateHistory().length);
1290
+ console.log(
1291
+ "\nHistory cleared. Events:",
1292
+ observer.getEventHistory().length,
1293
+ "State snapshots:",
1294
+ observer.getStateHistory().length,
1295
+ );
1140
1296
  // Output: History cleared. Events: 0 State snapshots: 1 (keeps current state)
1141
1297
 
1142
1298
  // 7. Disconnect observer when no longer needed to prevent memory leaks
1143
1299
  observer.disconnect();
1144
- console.log('\nObserver disconnected. No more events or state changes will be tracked.');
1300
+ console.log(
1301
+ "\nObserver disconnected. No more events or state changes will be tracked.",
1302
+ );
1145
1303
  // After disconnect, new updates won't be logged or tracked by Observer
1146
- await store.set({ messages: ['Final message after disconnect'] });
1304
+ await store.set({ messages: ["Final message after disconnect"] });
1147
1305
  ```
1148
1306
 
1149
1307
  ### Event System
@@ -1151,108 +1309,155 @@ await store.set({ messages: ['Final message after disconnect'] });
1151
1309
  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.on(eventName, listener)`. The `StoreObserver` leverages this event system internally to provide its rich debugging capabilities.
1152
1310
 
1153
1311
  ```typescript
1154
- import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
1312
+ import { ReactiveDataStore, type StoreEvent } from "@asaidimu/utils-store";
1155
1313
 
1156
1314
  interface MyState {
1157
1315
  value: number;
1158
1316
  status: string;
1159
1317
  }
1160
1318
 
1161
- const store = new ReactiveDataStore<MyState>({ value: 0, status: 'idle' });
1319
+ const store = new ReactiveDataStore<MyState>({ value: 0, status: "idle" });
1162
1320
 
1163
1321
  // Subscribe to 'update:start' event - triggered before an update begins processing.
1164
- store.on('update:start', (data) => {
1165
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⚡ Update started. Action ID: ${data.actionId || 'N/A'}`);
1322
+ store.on("update:start", (data) => {
1323
+ console.log(
1324
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ⚡ Update started. Action ID: ${data.actionId || "N/A"}`,
1325
+ );
1166
1326
  });
1167
1327
 
1168
1328
  // Subscribe to 'update:complete' event - triggered after an update is fully applied or blocked.
1169
- store.on('update:complete', (data) => {
1329
+ store.on("update:complete", (data) => {
1170
1330
  if (data.blocked) {
1171
- console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] ✋ Update blocked. Error:`, data.error?.message);
1331
+ console.warn(
1332
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ✋ Update blocked. Error:`,
1333
+ data.error?.message,
1334
+ );
1172
1335
  } else {
1173
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Update complete. Changed paths: ${data.deltas?.map((d:any) => d.path).join(', ')} (took ${data.duration?.toFixed(2)}ms)`);
1336
+ console.log(
1337
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Update complete. Changed paths: ${data.deltas?.map((d: any) => d.path).join(", ")} (took ${data.duration?.toFixed(2)}ms)`,
1338
+ );
1174
1339
  }
1175
1340
  });
1176
1341
 
1177
1342
  // Subscribe to middleware lifecycle events
1178
- store.on('middleware:start', (data) => {
1179
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ▶ Middleware "${data.name}" (${data.type || 'transform'}) started.`);
1343
+ store.on("middleware:start", (data) => {
1344
+ console.log(
1345
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ▶ Middleware "${data.name}" (${data.type || "transform"}) started.`,
1346
+ );
1180
1347
  });
1181
1348
 
1182
- store.on('middleware:complete', (data) => {
1183
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ◀ Middleware "${data.name}" (${data.type || 'transform'}) completed in ${data.duration?.toFixed(2)}ms.`);
1349
+ store.on("middleware:complete", (data) => {
1350
+ console.log(
1351
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ◀ Middleware "${data.name}" (${data.type || "transform"}) completed in ${data.duration?.toFixed(2)}ms.`,
1352
+ );
1184
1353
  });
1185
1354
 
1186
- store.on('middleware:error', (data) => {
1187
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Middleware "${data.name}" failed:`, data.error);
1355
+ store.on("middleware:error", (data) => {
1356
+ console.error(
1357
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Middleware "${data.name}" failed:`,
1358
+ data.error,
1359
+ );
1188
1360
  });
1189
1361
 
1190
- store.on('middleware:blocked', (data) => {
1191
- console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] 🛑 Middleware "${data.name}" blocked an update.`);
1362
+ store.on("middleware:blocked", (data) => {
1363
+ console.warn(
1364
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 🛑 Middleware "${data.name}" blocked an update.`,
1365
+ );
1192
1366
  });
1193
1367
 
1194
- store.on('middleware:executed', (data) => {
1368
+ store.on("middleware:executed", (data) => {
1195
1369
  // This event captures detailed execution info for all middlewares, useful for aggregate metrics.
1196
- console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 📊 Middleware executed: "${data.name}" - Duration: ${data.duration?.toFixed(2)}ms, Blocked: ${data.blocked}`);
1370
+ console.debug(
1371
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 📊 Middleware executed: "${data.name}" - Duration: ${data.duration?.toFixed(2)}ms, Blocked: ${data.blocked}`,
1372
+ );
1197
1373
  });
1198
1374
 
1199
1375
  // Subscribe to transaction lifecycle events
1200
- store.on('transaction:start', (data) => {
1201
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction started.`);
1376
+ store.on("transaction:start", (data) => {
1377
+ console.log(
1378
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction started.`,
1379
+ );
1202
1380
  });
1203
1381
 
1204
- store.on('transaction:complete', (data) => {
1205
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction complete.`);
1382
+ store.on("transaction:complete", (data) => {
1383
+ console.log(
1384
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction complete.`,
1385
+ );
1206
1386
  });
1207
1387
 
1208
- store.on('transaction:error', (data) => {
1209
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction failed:`, data.error);
1388
+ store.on("transaction:error", (data) => {
1389
+ console.error(
1390
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction failed:`,
1391
+ data.error,
1392
+ );
1210
1393
  });
1211
1394
 
1212
1395
  // Subscribe to persistence events
1213
- store.on('persistence:ready', (data) => {
1214
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 💾 Persistence layer is ready.`);
1396
+ store.on("persistence:ready", (data) => {
1397
+ console.log(
1398
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 💾 Persistence layer is ready.`,
1399
+ );
1215
1400
  });
1216
- store.on('persistence:queued', (data) => {
1217
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⏳ Persistence task queued: ${data.taskId}, queue size: ${data.queueSize}`);
1401
+ store.on("persistence:queued", (data) => {
1402
+ console.log(
1403
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ⏳ Persistence task queued: ${data.taskId}, queue size: ${data.queueSize}`,
1404
+ );
1218
1405
  });
1219
- store.on('persistence:success', (data) => {
1220
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Persistence success for task: ${data.taskId}, took ${data.duration?.toFixed(2)}ms`);
1406
+ store.on("persistence:success", (data) => {
1407
+ console.log(
1408
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Persistence success for task: ${data.taskId}, took ${data.duration?.toFixed(2)}ms`,
1409
+ );
1221
1410
  });
1222
- store.on('persistence:retry', (data) => {
1223
- console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] 🔄 Persistence retry for task: ${data.taskId}, attempt ${data.attempt}/${data.maxRetries}`);
1411
+ store.on("persistence:retry", (data) => {
1412
+ console.warn(
1413
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 🔄 Persistence retry for task: ${data.taskId}, attempt ${data.attempt}/${data.maxRetries}`,
1414
+ );
1224
1415
  });
1225
- store.on('persistence:failed', (data) => {
1226
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Persistence failed permanently for task: ${data.taskId}`, data.error);
1416
+ store.on("persistence:failed", (data) => {
1417
+ console.error(
1418
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Persistence failed permanently for task: ${data.taskId}`,
1419
+ data.error,
1420
+ );
1227
1421
  });
1228
1422
 
1229
-
1230
1423
  // Subscribe to action events
1231
- store.on('action:start', (data) => {
1232
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 🚀 Action "${data.name}" (ID: ${data.actionId}) started with params:`, data.params);
1424
+ store.on("action:start", (data) => {
1425
+ console.log(
1426
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 🚀 Action "${data.name}" (ID: ${data.actionId}) started with params:`,
1427
+ data.params,
1428
+ );
1233
1429
  });
1234
1430
 
1235
- store.on('action:complete', (data) => {
1236
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✔️ Action "${data.name}" (ID: ${data.actionId}) completed in ${data.duration?.toFixed(2)}ms.`);
1431
+ store.on("action:complete", (data) => {
1432
+ console.log(
1433
+ `[${new Date(data.timestamp).toLocaleTimeString()}] ✔️ Action "${data.name}" (ID: ${data.actionId}) completed in ${data.duration?.toFixed(2)}ms.`,
1434
+ );
1237
1435
  });
1238
1436
 
1239
- store.on('action:error', (data) => {
1240
- console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] 🔥 Action "${data.name}" (ID: ${data.actionId}) failed:`, data.error);
1437
+ store.on("action:error", (data) => {
1438
+ console.error(
1439
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 🔥 Action "${data.name}" (ID: ${data.actionId}) failed:`,
1440
+ data.error,
1441
+ );
1241
1442
  });
1242
1443
 
1243
1444
  // Subscribe to selector events
1244
- store.on('selector:accessed', (data) => {
1245
- console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 👀 Selector (ID: ${data.selectorId}) accessed paths: ${data.accessedPaths.join(', ')}`);
1445
+ store.on("selector:accessed", (data) => {
1446
+ console.debug(
1447
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 👀 Selector (ID: ${data.selectorId}) accessed paths: ${data.accessedPaths.join(", ")}`,
1448
+ );
1246
1449
  });
1247
1450
 
1248
- store.on('selector:changed', (data) => {
1249
- console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📢 Selector (ID: ${data.selectorId}) changed. New result:`, data.newResult);
1451
+ store.on("selector:changed", (data) => {
1452
+ console.log(
1453
+ `[${new Date(data.timestamp).toLocaleTimeString()}] 📢 Selector (ID: ${data.selectorId}) changed. New result:`,
1454
+ data.newResult,
1455
+ );
1250
1456
  });
1251
1457
 
1252
-
1253
1458
  // Add a transform middleware to demonstrate `middleware:start/complete/executed`
1254
1459
  store.use({
1255
- name: 'ValueIncrementMiddleware',
1460
+ name: "ValueIncrementMiddleware",
1256
1461
  action: (state, update) => {
1257
1462
  return { value: state.value + (update.value || 0) };
1258
1463
  },
@@ -1260,34 +1465,39 @@ store.use({
1260
1465
 
1261
1466
  // Add a blocking middleware to demonstrate `middleware:error` and `update:complete` (blocked)
1262
1467
  store.use({
1263
- name: 'StatusValidationMiddleware',
1468
+ name: "StatusValidationMiddleware",
1264
1469
  block: true,
1265
1470
  action: (state, update) => {
1266
- if (update.status === 'error' && state.value < 10) {
1267
- throw new Error('Cannot set status to error if value is too low!');
1471
+ if (update.status === "error" && state.value < 10) {
1472
+ throw new Error("Cannot set status to error if value is too low!");
1268
1473
  }
1269
1474
  return true;
1270
1475
  },
1271
1476
  });
1272
1477
 
1273
1478
  // Perform operations to trigger events
1274
- console.log('\n--- Perform Initial Update ---');
1275
- await store.set({ value: 5, status: 'active' }); // Will increment value by 5 (due to middleware)
1479
+ console.log("\n--- Perform Initial Update ---");
1480
+ await store.set({ value: 5, status: "active" }); // Will increment value by 5 (due to middleware)
1276
1481
 
1277
- console.log('\n--- Perform Transactional Update (Success) ---');
1482
+ console.log("\n--- Perform Transactional Update (Success) ---");
1278
1483
  await store.transaction(async () => {
1279
1484
  await store.set({ value: 3 }); // Inside transaction, value becomes 5 + 3 = 8
1280
- await store.set({ status: 'processing' });
1485
+ await store.set({ status: "processing" });
1281
1486
  });
1282
1487
 
1283
- console.log('\n--- Perform Update (Blocked by Middleware) ---');
1488
+ console.log("\n--- Perform Update (Blocked by Middleware) ---");
1284
1489
  try {
1285
- await store.set({ status: 'error' }); // This should be blocked by StatusValidationMiddleware (current value is 8, which is < 10)
1490
+ await store.set({ status: "error" }); // This should be blocked by StatusValidationMiddleware (current value is 8, which is < 10)
1286
1491
  } catch (e: any) {
1287
1492
  console.log(`Caught expected error: ${e.message}`);
1288
1493
  }
1289
1494
 
1290
- console.log('Final value:', store.get().value, 'Final status:', store.get().status);
1495
+ console.log(
1496
+ "Final value:",
1497
+ store.get().value,
1498
+ "Final status:",
1499
+ store.get().status,
1500
+ );
1291
1501
  ```
1292
1502
 
1293
1503
  ---
@@ -1298,53 +1508,53 @@ The `@asaidimu/utils-store` library is built with a modular, component-based arc
1298
1508
 
1299
1509
  ### Core Components
1300
1510
 
1301
- * **`ReactiveDataStore<T>`**: 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`, `dispatch`, `select`, `set`, `watch`, `transaction`, `use`, and `on`.
1302
- * **`StateManager<T>`**: 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.
1303
- * **`MiddlewareEngine<T>`**: 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.
1304
- * **`PersistenceHandler<T>`**: 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). It also manages a background queue for persistence tasks with retries and exponential backoff.
1305
- * **`TransactionManager<T>`**: 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.
1306
- * **`MetricsCollector`**: Observes the internal `eventBus` to gather and expose real-time performance metrics of the store, such as update counts, listener executions, average update times, largest update size, total events fired, and transaction/middleware execution counts.
1307
- * **`SelectorManager<T>`**: Manages the creation, memoization, and reactivity of selectors. It tracks the paths accessed by each selector and re-evaluates them efficiently only when relevant parts of the state change, notifying their subscribers.
1308
- * **`StoreObserver<T>`**: 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. It also supports saving/loading and exporting observer sessions.
1309
- * **`ArtifactContainer<T>`**: Implements a dependency injection system for managing services (artifacts) with different lifecycles (Singleton, Transient) and complex dependencies on state paths and other artifacts. It handles lazy initialization, reactive re-evaluation, and resource cleanup.
1310
- * **`ActionManager<T>`**: Manages the registration of named actions and their dispatch. It handles action lifecycle events, debouncing logic, and ensures actions interact correctly with the core `set` method.
1311
- * **`createMerge`**: 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.
1312
- * **`createDiff` / `createDerivePaths`**: Factory functions returning utilities for efficient comparison between two objects (`createDiff`) to identify changed paths, and for deriving all parent paths from a set of changes (`createDerivePaths`). These are fundamental for optimizing listener notifications and internal change detection.
1511
+ - **`ReactiveDataStore<T>`**: 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`, `dispatch`, `select`, `set`, `watch`, `transaction`, `use`, and `on`.
1512
+ - **`StateManager<T>`**: 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.
1513
+ - **`MiddlewareEngine<T>`**: 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.
1514
+ - **`PersistenceHandler<T>`**: 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). It also manages a background queue for persistence tasks with retries and exponential backoff.
1515
+ - **`TransactionManager<T>`**: 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.
1516
+ - **`MetricsCollector`**: Observes the internal `eventBus` to gather and expose real-time performance metrics of the store, such as update counts, listener executions, average update times, largest update size, total events fired, and transaction/middleware execution counts.
1517
+ - **`SelectorManager<T>`**: Manages the creation, memoization, and reactivity of selectors. It tracks the paths accessed by each selector and re-evaluates them efficiently only when relevant parts of the state change, notifying their subscribers.
1518
+ - **`StoreObserver<T>`**: 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. It also supports saving/loading and exporting observer sessions.
1519
+ - **`ArtifactContainer<T>`**: Implements a dependency injection system for managing services (artifacts) with different lifecycles (Singleton, Transient) and complex dependencies on state paths and other artifacts. It handles lazy initialization, reactive re-evaluation, and resource cleanup.
1520
+ - **`ActionManager<T>`**: Manages the registration of named actions and their dispatch. It handles action lifecycle events, debouncing logic, and ensures actions interact correctly with the core `set` method.
1521
+ - **`createMerge`**: 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.
1522
+ - **`createDiff` / `createDerivePaths`**: Factory functions returning utilities for efficient comparison between two objects (`createDiff`) to identify changed paths, and for deriving all parent paths from a set of changes (`createDerivePaths`). These are fundamental for optimizing listener notifications and internal change detection.
1313
1523
 
1314
1524
  ### Data Flow
1315
1525
 
1316
1526
  The `ReactiveDataStore` handles state updates in a robust, queued, and event-driven manner:
1317
1527
 
1318
1528
  1. **`store.set(update)` or `store.dispatch(actionName, actionFn, ...)` call**:
1319
- * If `dispatch` is used, an `action:start` event is emitted. The `actionFn` (which can be `async`) is executed to produce a `DeepPartial` update which is then passed to `store.set`. Actions can be debounced, delaying their `set` call and potentially cancelling previous in-flight actions.
1320
- * All `set` calls are automatically queued to prevent race conditions during concurrent updates, ensuring sequential processing. The `store.state().pendingChanges` reflects the queue.
1321
- * An `update:start` event is immediately emitted.
1529
+ - If `dispatch` is used, an `action:start` event is emitted. The `actionFn` (which can be `async`) is executed to produce a `DeepPartial` update which is then passed to `store.set`. Actions can be debounced, delaying their `set` call and potentially cancelling previous in-flight actions.
1530
+ - All `set` calls are automatically queued to prevent race conditions during concurrent updates, ensuring sequential processing. The `store.state().pendingChanges` reflects the queue.
1531
+ - An `update:start` event is immediately emitted.
1322
1532
  2. **Middleware Execution**:
1323
- * 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` and an `error` property is emitted, and the process stops, with the state remaining unchanged.
1324
- * 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.
1325
- * Detailed lifecycle events (`middleware:start`, `middleware:complete`, `middleware:error`, `middleware:blocked`, `middleware:executed`) are emitted during this phase, providing granular insight into middleware behavior.
1533
+ - 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` and an `error` property is emitted, and the process stops, with the state remaining unchanged.
1534
+ - 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.
1535
+ - Detailed lifecycle events (`middleware:start`, `middleware:complete`, `middleware:error`, `middleware:blocked`, `middleware:executed`) are emitted during this phase, providing granular insight into middleware behavior.
1326
1536
  3. **State Application**:
1327
- * The `StateManager` receives the (potentially transformed) final `DeepPartial` update.
1328
- * It internally uses the `createMerge` utility to immutably construct the new full state object.
1329
- * It then performs a `createDiff` comparison between the *previous* state and the *new* state to precisely identify all `changedPaths` (an array of `StateDelta` objects).
1330
- * If changes are detected, the `StateManager` updates its internal immutable `cache` to the `newState` and then emits an internal `update` event for each granular `changedPath` on its `updateBus`.
1537
+ - The `StateManager` receives the (potentially transformed) final `DeepPartial` update.
1538
+ - It internally uses the `createMerge` utility to immutably construct the new full state object.
1539
+ - It then performs a `createDiff` comparison between the _previous_ state and the _new_ state to precisely identify all `changedPaths` (an array of `StateDelta` objects).
1540
+ - If changes are detected, the `StateManager` updates its internal immutable `cache` to the `newState` and then emits an internal `update` event for each granular `changedPath` on its `updateBus`.
1331
1541
  4. **Listener Notification**:
1332
- * Any external subscribers (registered with `store.watch()` or `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.
1333
- * The `SelectorManager` re-evaluates reactive selectors whose `accessedPaths` are affected by the `changedPaths`. If a selector's result changes, it notifies its own subscribers and emits a `selector:changed` event.
1334
- * `ArtifactContainer` also receives change notifications for state paths its artifacts depend on, triggering re-evaluation for `Singleton` scoped artifacts.
1542
+ - Any external subscribers (registered with `store.watch()` or `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.
1543
+ - The `SelectorManager` re-evaluates reactive selectors whose `accessedPaths` are affected by the `changedPaths`. If a selector's result changes, it notifies its own subscribers and emits a `selector:changed` event.
1544
+ - `ArtifactContainer` also receives change notifications for state paths its artifacts depend on, triggering re-evaluation for `Singleton` scoped artifacts.
1335
1545
  5. **Persistence Handling**:
1336
- * 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()`. This is done in the background via a queue, emitting `persistence:queued`, `persistence:success`, `persistence:retry`, and `persistence:failed` events.
1337
- * The `PersistenceHandler` also manages loading initial state and reacting to external state changes (e.g., from other browser tabs or processes) through `persistence.subscribe()`.
1546
+ - 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()`. This is done in the background via a queue, emitting `persistence:queued`, `persistence:success`, `persistence:retry`, and `persistence:failed` events.
1547
+ - The `PersistenceHandler` also manages loading initial state and reacting to external state changes (e.g., from other browser tabs or processes) through `persistence.subscribe()`.
1338
1548
  6. **Completion & Queue Processing**:
1339
- * An `update:complete` event is emitted, containing crucial information about the update's duration, the `StateDelta[]`, and any blocking errors.
1340
- * If the update originated from a `dispatch` call, an `action:complete` or `action:error` event is emitted, correlating with the `action:start` event via a shared `actionId`.
1341
- * The update queue automatically processes the next pending update.
1549
+ - An `update:complete` event is emitted, containing crucial information about the update's duration, the `StateDelta[]`, and any blocking errors.
1550
+ - If the update originated from a `dispatch` call, an `action:complete` or `action:error` event is emitted, correlating with the `action:start` event via a shared `actionId`.
1551
+ - The update queue automatically processes the next pending update.
1342
1552
 
1343
1553
  ### Extension Points
1344
1554
 
1345
- * **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.
1346
- * **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.
1347
- * **Custom Artifacts**: The `ArtifactContainer` (directly or via React integration) allows defining and managing any custom services, utilities, or dependencies with defined scopes and lifecycle management.
1555
+ - **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.
1556
+ - **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.
1557
+ - **Custom Artifacts**: The `ArtifactContainer` (directly or via React integration) allows defining and managing any custom services, utilities, or dependencies with defined scopes and lifecycle management.
1348
1558
 
1349
1559
  ---
1350
1560
 
@@ -1367,6 +1577,7 @@ Contributions are welcome! Follow these guidelines to get started with local dev
1367
1577
  ```
1368
1578
  3. **Build the project:**
1369
1579
  Navigate to the `store` package directory and run the build script, or build the entire monorepo from the root.
1580
+
1370
1581
  ```bash
1371
1582
  # From the monorepo root:
1372
1583
  pnpm build # Builds all packages in the monorepo
@@ -1380,10 +1591,10 @@ Contributions are welcome! Follow these guidelines to get started with local dev
1380
1591
 
1381
1592
  From the `src/store` directory, the following `pnpm` scripts are available:
1382
1593
 
1383
- * `pnpm test`: Runs all unit tests using [Vitest](https://vitest.dev/).
1384
- * `pnpm test:watch`: Runs tests in watch mode for continuous development.
1385
- * `pnpm dev`: Starts the Vite development server for the UI example.
1386
- * `pnpm lint`: Lints the codebase using [ESLint](https://eslint.org/).
1594
+ - `pnpm test`: Runs all unit tests using [Vitest](https://vitest.dev/).
1595
+ - `pnpm test:watch`: Runs tests in watch mode for continuous development.
1596
+ - `pnpm dev`: Starts the Vite development server for the UI example.
1597
+ - `pnpm lint`: Lints the codebase using [ESLint](https://eslint.org/).
1387
1598
 
1388
1599
  ### Testing
1389
1600
 
@@ -1416,8 +1627,9 @@ Please ensure all new features have comprehensive test coverage and all existing
1416
1627
  ### Issue Reporting
1417
1628
 
1418
1629
  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).
1419
- * For **bug reports**, include steps to reproduce, expected behavior, and actual behavior. Provide relevant code snippets or a minimal reproducible example.
1420
- * For **feature requests**, describe the use case, the problem it solves, and your proposed solution or ideas.
1630
+
1631
+ - For **bug reports**, include steps to reproduce, expected behavior, and actual behavior. Provide relevant code snippets or a minimal reproducible example.
1632
+ - For **feature requests**, describe the use case, the problem it solves, and your proposed solution or ideas.
1421
1633
 
1422
1634
  ---
1423
1635
 
@@ -1425,28 +1637,28 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
1425
1637
 
1426
1638
  ### Troubleshooting
1427
1639
 
1428
- * **"Update not triggering listeners"**:
1429
- * Ensure you are subscribing to the correct path. `store.watch('user.name', ...)` will not trigger if you update `user.email` (unless you also subscribe to `user` or the root `''` path).
1430
- * 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.
1431
- * Verify your `DeepPartial` update correctly targets the intended part of the state.
1432
- * **"Reactive Selector not re-evaluating"**:
1433
- * Ensure the state path(s) accessed by the selector are actually changing. The selector re-evaluates only when its dependencies change.
1434
- * If using `select(() => state.someArray.length)`, it might not re-evaluate if array elements change but `length` remains the same.
1435
- * Check for strict equality issues: if the new computed value is strictly equal to the old one, no re-evaluation notification will occur.
1436
- * Avoid complex operations (array methods, conditionals, arithmetic) within selectors as they might bypass the static path analysis and lead to unexpected behavior or errors.
1437
- * **"State not rolling back after transaction error"**:
1438
- * 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.
1439
- * Promises within the transaction *must* be `await`ed so the `TransactionManager` can capture potential rejections and manage the atomic operation correctly.
1440
- * **"Middleware not being applied"**:
1441
- * 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.
1442
- * Check middleware `name` in console logs (if `StoreObserver` is enabled with `enableConsoleLogging: true`) to confirm it's being hit.
1443
- * Ensure your `transform` middleware returns a `DeepPartial<T>` or `void`/`Promise<void>`, and `blocking` middleware returns `boolean`/`Promise<boolean>`.
1444
- * **"Performance warnings in console"**:
1445
- * 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.
1446
- * **"Artifact not resolving or re-evaluating"**:
1447
- * Check for `Circular dependency` errors in the console during `resolve`.
1448
- * For `Singleton` artifacts, ensure its `state` or `artifact` dependencies are actually changing to trigger re-evaluation. If a dependency changes, the artifact (and its downstream dependents) will be invalidated and re-created on next `resolve`.
1449
- * For `Transient` artifacts, a new instance is created on every `resolve`.
1640
+ - **"Update not triggering listeners"**:
1641
+ - Ensure you are subscribing to the correct path. `store.watch('user.name', ...)` will not trigger if you update `user.email` (unless you also subscribe to `user` or the root `''` path).
1642
+ - 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.
1643
+ - Verify your `DeepPartial` update correctly targets the intended part of the state.
1644
+ - **"Reactive Selector not re-evaluating"**:
1645
+ - Ensure the state path(s) accessed by the selector are actually changing. The selector re-evaluates only when its dependencies change.
1646
+ - If using `select(() => state.someArray.length)`, it might not re-evaluate if array elements change but `length` remains the same.
1647
+ - Check for strict equality issues: if the new computed value is strictly equal to the old one, no re-evaluation notification will occur.
1648
+ - Avoid complex operations (array methods, conditionals, arithmetic) within selectors as they might bypass the static path analysis and lead to unexpected behavior or errors.
1649
+ - **"State not rolling back after transaction error"**:
1650
+ - 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.
1651
+ - Promises within the transaction _must_ be `await`ed so the `TransactionManager` can capture potential rejections and manage the atomic operation correctly.
1652
+ - **"Middleware not being applied"**:
1653
+ - 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.
1654
+ - Check middleware `name` in console logs (if `StoreObserver` is enabled with `enableConsoleLogging: true`) to confirm it's being hit.
1655
+ - Ensure your `transform` middleware returns a `DeepPartial<T>` or `void`/`Promise<void>`, and `blocking` middleware returns `boolean`/`Promise<boolean>`.
1656
+ - **"Performance warnings in console"**:
1657
+ - 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.
1658
+ - **"Artifact not resolving or re-evaluating"**:
1659
+ - Check for `Circular dependency` errors in the console during `resolve`.
1660
+ - For `Singleton` artifacts, ensure its `state` or `artifact` dependencies are actually changing to trigger re-evaluation. If a dependency changes, the artifact (and its downstream dependents) will be invalidated and re-created on next `resolve`.
1661
+ - For `Transient` artifacts, a new instance is created on every `resolve`.
1450
1662
 
1451
1663
  ### FAQ
1452
1664
 
@@ -1470,12 +1682,12 @@ A: `set` is the foundational method for updating the state, handling the core lo
1470
1682
 
1471
1683
  ### Changelog / Roadmap
1472
1684
 
1473
- * **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.
1474
- * **Roadmap**: Future plans for `@asaidimu/utils-store` may include:
1475
- * More advanced query/selector capabilities with built-in memoization for derived state.
1476
- * Built-in serialization/deserialization options for persistence, perhaps with schema validation.
1477
- * Higher-order middlewares for common patterns (e.g., async data fetching, debouncing updates).
1478
- * Further performance optimizations for very large states or high update frequencies.
1685
+ - **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.
1686
+ - **Roadmap**: Future plans for `@asaidimu/utils-store` may include:
1687
+ - More advanced query/selector capabilities with built-in memoization for derived state.
1688
+ - Built-in serialization/deserialization options for persistence, perhaps with schema validation.
1689
+ - Higher-order middlewares for common patterns (e.g., async data fetching, debouncing updates).
1690
+ - Further performance optimizations for very large states or high update frequencies.
1479
1691
 
1480
1692
  ### License
1481
1693
 
@@ -1483,8 +1695,8 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
1483
1695
 
1484
1696
  ### Acknowledgments
1485
1697
 
1486
- * Inspired by modern state management patterns such as Redux, Zustand, and Vuex, emphasizing immutability and explicit state changes.
1487
- * Leverages the `@asaidimu/events` package for robust internal event bus capabilities.
1488
- * Utilizes the `uuid` library for generating unique instance IDs.
1698
+ - Inspired by modern state management patterns such as Redux, Zustand, and Vuex, emphasizing immutability and explicit state changes.
1699
+ - Leverages the `@asaidimu/events` package for robust internal event bus capabilities.
1700
+ - Utilizes the `uuid` library for generating unique instance IDs.
1489
1701
 
1490
1702
  ---