@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/LICENSE.md +21 -0
- package/README.md +900 -0
- package/index.d.mts +286 -0
- package/index.d.ts +286 -0
- package/index.js +1295 -0
- package/index.mjs +1280 -0
- package/package.json +67 -0
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
|
+
[](https://www.npmjs.com/package/@asaidimu/utils-store)
|
6
|
+
[](LICENSE)
|
7
|
+
[](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.
|