@asaidimu/utils-store 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,900 @@
1
+ # `@asaidimu/utils-store` - Reactive Data Store
2
+
3
+ A comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware, transactional updates, deep observability, and an optional persistence layer.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-store?style=flat-square)](https://www.npmjs.com/package/@asaidimu/utils-store)
6
+ [![License](https://img.shields.io/npm/l/@asaidimu/utils-store?style=flat-square)](LICENSE)
7
+ [![Build Status](https://img.shields.io/badge/Build-Passing-brightgreen?style=flat-square)](https://github.com/asaidimu/utils-actions?query=workflow%3ACI)
8
+
9
+ ---
10
+
11
+ ### Quick Links
12
+
13
+ - [Overview & Features](#overview--features)
14
+ - [Installation & Setup](#installation--setup)
15
+ - [Usage Documentation](#usage-documentation)
16
+ - [Basic Usage](#basic-usage)
17
+ - [Persistence Integration](#persistence-integration)
18
+ - [Middleware System](#middleware-system)
19
+ - [Transaction Support](#transaction-support)
20
+ - [Store Observability](#store-observability)
21
+ - [Event System](#event-system)
22
+ - [Project Architecture](#project-architecture)
23
+ - [Development & Contributing](#development--contributing)
24
+ - [Additional Information](#additional-information)
25
+
26
+ ---
27
+
28
+ ## Overview & Features
29
+
30
+ `@asaidimu/utils-store` is a powerful and flexible state management solution designed for modern TypeScript applications. It provides a highly performant and observable way to manage your application's data, ensuring type safety and predictability across complex state interactions. Built on principles of immutability and explicit updates, it makes state changes easy to track, debug, and extend.
31
+
32
+ Whether you're building a simple utility or a complex application, this library offers the tools to handle your state with confidence, enabling features like atomic transactions, a pluggable middleware pipeline, and deep runtime introspection for unparalleled debugging capabilities. It emphasizes a component-based design internally, allowing for clear separation of concerns for state management, middleware processing, persistence, and observability.
33
+
34
+ ### Key Features
35
+
36
+ * 📊 **Type-safe State Management**: Full TypeScript support for defining and interacting with your application state, leveraging `DeepPartial<T>` for precise, structural updates.
37
+ * 🔄 **Reactive Updates**: Subscribe to granular changes at specific paths within your state or listen for any change, ensuring efficient re-renders or side effects.
38
+ * 🧠 **Composable Middleware System**:
39
+ * **Transform Middleware**: Modify, normalize, or enrich state updates before they are applied. Return a `DeepPartial<T>` to apply further changes.
40
+ * **Blocking Middleware**: Implement custom validation or authorization logic to prevent invalid state changes from occurring. These middlewares return a boolean.
41
+ * 📦 **Atomic Transaction Support**: Group multiple state updates into a single, atomic operation. If any update within the transaction fails, the entire transaction is rolled back to the state before the transaction began, guaranteeing data integrity.
42
+ * 💾 **Optional Persistence Layer**: Seamlessly integrate with any `SimplePersistence<T>` implementation (e.g., for local storage, IndexedDB, or backend sync) to load and save state, ensuring data durability and synchronization across instances.
43
+ * 🔍 **Deep Observability & Debugging**: An optional `StoreObservability` class provides unparalleled runtime introspection:
44
+ * **Event History**: Keep a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, etc.).
45
+ * **State Snapshots**: Maintain a configurable history of your state over time, allowing for easy inspection of changes between updates.
46
+ * **Time-Travel Debugging**: Undo and redo state changes using the recorded state history, providing powerful capabilities for debugging complex scenarios.
47
+ * **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times, and largest update size to identify bottlenecks.
48
+ * **Console Logging**: Configurable, human-readable logging of store events directly to the browser console for immediate feedback during development.
49
+ * **Pre-built Debugging Middlewares**: Includes helpers to create a generic logging middleware and a validation middleware.
50
+ * ✨ **Efficient Change Detection**: Utilizes a custom `diff` algorithm to identify only the truly changed paths (`string[]`), optimizing listener notifications and ensuring minimal overhead.
51
+ * 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using `Symbol.for("delete")`.
52
+ * ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in order.
53
+
54
+ ---
55
+
56
+ ## Installation & Setup
57
+
58
+ Install the entire collection using your preferred package manager. You can then import specific utilities as needed. This library is designed for browser and Node.js environments.
59
+
60
+ ```bash
61
+ # Using Bun
62
+ bun add @asaidimu/utils-store
63
+ ```
64
+
65
+ ### Prerequisites
66
+
67
+ * Node.js (LTS version recommended)
68
+ * TypeScript (for type-safe development)
69
+
70
+ ### Verification
71
+
72
+ To verify the installation, you can run a simple test script:
73
+
74
+ ```typescript
75
+ // verify.ts
76
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
77
+
78
+ interface MyState {
79
+ count: number;
80
+ message: string;
81
+ }
82
+
83
+ const store = new ReactiveDataStore<MyState>({ count: 0, message: "Hello" });
84
+
85
+ store.subscribe("count", (state) => {
86
+ console.log(`Count changed to: ${state.count}`);
87
+ });
88
+
89
+ await store.set({ count: 1 });
90
+ await store.set({ message: "World" }); // This won't trigger the 'count' listener
91
+
92
+ console.log("Current state:", store.get());
93
+
94
+ // Expected Output:
95
+ // Count changed to: 1
96
+ // Current state: { count: 1, message: "World" }
97
+ ```
98
+
99
+ Run this file:
100
+ ```bash
101
+ npx ts-node verify.ts
102
+ ```
103
+
104
+ ---
105
+
106
+ ## Usage Documentation
107
+
108
+ This section provides practical examples of how to use `@asaidimu/utils-store` to manage your application state.
109
+
110
+ ### Basic Usage
111
+
112
+ Creating a store, getting state, and setting updates.
113
+
114
+ ```typescript
115
+ import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
116
+
117
+ // 1. Define your state interface for type safety
118
+ interface AppState {
119
+ user: {
120
+ id: string;
121
+ name: string;
122
+ email: string;
123
+ isActive: boolean;
124
+ };
125
+ products: Array<{ id: string; name: string; price: number }>;
126
+ settings: {
127
+ theme: 'light' | 'dark';
128
+ notificationsEnabled: boolean;
129
+ };
130
+ lastUpdated: number;
131
+ }
132
+
133
+ // 2. Initialize the store with an initial state
134
+ const initialState: AppState = {
135
+ user: {
136
+ id: '123',
137
+ name: 'Jane Doe',
138
+ email: 'jane@example.com',
139
+ isActive: true,
140
+ },
141
+ products: [
142
+ { id: 'p1', name: 'Laptop', price: 1200 },
143
+ { id: 'p2', name: 'Mouse', price: 25 },
144
+ ],
145
+ settings: {
146
+ theme: 'light',
147
+ notificationsEnabled: true,
148
+ },
149
+ lastUpdated: Date.now(),
150
+ };
151
+
152
+ const store = new ReactiveDataStore<AppState>(initialState);
153
+
154
+ // 3. Get the current state
155
+ const currentState = store.get();
156
+ console.log('Initial state:', currentState);
157
+ // Output: Initial state: { user: { ... }, products: [ ... ], ... }
158
+
159
+ // 4. Update the state using a partial object
160
+ // You can update deeply nested properties without affecting siblings
161
+ await store.set({
162
+ user: {
163
+ name: 'Jane Smith',
164
+ isActive: false,
165
+ },
166
+ settings: {
167
+ theme: 'dark',
168
+ },
169
+ });
170
+
171
+ console.log('State after partial update:', store.get());
172
+ // Output: User name is now 'Jane Smith', isActive is false, theme is 'dark'.
173
+ // Email and products remain unchanged.
174
+
175
+ // 5. Update the state using a function (StateUpdater)
176
+ // This is useful when the new state depends on the current state.
177
+ await store.set((state) => ({
178
+ products: [
179
+ ...state.products, // Keep existing products
180
+ { id: 'p3', name: 'Keyboard', price: 75 }, // Add a new product
181
+ ],
182
+ lastUpdated: Date.now(),
183
+ }));
184
+
185
+ console.log('State after functional update:', store.get().products.length);
186
+ // Output: State after functional update: 3
187
+
188
+ // 6. Subscribing to state changes
189
+ // You can subscribe to the entire state or specific paths.
190
+ const unsubscribeUser = store.subscribe('user', (state) => {
191
+ console.log('User data changed:', state.user);
192
+ });
193
+
194
+ const unsubscribeNotifications = store.subscribe('settings.notificationsEnabled', (state) => {
195
+ console.log('Notifications setting changed:', state.settings.notificationsEnabled);
196
+ });
197
+
198
+ // Subscribe to multiple paths
199
+ const unsubscribeMulti = store.subscribe(['user.name', 'products'], (state) => {
200
+ console.log('User name or products changed:', state.user.name, state.products.length);
201
+ });
202
+
203
+ // Subscribe to any change in the store
204
+ const unsubscribeAll = store.subscribe('', (state) => {
205
+ console.log('Store updated (any path changed).');
206
+ });
207
+
208
+ await store.set({ user: { email: 'jane.smith@example.com' } });
209
+ // Output:
210
+ // User data changed: { id: '123', name: 'Jane Smith', email: 'jane.smith@example.com', isActive: false }
211
+ // User name or products changed: Jane Smith 3
212
+ // Store updated (any path changed).
213
+
214
+ await store.set({ settings: { notificationsEnabled: false } });
215
+ // Output:
216
+ // Notifications setting changed: false
217
+ // Store updated (any path changed).
218
+
219
+ // 7. Unsubscribe from changes
220
+ unsubscribeUser();
221
+ unsubscribeNotifications();
222
+ unsubscribeMulti();
223
+ unsubscribeAll();
224
+
225
+ await store.set({ user: { isActive: true } });
226
+ // No console output from the above listeners after unsubscribing.
227
+
228
+ // 8. Deleting properties
229
+ // Use Symbol.for("delete") to remove a property from the state.
230
+ const deleteSymbol = Symbol.for("delete");
231
+ await store.set({
232
+ user: {
233
+ email: deleteSymbol as DeepPartial<string> // Cast is needed for type inference
234
+ }
235
+ });
236
+ console.log('User email after deletion:', store.get().user.email);
237
+ // Output: User email after deletion: undefined
238
+ ```
239
+
240
+ ### Persistence Integration
241
+
242
+ The `ReactiveDataStore` can integrate with any persistence layer that implements the `SimplePersistence<T>` interface. This allows you to load an initial state and save subsequent changes.
243
+
244
+ ```typescript
245
+ import { ReactiveDataStore, StoreEvent } from '@asaidimu/utils-store';
246
+ import { SimplePersistence } from '@core/persistence'; // Your persistence implementation
247
+
248
+ // Example: A simple in-memory persistence for demonstration
249
+ // In a real app, this would interact with localStorage, IndexedDB, a backend, etc.
250
+ class InMemoryPersistence<T> implements SimplePersistence<T> {
251
+ private data: T | null = null;
252
+ private subscribers: Map<string, (state: T) => void> = new Map();
253
+
254
+ async get(): Promise<T | null> {
255
+ console.log('Persistence: Loading state...');
256
+ return this.data;
257
+ }
258
+
259
+ async set(instanceId: string, state: T): Promise<boolean> {
260
+ console.log(`Persistence: Saving state for instance ${instanceId}...`);
261
+ this.data = state;
262
+ // Simulate external change notification for other instances
263
+ this.subscribers.forEach((callback, subId) => {
264
+ if (subId !== instanceId) { // Don't notify the instance that just saved
265
+ callback(this.data!);
266
+ }
267
+ });
268
+ return true;
269
+ }
270
+
271
+ subscribe(instanceId: string, callback: (state: T) => void): () => void {
272
+ console.log(`Persistence: Subscribing to external changes for instance ${instanceId}`);
273
+ this.subscribers.set(instanceId, callback);
274
+ return () => {
275
+ console.log(`Persistence: Unsubscribing for instance ${instanceId}`);
276
+ this.subscribers.delete(instanceId);
277
+ };
278
+ }
279
+ }
280
+
281
+ interface UserConfig {
282
+ theme: 'light' | 'dark';
283
+ fontSize: number;
284
+ }
285
+
286
+ // Create a persistence instance
287
+ const userConfigPersistence = new InMemoryPersistence<UserConfig>();
288
+
289
+ // Optionally, listen for persistence readiness
290
+ const storeReady = new Promise<void>(resolve => {
291
+ store.onStoreEvent('persistence:ready', () => {
292
+ console.log('Store is ready and persistence is initialized!');
293
+ resolve();
294
+ });
295
+ });
296
+
297
+ // Initialize the store with persistence
298
+ const store = new ReactiveDataStore<UserConfig>(
299
+ { theme: 'light', fontSize: 16 }, // Initial state if no persisted data
300
+ userConfigPersistence // Pass your persistence implementation here
301
+ );
302
+
303
+ console.log('Store initial state:', store.get()); // May reflect initial state if persistence is empty
304
+
305
+ await storeReady; // Wait for persistence to load/initialize
306
+
307
+ // Now update the state, which will trigger persistence.set()
308
+ await store.set({ theme: 'dark' });
309
+ console.log('Current theme:', store.get().theme);
310
+
311
+ // Simulate an external change (e.g., another tab updating the state)
312
+ await userConfigPersistence.set('another-instance-id', { theme: 'light', fontSize: 18 });
313
+ // Store will automatically update its state and notify its listeners.
314
+ console.log('Current theme after external update:', store.get().theme);
315
+ ```
316
+
317
+ ### Middleware System
318
+
319
+ Middleware functions allow you to intercept and modify state updates.
320
+
321
+ #### Transform Middleware
322
+
323
+ These middlewares can transform the `DeepPartial` update or perform side effects. They receive the current state and the incoming partial update, and can return a new partial state to be merged.
324
+
325
+ ```typescript
326
+ import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
327
+
328
+ interface MyState {
329
+ counter: number;
330
+ logs: string[];
331
+ lastAction: string | null;
332
+ }
333
+
334
+ const store = new ReactiveDataStore<MyState>({
335
+ counter: 0,
336
+ logs: [],
337
+ lastAction: null,
338
+ });
339
+
340
+ // Middleware 1: Logger
341
+ // Logs the incoming update before it's processed
342
+ store.use({
343
+ name: 'LoggerMiddleware',
344
+ action: (state, update) => {
345
+ console.log('Middleware: Incoming update:', update);
346
+ // You don't need to return anything if you just want to observe or perform side effects
347
+ },
348
+ });
349
+
350
+ // Middleware 2: Timestamp and Action Tracker
351
+ // Modifies the update to add a timestamp and track the last action
352
+ store.use({
353
+ name: 'TimestampActionMiddleware',
354
+ action: (state, update) => {
355
+ // This middleware transforms the update by adding `lastAction`
356
+ const actionDescription = JSON.stringify(update);
357
+ return {
358
+ lastAction: `Updated at ${new Date().toLocaleTimeString()} with ${actionDescription}`,
359
+ logs: [...state.logs, `Update processed: ${actionDescription}`],
360
+ };
361
+ },
362
+ });
363
+
364
+ // Middleware 3: Counter Incrementor
365
+ // Increments the counter whenever a specific property is updated
366
+ store.use({
367
+ name: 'CounterIncrementMiddleware',
368
+ action: (state, update) => {
369
+ if (update.counter !== undefined && typeof update.counter === 'number') {
370
+ return { counter: state.counter + update.counter };
371
+ }
372
+ // Return original update or void if no transformation needed
373
+ return update;
374
+ },
375
+ });
376
+
377
+ await store.set({ counter: 5 }); // Will increment counter by 5, not set to 5
378
+ // Output:
379
+ // Middleware: Incoming update: { counter: 5 }
380
+ console.log('State after counter set:', store.get());
381
+ // Output: counter: 5 (initial) + 5 (update) = 10, lastAction, logs updated
382
+
383
+ await store.set({ lastAction: 'Manual update' });
384
+ // Output:
385
+ // Middleware: Incoming update: { lastAction: 'Manual update' }
386
+ console.log('State after manual action:', store.get());
387
+ // Output: lastAction will be overwritten by TimestampActionMiddleware logic
388
+ // and a new log entry will be added.
389
+
390
+ // Unuse a middleware by its ID
391
+ const loggerId = store.use({ name: 'AnotherLogger', action: (s, u) => console.log('Another logger', u) });
392
+ await store.set({ counter: 1 });
393
+ store.unuse(loggerId);
394
+ await store.set({ counter: 1 }); // AnotherLogger will not be called now
395
+ ```
396
+
397
+ #### Blocking Middleware
398
+
399
+ These middlewares can prevent an update from proceeding if certain conditions are not met. They return a boolean: `true` to allow, `false` to block. If a blocking middleware throws an error, the update is also blocked.
400
+
401
+ ```typescript
402
+ import { ReactiveDataStore, DeepPartial } from '@asaidimu/utils-store';
403
+
404
+ interface UserProfile {
405
+ name: string;
406
+ age: number;
407
+ isAdmin: boolean;
408
+ }
409
+
410
+ const store = new ReactiveDataStore<UserProfile>({
411
+ name: 'Guest',
412
+ age: 0,
413
+ isAdmin: false,
414
+ });
415
+
416
+ // Blocking middleware: Age validation
417
+ store.use({
418
+ block: true, // Mark as a blocking middleware
419
+ name: 'AgeValidationMiddleware',
420
+ action: (state, update) => {
421
+ if (update.age !== undefined && update.age < 18) {
422
+ console.warn('Blocking update: Age must be 18 or older.');
423
+ return false; // Block the update
424
+ }
425
+ return true; // Allow the update
426
+ },
427
+ });
428
+
429
+ // Blocking middleware: Admin check
430
+ store.use({
431
+ block: true,
432
+ name: 'AdminRestrictionMiddleware',
433
+ action: (state, update) => {
434
+ if (update.isAdmin === true && state.age < 21) {
435
+ console.warn('Blocking update: User must be 21+ to become admin.');
436
+ return false;
437
+ }
438
+ return true;
439
+ },
440
+ });
441
+
442
+ // Attempt to set a valid age
443
+ await store.set({ age: 25 });
444
+ console.log('User age after valid update:', store.get().age); // Output: 25
445
+
446
+ // Attempt to set an invalid age
447
+ await store.set({ age: 16 });
448
+ console.log('User age after invalid update attempt (should be 25):', store.get().age); // Output: 25
449
+
450
+ // Attempt to make user admin under age 21
451
+ await store.set({ age: 20 });
452
+ await store.set({ isAdmin: true });
453
+ console.log('User admin status after failed attempt (should be false):', store.get().isAdmin); // Output: false
454
+
455
+ // Now make user old enough and try again
456
+ await store.set({ age: 25 });
457
+ await store.set({ isAdmin: true });
458
+ console.log('User admin status after successful attempt (should be true):', store.get().isAdmin); // Output: true
459
+ ```
460
+
461
+ ### Transaction Support
462
+
463
+ Use `store.transaction()` to group multiple state updates into a single atomic operation. If an error occurs during the transaction, all changes made within that transaction will be rolled back.
464
+
465
+ ```typescript
466
+ import { ReactiveDataStore } from '@asaidimu/utils-store';
467
+
468
+ interface BankAccount {
469
+ balance: number;
470
+ transactions: string[];
471
+ }
472
+
473
+ const store = new ReactiveDataStore<BankAccount>({
474
+ balance: 1000,
475
+ transactions: ['Initial deposit'],
476
+ });
477
+
478
+ async function transferFunds(
479
+ fromStore: ReactiveDataStore<BankAccount>,
480
+ toStore: ReactiveDataStore<BankAccount>,
481
+ amount: number,
482
+ ) {
483
+ await fromStore.transaction(async () => {
484
+ console.log(`Starting transfer of ${amount}. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
485
+
486
+ // Deduct from sender
487
+ await fromStore.set((state) => {
488
+ if (state.balance < amount) {
489
+ throw new Error('Insufficient funds');
490
+ }
491
+ return {
492
+ balance: state.balance - amount,
493
+ transactions: [...state.transactions, `Debited ${amount}`],
494
+ };
495
+ });
496
+
497
+ // Simulate a delay or another async operation
498
+ await new Promise(resolve => setTimeout(resolve, 50));
499
+
500
+ // Add to receiver
501
+ await toStore.set((state) => ({
502
+ balance: state.balance + amount,
503
+ transactions: [...state.transactions, `Credited ${amount}`],
504
+ }));
505
+
506
+ console.log(`Transfer complete. From: ${fromStore.get().balance}, To: ${toStore.get().balance}`);
507
+ });
508
+ }
509
+
510
+ const accountA = new ReactiveDataStore<BankAccount>({ balance: 500, transactions: [] });
511
+ const accountB = new ReactiveDataStore<BankAccount>({ balance: 200, transactions: [] });
512
+
513
+ // Successful transfer
514
+ try {
515
+ await transferFunds(accountA, accountB, 100);
516
+ console.log('Transfer 1 successful:');
517
+ console.log('Account A:', accountA.get()); // Expected: balance 400
518
+ console.log('Account B:', accountB.get()); // Expected: balance 300
519
+ } catch (error: any) {
520
+ console.error('Transfer 1 failed:', error.message);
521
+ }
522
+
523
+ // Failed transfer (insufficient funds)
524
+ try {
525
+ await transferFunds(accountA, accountB, 1000); // Account A only has 400
526
+ } catch (error: any) {
527
+ console.error('Transfer 2 failed:', error.message);
528
+ } finally {
529
+ console.log('Transfer 2 attempt, state after rollback:');
530
+ console.log('Account A:', accountA.get()); // Expected: balance 400 (rolled back to state before transfer attempt)
531
+ console.log('Account B:', accountB.get()); // Expected: balance 300 (rolled back to state before transfer attempt)
532
+ }
533
+ ```
534
+
535
+ ### Store Observability
536
+
537
+ The `StoreObservability` class provides advanced debugging and monitoring capabilities for any `ReactiveDataStore` instance. It allows you to inspect event history, state changes, and time-travel through your application's state.
538
+
539
+ ```typescript
540
+ import { ReactiveDataStore, StoreObservability } from '@asaidimu/utils-store';
541
+
542
+ interface DebuggableState {
543
+ user: { name: string; status: 'online' | 'offline' };
544
+ messages: string[];
545
+ settings: { debugMode: boolean };
546
+ }
547
+
548
+ const store = new ReactiveDataStore<DebuggableState>({
549
+ user: { name: 'Debugger', status: 'online' },
550
+ messages: [],
551
+ settings: { debugMode: true },
552
+ });
553
+
554
+ // Initialize observability for the store
555
+ const observability = new StoreObservability(store, {
556
+ maxEvents: 100, // Keep up to 100 events in history
557
+ maxStateHistory: 10, // Keep up to 10 state snapshots for time-travel
558
+ enableConsoleLogging: true, // Log events to console for immediate feedback
559
+ logEvents: {
560
+ updates: true, // Log all update lifecycle events
561
+ middleware: true, // Log middleware start/complete/error
562
+ transactions: true, // Log transaction start/complete/error
563
+ },
564
+ performanceThresholds: {
565
+ updateTime: 50, // Warn if an update takes > 50ms
566
+ middlewareTime: 20, // Warn if a middleware takes > 20ms
567
+ },
568
+ });
569
+
570
+ // Perform some state updates
571
+ await store.set({ user: { status: 'offline' } });
572
+ await store.set({ messages: ['Hello World!'] });
573
+ await store.set({ settings: { debugMode: false } });
574
+
575
+ // 1. Get Event History
576
+ console.log('\n--- Event History ---');
577
+ const events = observability.getEventHistory();
578
+ // Events will include: update:start, update:complete (multiple times), middleware:start, middleware:complete, etc.
579
+ events.forEach(event => console.log(`${event.type} at ${new Date(event.timestamp).toLocaleTimeString()}`));
580
+
581
+ // 2. Get State History
582
+ console.log('\n--- State History (Most Recent First) ---');
583
+ const stateSnapshots = observability.getStateHistory();
584
+ stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}:`, snapshot));
585
+
586
+ // 3. Get Recent Changes
587
+ console.log('\n--- Recent State Changes (Diff) ---');
588
+ const recentChanges = observability.getRecentChanges(3); // Show diffs for last 3 changes
589
+ recentChanges.forEach((change, index) => {
590
+ console.log(`Change #${index}:`, {
591
+ timestamp: new Date(change.timestamp).toLocaleTimeString(),
592
+ changedPaths: change.changedPaths,
593
+ from: change.from,
594
+ to: change.to,
595
+ });
596
+ });
597
+
598
+ // 4. Time-Travel Debugging
599
+ console.log('\n--- Time-Travel ---');
600
+ const timeTravel = observability.createTimeTravel();
601
+
602
+ await store.set({ user: { status: 'online' } }); // State 4
603
+ await store.set({ messages: ['First message'] }); // State 3
604
+ await store.set({ messages: ['Second message'] }); // State 2
605
+
606
+ console.log('Current state (latest):', store.get().messages);
607
+
608
+ if (timeTravel.canUndo()) {
609
+ await timeTravel.undo(); // Go back to State 3
610
+ console.log('After undo 1:', store.get().messages);
611
+ }
612
+
613
+ if (timeTravel.canUndo()) {
614
+ await timeTravel.undo(); // Go back to State 4
615
+ console.log('After undo 2:', store.get().messages);
616
+ }
617
+
618
+ if (timeTravel.canRedo()) {
619
+ await timeTravel.redo(); // Go forward to State 3
620
+ console.log('After redo 1:', store.get().messages);
621
+ }
622
+
623
+ // 5. Custom Debugging Middleware
624
+ const loggingMiddleware = observability.createLoggingMiddleware({
625
+ logLevel: 'info',
626
+ logUpdates: true,
627
+ });
628
+ store.use({ name: 'DebugLogging', action: loggingMiddleware });
629
+
630
+ await store.set({ user: { name: 'New User' } }); // This update will be logged by the created middleware.
631
+
632
+ // 6. Disconnect observability when no longer needed to prevent memory leaks
633
+ observability.disconnect();
634
+ ```
635
+
636
+ ### Event System
637
+
638
+ The store emits various events during its lifecycle, allowing for advanced monitoring, logging, and integration with external systems. You can subscribe to these events using `store.onStoreEvent()`.
639
+
640
+ ```typescript
641
+ import { ReactiveDataStore, StoreEvent } from '@asaidimu/utils-store';
642
+
643
+ interface MyState {
644
+ value: number;
645
+ }
646
+
647
+ const store = new ReactiveDataStore<MyState>({ value: 0 });
648
+
649
+ // Subscribe to 'update:start' event
650
+ store.onStoreEvent('update:start', (data) => {
651
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update started.`);
652
+ });
653
+
654
+ // Subscribe to 'update:complete' event
655
+ store.onStoreEvent('update:complete', (data) => {
656
+ if (data.blocked) {
657
+ console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] Update blocked. Error:`, data.error?.message);
658
+ } else {
659
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Update complete. Changed paths: ${data.changedPaths?.join(', ')} (took ${data.duration?.toFixed(2)}ms)`);
660
+ }
661
+ });
662
+
663
+ // Subscribe to middleware events
664
+ store.onStoreEvent('middleware:start', (data) => {
665
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) started.`);
666
+ });
667
+
668
+ store.onStoreEvent('middleware:complete', (data) => {
669
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" (${data.type}) completed in ${data.duration?.toFixed(2)}ms.`);
670
+ });
671
+
672
+ store.onStoreEvent('middleware:error', (data) => {
673
+ console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] Middleware "${data.name}" failed:`, data.error);
674
+ });
675
+
676
+ // Subscribe to transaction events
677
+ store.onStoreEvent('transaction:start', (data) => {
678
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction started.`);
679
+ });
680
+
681
+ store.onStoreEvent('transaction:complete', (data) => {
682
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction complete.`);
683
+ });
684
+
685
+ store.onStoreEvent('transaction:error', (data) => {
686
+ console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] Transaction failed:`, data.error);
687
+ });
688
+
689
+ // Subscribe to persistence events
690
+ store.onStoreEvent('persistence:ready', (data) => {
691
+ console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] Persistence layer is ready.`);
692
+ });
693
+
694
+
695
+ // Add a middleware to demonstrate
696
+ store.use({
697
+ name: 'SampleMiddleware',
698
+ action: (state, update) => {
699
+ return { value: state.value + (update.value || 0) };
700
+ },
701
+ });
702
+
703
+ // Perform operations
704
+ await store.set({ value: 10 });
705
+
706
+ await store.transaction(async () => {
707
+ await store.set({ value: 5 }); // Inside transaction
708
+ if (store.get().value > 15) {
709
+ throw new Error('Value too high!');
710
+ }
711
+ });
712
+
713
+ console.log('Final value:', store.get().value);
714
+ ```
715
+
716
+ ---
717
+
718
+ ## Project Architecture
719
+
720
+ The `@asaidimu/utils-store` library is built with a modular, component-based architecture to promote maintainability, testability, and extensibility.
721
+
722
+ ```
723
+ src/store/
724
+ ├─── diff.ts # Efficient change detection and path derivation
725
+ ├─── metrics.ts # Gathers performance metrics from store events
726
+ ├─── middleware.ts # Manages and executes state transformation and blocking middlewares
727
+ ├─── observability.ts # Optional module for deep debugging, state history, and time-travel
728
+ ├─── persistence.ts # Handles integration with external SimplePersistence implementations
729
+ ├─── state.ts # Core immutable state management and internal change notifications
730
+ ├─── store.ts # The main ReactiveDataStore class, orchestrates all components
731
+ ├─── transactions.ts # Provides atomic state update transactions with rollback
732
+ ├─── types.ts # Central TypeScript type definitions for the entire store
733
+ └─── index.ts # Export barrel file for the store module
734
+ ```
735
+
736
+ ### Core Components
737
+
738
+ * **`ReactiveDataStore<T>` (store.ts)**: The primary entry point and public API for the state management system. It acts as an orchestrator, delegating tasks to specialized internal components. It manages the update queue and public interfaces like `set`, `get`, `subscribe`, `transaction`, `use`, and `onStoreEvent`.
739
+ * **`CoreStateManager<T>` (state.ts)**: Encapsulates the actual, immutable state (`cache`). It's responsible for applying incoming state changes, performing efficient `diff`ing to identify modified paths, and notifying internal listeners (via an `updateBus`) about concrete state changes.
740
+ * **`MiddlewareEngine<T>` (middleware.ts)**: Manages the registration and execution of middleware functions. It differentiates between `blocking` middleware (which can halt an update) and `transform` middleware (which can modify the update payload), ensuring they run in the correct order.
741
+ * **`PersistenceHandler<T>` (persistence.ts)**: Deals with external data persistence. It loads initial state from a provided `SimplePersistence` implementation and saves subsequent state changes. It also listens for external updates from the persistence layer to keep the in-memory state synchronized.
742
+ * **`TransactionManager<T>` (transactions.ts)**: Provides atomic state operations. It creates a snapshot of the state before an `operation` and, if the operation fails, it ensures the state is reverted to this snapshot, guaranteeing data integrity.
743
+ * **`MetricsCollector` (metrics.ts)**: Observes the internal `eventBus` to gather and expose performance metrics of the store, such as update counts, listener executions, and average update times.
744
+ * **`StoreObservability<T>` (observability.ts)**: An optional, but highly recommended, debugging companion. It taps into the `ReactiveDataStore`'s events and state changes to build a comprehensive history of events and state snapshots, enabling time-travel debugging, detailed console logging, and performance monitoring.
745
+ * **`merge` (merge.ts)**: A utility function for deep merging objects, preserving immutability and handling `Symbol.for("delete")` for property removal.
746
+ * **`diff` (diff.ts)**: A utility function that efficiently compares two objects (original and partial) and returns an array of paths that have changed. This is crucial for optimizing listener notifications.
747
+
748
+ ### Data Flow
749
+
750
+ The `ReactiveDataStore` handles state updates in a robust, queued manner.
751
+
752
+ 1. **`store.set(update)` call**:
753
+ * If an update is already in progress (`isUpdating`), the new `update` is queued in `pendingUpdates`.
754
+ * An `update:start` event is emitted.
755
+ 2. **Middleware Execution**:
756
+ * The `MiddlewareEngine` first runs all `blocking` middlewares with the current state and incoming `DeepPartial` update. If any blocking middleware returns `false` or throws an error, the update is halted, an `update:complete` (blocked) event is emitted, and the process stops.
757
+ * If not blocked, `transform` middlewares are executed sequentially. Each transform middleware can modify the update payload.
758
+ 3. **State Application**:
759
+ * The `CoreStateManager` receives the (potentially transformed) update.
760
+ * It performs a `diff` comparison between the current state and the new state to identify all changed paths.
761
+ * If changes are detected, the `CoreStateManager` updates its internal immutable state (`cache`) and then emits an `update` event for each changed path on its internal `updateBus`.
762
+ 4. **Listener Notification**:
763
+ * Any external subscribers (from `store.subscribe()`) whose registered paths match or are parent paths of the `changedPaths` are notified with the latest state.
764
+ 5. **Persistence Handling**:
765
+ * The `PersistenceHandler` receives the `changedPaths` and the new state. If a `SimplePersistence` implementation is configured, it attempts to save the new state.
766
+ 6. **Completion & Queue Processing**:
767
+ * An `update:complete` event is emitted, containing information about the update's duration, changed paths, and any blocking errors.
768
+ * The `isUpdating` flag is reset, and if there are `pendingUpdates`, the next update in the queue is immediately processed.
769
+
770
+ ### Extension Points
771
+
772
+ * **Custom Middleware**: Users can inject their own `Middleware` and `BlockingMiddleware` functions using `store.use()` to customize update logic, add logging, validation, or side effects.
773
+ * **Custom Persistence**: The `SimplePersistence<T>` interface allows developers to integrate the store with any storage solution, whether it's local storage, IndexedDB, a backend API, or a WebSocket connection.
774
+
775
+ ---
776
+
777
+ ## Development & Contributing
778
+
779
+ Contributions are welcome! Follow these guidelines to get started.
780
+
781
+ ### Development Setup
782
+
783
+ 1. **Clone the repository:**
784
+ ```bash
785
+ git clone https://github.com/asaidimu/utils.git
786
+ cd utils
787
+ ```
788
+ 2. **Install dependencies:**
789
+ The `@asaidimu/utils-store` module is part of a monorepo (likely `pnpm` workspaces).
790
+ ```bash
791
+ pnpm install
792
+ # Or if you don't have pnpm:
793
+ # npm install -g pnpm
794
+ # pnpm install
795
+ ```
796
+ 3. **Build the project:**
797
+ Navigate to the `store` package directory and run the build script.
798
+ ```bash
799
+ cd packages/store # (Assuming the store package is in 'packages/store')
800
+ pnpm build # or npm run build
801
+ ```
802
+ Alternatively, you might be able to build the whole monorepo from the root:
803
+ ```bash
804
+ pnpm build # (from the monorepo root)
805
+ ```
806
+
807
+ ### Scripts
808
+
809
+ * `pnpm test`: Runs all unit tests using Vitest.
810
+ * `pnpm test:watch`: Runs tests in watch mode.
811
+ * `pnpm lint`: Lints the codebase using ESLint.
812
+ * `pnpm format`: Formats the code using Prettier.
813
+ * `pnpm build`: Compiles TypeScript to JavaScript.
814
+
815
+ ### Testing
816
+
817
+ All tests are written with [Vitest](https://vitest.dev/).
818
+
819
+ To run tests:
820
+
821
+ ```bash
822
+ pnpm test
823
+ ```
824
+
825
+ To run tests and watch for changes during development:
826
+
827
+ ```bash
828
+ pnpm test:watch
829
+ ```
830
+
831
+ Ensure all new features have comprehensive test coverage and existing tests pass.
832
+
833
+ ### Contributing Guidelines
834
+
835
+ 1. **Fork the repository** and create your branch from `main`.
836
+ 2. **Ensure code quality**: Write clean, readable, and maintainable code. Adhere to existing coding styles.
837
+ 3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all existing tests pass.
838
+ 4. **Commit messages**: Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for clear and consistent commit history (e.g., `feat: add new feature`, `fix: resolve bug`).
839
+ 5. **Pull Requests**: Open a pull request to the `main` branch. Describe your changes clearly and link to any relevant issues.
840
+
841
+ ### Issue Reporting
842
+
843
+ If you find a bug or have a feature request, please open an issue on the [GitHub repository](https://github.com/asaidimu/utils-issues).
844
+ * For bugs, include steps to reproduce, expected behavior, and actual behavior.
845
+ * For feature requests, describe the use case and proposed solution.
846
+
847
+ ---
848
+
849
+ ## Additional Information
850
+
851
+ ### Troubleshooting
852
+
853
+ * **"Update not triggering listeners"**:
854
+ * Ensure you are subscribing to the correct path. `store.subscribe('user.name', ...)` will not trigger if you update `user.email`.
855
+ * If the new value is strictly equal (`===`) to the old value, no change will be detected, and listeners will not be notified. The `diff` function effectively handles this.
856
+ * Verify your `DeepPartial` update correctly targets the intended part of the state.
857
+ * **"State not rolling back after transaction error"**:
858
+ * Ensure the error is thrown *within* the `transaction` callback. Errors outside will not be caught by the transaction manager.
859
+ * Promises within the transaction *must* be `await`ed so the transaction manager can capture potential rejections.
860
+ * **"Middleware not being applied"**:
861
+ * Verify the middleware is registered with `store.use()` *before* the `set` operation it should affect.
862
+ * Check middleware `name` if debugging.
863
+ * Ensure your middleware returns `DeepPartial<T>` or `void`/`Promise<void>` for transform middleware, and `boolean`/`Promise<boolean>` for blocking middleware.
864
+ * **"Performance warnings in console"**:
865
+ * If `StoreObservability` is enabled, it will warn about updates or middlewares exceeding defined `performanceThresholds`. This is a warning, not an error. Consider optimizing the specific updates or middlewares identified.
866
+
867
+ ### FAQ
868
+
869
+ **Q: How are arrays handled during updates?**
870
+ A: Arrays are treated as primitive values. When you provide an array in a `DeepPartial` update, the entire array at that path is replaced, not merged. This ensures predictable behavior and prevents complex partial array merging logic.
871
+
872
+ **Q: What is `Symbol.for("delete")`?**
873
+ A: It's a special symbol used to explicitly remove a property from the state during a `set` operation. If you pass `Symbol.for("delete")` as the value for a key in your `DeepPartial` update, that key will be removed from the state.
874
+
875
+ **Q: How do I debug my store's state changes?**
876
+ A: The `StoreObservability` class is your primary tool. Instantiate it with your `ReactiveDataStore` instance. It provides methods to get event history, state snapshots, and even time-travel debugging capabilities. Enabling `enableConsoleLogging: true` in `StoreObservability` options provides immediate, formatted console output.
877
+
878
+ **Q: What is `SimplePersistence<T>`?**
879
+ A: It's a simple interface (`{ get(): Promise<T | null>; set(instanceId: string, state: T): Promise<boolean>; subscribe(instanceId: string, listener: (state: T) => void): () => void; }`) that defines the contract for any persistence layer. You need to provide an implementation of this interface to enable state saving and loading.
880
+
881
+ **Q: Can I use this with React/Vue/Angular?**
882
+ A: Yes. This is a framework-agnostic state management library. You would typically use the `subscribe` method within your framework's lifecycle hooks (e.g., `useEffect` in React) to react to state changes and update your UI components.
883
+
884
+ ### Changelog / Roadmap
885
+
886
+ * **Changelog**: For detailed version history, please refer to the [CHANGELOG.md](CHANGELOG.md) file.
887
+ * **Roadmap**: Future plans may include:
888
+ * Plugins for common integrations (e.g., React hooks, Redux DevTools extension compatibility).
889
+ * More advanced query/selector capabilities.
890
+ * Built-in serialization options for persistence.
891
+
892
+ ### License
893
+
894
+ This project is licensed under the [MIT License](LICENSE).
895
+
896
+ ### Acknowledgments
897
+
898
+ * Inspired by modern state management patterns and principles of immutability.
899
+ * Uses `@asaidimu/events` for the internal event bus.
900
+ * Utilizes `uuid` for unique instance IDs.