@asaidimu/utils-store 2.3.2 → 4.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 +728 -93
- package/index.d.mts +566 -197
- package/index.d.ts +566 -197
- package/index.js +26 -1
- package/index.mjs +25 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# `@asaidimu/utils-store`
|
|
2
2
|
|
|
3
|
-
**A Reactive Data Store for TypeScript Applications**
|
|
3
|
+
**A Type-Safe Reactive Data Store for TypeScript Applications**
|
|
4
4
|
|
|
5
|
-
A comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware,
|
|
5
|
+
A comprehensive, type-safe, and reactive state management library for TypeScript applications, featuring robust middleware, atomic transactions, dependency injection for services (artifacts), deep observability, and an optional persistence layer. It simplifies complex state interactions by promoting immutability, explicit updates, and a modular design.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@asaidimu/utils-store)
|
|
8
8
|
[](https://github.com/asaidimu/erp-utils/blob/main/LICENSE)
|
|
@@ -16,9 +16,13 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
|
|
|
16
16
|
- [Installation & Setup](#installation--setup)
|
|
17
17
|
- [Usage Documentation](#usage-documentation)
|
|
18
18
|
- [Basic Usage](#basic-usage)
|
|
19
|
+
- [Actions & Dispatch](#actions--dispatch)
|
|
20
|
+
- [Reactive Selectors](#reactive-selectors)
|
|
19
21
|
- [Persistence Integration](#persistence-integration)
|
|
20
22
|
- [Middleware System](#middleware-system)
|
|
21
23
|
- [Transaction Support](#transaction-support)
|
|
24
|
+
- [Artifacts (Dependency Injection)](#artifacts-dependency-injection)
|
|
25
|
+
- [React Integration](#react-integration)
|
|
22
26
|
- [Store Observer (Debugging & Observability)](#store-observer-debugging--observability)
|
|
23
27
|
- [Event System](#event-system)
|
|
24
28
|
- [Project Architecture](#project-architecture)
|
|
@@ -31,25 +35,30 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
|
|
|
31
35
|
|
|
32
36
|
`@asaidimu/utils-store` is a powerful and flexible state management solution designed for modern TypeScript applications. It provides a highly performant and observable way to manage your application's data, ensuring type safety and predictability across complex state interactions. Built on principles of immutability and explicit updates, it makes state changes easy to track, debug, and extend.
|
|
33
37
|
|
|
34
|
-
This library offers robust tools to handle your state with confidence, enabling features like atomic transactions, a pluggable middleware pipeline, and deep runtime introspection for unparalleled debugging capabilities. It emphasizes a component-based design internally, allowing for clear separation of concerns for core state management, middleware processing, persistence, and observability.
|
|
38
|
+
This library offers robust tools to handle your state with confidence, enabling features like atomic transactions, a pluggable middleware pipeline, a powerful dependency injection system for services (artifacts), and deep runtime introspection for unparalleled debugging capabilities. It emphasizes a component-based design internally, allowing for clear separation of concerns for core state management, middleware processing, persistence, and observability.
|
|
35
39
|
|
|
36
40
|
### Key Features
|
|
37
41
|
|
|
38
42
|
* 📊 **Type-safe State Management**: Full TypeScript support for defining and interacting with your application state, leveraging `DeepPartial<T>` for precise, structural updates while maintaining strong type inference.
|
|
39
43
|
* 🔄 **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.
|
|
44
|
+
* 🚀 **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.
|
|
45
|
+
* 🔍 **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.
|
|
40
46
|
* 🧠 **Composable Middleware System**:
|
|
41
47
|
* **Transform Middleware**: Intercept, modify, normalize, or enrich state updates before they are applied. These can return a `DeepPartial<T>` to apply further changes or `void` for side effects.
|
|
42
48
|
* **Blocking Middleware**: Implement custom validation, authorization, or other conditional logic to prevent invalid state changes from occurring. If a blocking middleware returns `false` or throws an error, the update is immediately halted and rolled back.
|
|
43
49
|
* 📦 **Atomic Transaction Support**: Group multiple state updates into a single, atomic operation. If any update within the transaction fails or an error is thrown, the entire transaction is rolled back to the state before the transaction began, guaranteeing data integrity. Supports both synchronous and asynchronous operations.
|
|
44
|
-
* 💾 **Optional Persistence Layer**: Seamlessly integrate with any `SimplePersistence<T>` implementation (e.g., for local storage, IndexedDB, or backend synchronization) to load an initial state and save subsequent changes. The store emits a `persistence:ready` event and listens for external updates.
|
|
45
|
-
*
|
|
46
|
-
|
|
50
|
+
* 💾 **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.
|
|
51
|
+
* 🧩 **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.
|
|
52
|
+
* ⚛️ **React Integration**: Provides a `createStore` factory function that returns a custom React hook (`useStore`). This hook offers `select` (memoized selector hook), `actions` (bound action dispatchers), and `resolve` (reactive artifact resolver) for seamless integration with React components. It leverages React's `useSyncExternalStore` for optimal performance.
|
|
53
|
+
* 👀 **Deep Observer & Debugging (`StoreObserver`)**: An optional but highly recommended class for unparalleled runtime introspection and debugging:
|
|
54
|
+
* **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.).
|
|
47
55
|
* **State Snapshots**: Maintains a configurable history of your application's state over time, allowing for easy inspection of changes between updates and post-mortem analysis.
|
|
48
56
|
* **Time-Travel Debugging**: Leverage the recorded state history to `undo` and `redo` state changes, providing powerful capabilities for debugging complex asynchronous flows and state transitions.
|
|
49
57
|
* **Performance Metrics**: Track real-time performance indicators like total update count, listener executions, average update times, largest update size, and slow operation warnings to identify bottlenecks.
|
|
50
58
|
* **Configurable Console Logging**: Provides human-readable, color-coded logging of store events directly to the browser console for immediate feedback during development.
|
|
51
59
|
* **Pre-built Debugging Middlewares**: Includes helper methods to easily create a generic logging middleware and a validation middleware for immediate use.
|
|
52
|
-
*
|
|
60
|
+
* **Session Management**: Save and load observer sessions for offline analysis or sharing bug reproductions.
|
|
61
|
+
* 🗑️ **Property Deletion**: Supports explicit property deletion within partial updates using the global `Symbol.for("delete")` or a custom marker.
|
|
53
62
|
* ⚡ **Concurrency Handling**: Automatically queues and processes `set` updates to prevent race conditions during concurrent calls, ensuring updates are applied in a predictable, sequential order.
|
|
54
63
|
|
|
55
64
|
---
|
|
@@ -72,7 +81,8 @@ yarn add @asaidimu/utils-store
|
|
|
72
81
|
### Prerequisites
|
|
73
82
|
|
|
74
83
|
* **Node.js**: (LTS version recommended) for development and compilation.
|
|
75
|
-
* **TypeScript**: (v4.0+ recommended) for full type-safety during development.
|
|
84
|
+
* **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`.
|
|
85
|
+
* **React**: (v16.8+ for hooks) if using the `@asaidimu/utils-store/react` integration.
|
|
76
86
|
|
|
77
87
|
### Verification
|
|
78
88
|
|
|
@@ -90,12 +100,12 @@ interface MyState {
|
|
|
90
100
|
const store = new ReactiveDataStore<MyState>({ count: 0, message: "Hello" });
|
|
91
101
|
|
|
92
102
|
// Subscribing to "count" will only log when 'count' path changes
|
|
93
|
-
store.
|
|
103
|
+
store.watch("count", (state) => {
|
|
94
104
|
console.log(`Count changed to: ${state.count}`);
|
|
95
105
|
});
|
|
96
106
|
|
|
97
107
|
// Subscribing to "" (empty string) will log for any store update
|
|
98
|
-
store.
|
|
108
|
+
store.watch("", (state) => {
|
|
99
109
|
console.log(`Store updated to: ${JSON.stringify(state)}`);
|
|
100
110
|
});
|
|
101
111
|
|
|
@@ -133,14 +143,14 @@ This section provides practical examples and detailed explanations of how to use
|
|
|
133
143
|
Learn how to create a store, read state, and update state with partial objects or functions.
|
|
134
144
|
|
|
135
145
|
```typescript
|
|
136
|
-
import { ReactiveDataStore, type DeepPartial } from '@asaidimu/utils-store';
|
|
146
|
+
import { ReactiveDataStore, DELETE_SYMBOL, type DeepPartial } from '@asaidimu/utils-store';
|
|
137
147
|
|
|
138
148
|
// 1. Define your state interface for type safety
|
|
139
149
|
interface AppState {
|
|
140
150
|
user: {
|
|
141
151
|
id: string;
|
|
142
152
|
name: string;
|
|
143
|
-
email
|
|
153
|
+
email?: string; // email is optional as we'll delete it
|
|
144
154
|
isActive: boolean;
|
|
145
155
|
};
|
|
146
156
|
products: Array<{ id: string; name: string; price: number }>;
|
|
@@ -223,21 +233,21 @@ console.log('State after functional update, products count:', store.get().produc
|
|
|
223
233
|
|
|
224
234
|
// 6. Subscribing to state changes
|
|
225
235
|
// You can subscribe to the entire state (path: '') or specific paths (e.g., 'user.name', 'settings.notificationsEnabled').
|
|
226
|
-
const unsubscribeUser = store.
|
|
236
|
+
const unsubscribeUser = store.watch('user', (state) => {
|
|
227
237
|
console.log('User data changed:', state.user);
|
|
228
238
|
});
|
|
229
239
|
|
|
230
|
-
const unsubscribeNotifications = store.
|
|
240
|
+
const unsubscribeNotifications = store.watch('settings.notificationsEnabled', (state) => {
|
|
231
241
|
console.log('Notifications setting changed:', state.settings.notificationsEnabled);
|
|
232
242
|
});
|
|
233
243
|
|
|
234
244
|
// Subscribe to multiple paths at once
|
|
235
|
-
const unsubscribeMulti = store.
|
|
245
|
+
const unsubscribeMulti = store.watch(['user.name', 'products'], (state) => {
|
|
236
246
|
console.log('User name or products changed:', state.user.name, state.products.length);
|
|
237
247
|
});
|
|
238
248
|
|
|
239
249
|
// Subscribe to any change in the store (root listener)
|
|
240
|
-
const unsubscribeAll = store.
|
|
250
|
+
const unsubscribeAll = store.watch('', (state) => {
|
|
241
251
|
console.log('Store updated (any path changed). Current products count:', state.products.length);
|
|
242
252
|
});
|
|
243
253
|
|
|
@@ -264,31 +274,238 @@ await store.set({ user: { isActive: true } });
|
|
|
264
274
|
// No console output from the above listeners after unsubscribing.
|
|
265
275
|
|
|
266
276
|
// 8. Deleting properties
|
|
267
|
-
// Use `
|
|
268
|
-
// Note: You might need a type cast (e.g., `as DeepPartial<string>`) for TypeScript if strict type checking is enabled.
|
|
269
|
-
const DELETE_ME = Symbol.for("delete");
|
|
277
|
+
// Use `DELETE_SYMBOL` (exported from the library) to explicitly remove a property from the state.
|
|
270
278
|
await store.set({
|
|
271
279
|
user: {
|
|
272
|
-
email:
|
|
280
|
+
email: DELETE_SYMBOL as DeepPartial<string> // Type cast is needed for strict TypeScript environments
|
|
273
281
|
}
|
|
274
282
|
});
|
|
275
283
|
console.log('User email after deletion:', store.get().user.email);
|
|
276
284
|
// Output: User email after deletion: undefined
|
|
277
285
|
```
|
|
278
286
|
|
|
287
|
+
### Actions & Dispatch
|
|
288
|
+
|
|
289
|
+
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.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
293
|
+
|
|
294
|
+
interface CounterState {
|
|
295
|
+
value: number;
|
|
296
|
+
history: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const store = new ReactiveDataStore<CounterState>({ value: 0, history: [] });
|
|
300
|
+
|
|
301
|
+
// 1. Register an action
|
|
302
|
+
store.register({
|
|
303
|
+
name: 'incrementCounter',
|
|
304
|
+
fn: (state, amount: number) => ({ // Action function receives state and dispatched parameters
|
|
305
|
+
value: state.value + amount,
|
|
306
|
+
history: [...state.history, `Incremented by ${amount} at ${new Date().toLocaleTimeString()}`],
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// 2. Register an action with debounce
|
|
311
|
+
store.register({
|
|
312
|
+
name: 'incrementCounterDebounced',
|
|
313
|
+
fn: (state, amount: number) => ({
|
|
314
|
+
value: state.value + amount,
|
|
315
|
+
history: [...state.history, `Debounced Increment by ${amount} at ${new Date().toLocaleTimeString()}`],
|
|
316
|
+
}),
|
|
317
|
+
debounce: {
|
|
318
|
+
delay: 100, // Debounce for 100ms
|
|
319
|
+
// Optional: condition to decide whether to debounce.
|
|
320
|
+
// Here, we always debounce if `amount` is different from previous.
|
|
321
|
+
condition: (previousArgs, currentArgs) => previousArgs?.[0] !== currentArgs[0],
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 3. Register an async action
|
|
326
|
+
store.register({
|
|
327
|
+
name: 'loadInitialValue',
|
|
328
|
+
fn: async (state, userId: string) => {
|
|
329
|
+
console.log(`Simulating loading initial value for user: ${userId}`);
|
|
330
|
+
await new Promise(resolve => setTimeout(resolve, 200)); // Simulate API call
|
|
331
|
+
return {
|
|
332
|
+
value: 100, // Fetched value
|
|
333
|
+
history: [...state.history, `Loaded initial value for ${userId}`],
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// 4. Dispatch actions
|
|
339
|
+
console.log('Initial value:', store.get().value); // 0
|
|
340
|
+
|
|
341
|
+
await store.dispatch('incrementCounter', 5);
|
|
342
|
+
console.log('Value after incrementCounter(5):', store.get().value); // 5
|
|
343
|
+
|
|
344
|
+
await store.dispatch('incrementCounter', 10);
|
|
345
|
+
console.log('Value after incrementCounter(10):', store.get().value); // 15
|
|
346
|
+
|
|
347
|
+
// Dispatch debounced actions multiple times quickly
|
|
348
|
+
store.dispatch('incrementCounterDebounced', 1);
|
|
349
|
+
store.dispatch('incrementCounterDebounced', 2);
|
|
350
|
+
store.dispatch('incrementCounterDebounced', 3);
|
|
351
|
+
console.log('Value after immediate debounced dispatches (still 15, waiting for debounce):', store.get().value);
|
|
352
|
+
|
|
353
|
+
// Wait for the debounce to complete
|
|
354
|
+
await new Promise(resolve => setTimeout(150, resolve));
|
|
355
|
+
console.log('Value after debounced dispatches settled (only last one applied):', store.get().value); // 15 + 3 = 18
|
|
356
|
+
|
|
357
|
+
// Dispatch an async action
|
|
358
|
+
await store.dispatch('loadInitialValue', 'user-abc');
|
|
359
|
+
console.log('Value after loadInitialValue:', store.get().value); // 100 (overwritten by fetched value)
|
|
360
|
+
console.log('History:', store.get().history);
|
|
361
|
+
|
|
362
|
+
// 5. Deregister an action
|
|
363
|
+
const deregisterRisky = store.register({
|
|
364
|
+
name: 'riskyAction',
|
|
365
|
+
fn: () => { throw new Error('Risky action!'); }
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
deregisterRisky(); // Action is now removed
|
|
369
|
+
|
|
370
|
+
try {
|
|
371
|
+
await store.dispatch('riskyAction');
|
|
372
|
+
} catch (error: any) {
|
|
373
|
+
console.error('Expected error when dispatching deregistered action:', error.message);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Reactive Selectors
|
|
378
|
+
|
|
379
|
+
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.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { ReactiveDataStore } from '@asaidimu/utils-store';
|
|
383
|
+
|
|
384
|
+
interface UserProfile {
|
|
385
|
+
firstName: string;
|
|
386
|
+
lastName: string;
|
|
387
|
+
email: string;
|
|
388
|
+
address: {
|
|
389
|
+
city: string;
|
|
390
|
+
zipCode: string;
|
|
391
|
+
};
|
|
392
|
+
isActive: boolean;
|
|
393
|
+
friends: string[];
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const store = new ReactiveDataStore<UserProfile>({
|
|
397
|
+
firstName: 'John',
|
|
398
|
+
lastName: 'Doe',
|
|
399
|
+
email: 'john.doe@example.com',
|
|
400
|
+
address: {
|
|
401
|
+
city: 'New York',
|
|
402
|
+
zipCode: '10001',
|
|
403
|
+
},
|
|
404
|
+
isActive: true,
|
|
405
|
+
friends: ['Alice', 'Bob'],
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Create a reactive selector for the full name
|
|
409
|
+
const selectFullName = store.select((state) => `${state.firstName} ${state.lastName}`);
|
|
410
|
+
|
|
411
|
+
// Create a reactive selector for user's location
|
|
412
|
+
const selectLocation = store.select((state) => `${state.address.city}, ${state.address.zipCode}`);
|
|
413
|
+
|
|
414
|
+
// Create a reactive selector for user's active status
|
|
415
|
+
const selectIsActive = store.select((state) => state.isActive);
|
|
416
|
+
|
|
417
|
+
// Create a reactive selector for the number of friends
|
|
418
|
+
const selectFriendCount = store.select((state) => state.friends.length);
|
|
419
|
+
|
|
420
|
+
let lastFullName = '';
|
|
421
|
+
let lastLocation = '';
|
|
422
|
+
let lastIsActive = false;
|
|
423
|
+
let lastFriendCount = 0;
|
|
424
|
+
|
|
425
|
+
// Subscribe to the full name selector
|
|
426
|
+
const unsubscribeFullName = selectFullName.subscribe((fullName) => {
|
|
427
|
+
lastFullName = fullName;
|
|
428
|
+
console.log('Full Name Changed:', lastFullName);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Subscribe to the location selector
|
|
432
|
+
const unsubscribeLocation = selectLocation.subscribe((location) => {
|
|
433
|
+
lastLocation = location;
|
|
434
|
+
console.log('Location Changed:', lastLocation);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Subscribe to the active status selector
|
|
438
|
+
const unsubscribeIsActive = selectIsActive.subscribe((isActive) => {
|
|
439
|
+
lastIsActive = isActive;
|
|
440
|
+
console.log('Is Active Changed:', lastIsActive);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Subscribe to the friend count selector
|
|
444
|
+
const unsubscribeFriendCount = selectFriendCount.subscribe((count) => {
|
|
445
|
+
lastFriendCount = count;
|
|
446
|
+
console.log('Friend Count Changed:', lastFriendCount);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
// Get initial values
|
|
451
|
+
lastFullName = selectFullName.get();
|
|
452
|
+
lastLocation = selectLocation.get();
|
|
453
|
+
lastIsActive = selectIsActive.get();
|
|
454
|
+
lastFriendCount = selectFriendCount.get();
|
|
455
|
+
console.log('Initial Full Name:', lastFullName);
|
|
456
|
+
console.log('Initial Location:', lastLocation);
|
|
457
|
+
console.log('Initial Is Active:', lastIsActive);
|
|
458
|
+
console.log('Initial Friend Count:', lastFriendCount);
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
console.log('\n--- Performing Updates ---');
|
|
462
|
+
|
|
463
|
+
// Update first name (should trigger selectFullName)
|
|
464
|
+
await store.set({ firstName: 'Jane' });
|
|
465
|
+
// Output: Full Name Changed: Jane Doe
|
|
466
|
+
|
|
467
|
+
await store.set({ lastName: 'Smith' }); // Should trigger selectFullName
|
|
468
|
+
// Output: Full Name Changed: Jane Smith
|
|
469
|
+
|
|
470
|
+
await store.set({ address: { city: 'Los Angeles' } }); // Should trigger selectLocation
|
|
471
|
+
// Output: Location Changed: Los Angeles, 10001
|
|
472
|
+
|
|
473
|
+
await store.set({ isActive: false }); // Should trigger selectIsActive
|
|
474
|
+
// Output: Is Active Changed: false
|
|
475
|
+
|
|
476
|
+
await store.set({ email: 'jane.smith@example.com' }); // Should NOT trigger any of the above selectors directly
|
|
477
|
+
// No output from subscribed selectors
|
|
478
|
+
|
|
479
|
+
await store.set((state) => ({ friends: [...state.friends, 'Charlie'] })); // Should trigger selectFriendCount
|
|
480
|
+
// Output: Friend Count Changed: 3
|
|
481
|
+
|
|
482
|
+
console.log('\n--- Current Values ---');
|
|
483
|
+
console.log('Current Full Name:', selectFullName.get());
|
|
484
|
+
console.log('Current Location:', selectLocation.get());
|
|
485
|
+
console.log('Current Is Active:', selectIsActive.get());
|
|
486
|
+
console.log('Current Friend Count:', selectFriendCount.get());
|
|
487
|
+
|
|
488
|
+
// Cleanup
|
|
489
|
+
unsubscribeFullName();
|
|
490
|
+
unsubscribeLocation();
|
|
491
|
+
unsubscribeIsActive();
|
|
492
|
+
unsubscribeFriendCount();
|
|
493
|
+
```
|
|
494
|
+
|
|
279
495
|
### Persistence Integration
|
|
280
496
|
|
|
281
|
-
The `ReactiveDataStore` can integrate with any persistence layer that implements the `SimplePersistence<T>` interface. This allows you to load an initial state and automatically save subsequent changes. The store emits a `persistence:ready` event once the persistence layer has loaded any initial state.
|
|
497
|
+
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.
|
|
282
498
|
|
|
283
499
|
```typescript
|
|
284
500
|
import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
|
|
501
|
+
import { v4 as uuidv4 } from 'uuid'; // For generating unique instance IDs
|
|
285
502
|
|
|
286
503
|
// Define the SimplePersistence interface (from `@asaidimu/utils-persistence` or your own)
|
|
287
504
|
interface SimplePersistence<T extends object> {
|
|
288
|
-
get(): Promise<T | null>;
|
|
289
|
-
set(instanceId: string, state: T): Promise<boolean>;
|
|
290
|
-
subscribe(instanceId: string, listener: (state: T) => void): () => void;
|
|
291
|
-
|
|
505
|
+
get(): T | null | Promise<T | null>; // Can be synchronous or asynchronous
|
|
506
|
+
set(instanceId: string, state: T): boolean | Promise<boolean>; // Can be synchronous or asynchronous
|
|
507
|
+
subscribe?(instanceId: string, listener: (state: T) => void): () => void; // Optional: for external change detection
|
|
508
|
+
clear?(): boolean | Promise<boolean>; // Optional: for clearing persisted data
|
|
292
509
|
}
|
|
293
510
|
|
|
294
511
|
// Example: A simple in-memory persistence for demonstration
|
|
@@ -297,56 +514,63 @@ class InMemoryPersistence<T extends object> implements SimplePersistence<T> {
|
|
|
297
514
|
private data: T | null = null;
|
|
298
515
|
// Using a map to simulate different instances subscribing to common data
|
|
299
516
|
private subscribers: Map<string, (state: T) => void> = new Map();
|
|
517
|
+
private uniqueStoreId: string; // Acts as the storageKey/store identifier
|
|
300
518
|
|
|
301
|
-
constructor(initialData: T | null = null) {
|
|
519
|
+
constructor(uniqueStoreId: string, initialData: T | null = null) {
|
|
520
|
+
this.uniqueStoreId = uniqueStoreId;
|
|
302
521
|
this.data = initialData;
|
|
303
522
|
}
|
|
304
523
|
|
|
305
|
-
|
|
306
|
-
console.log(
|
|
524
|
+
get(): T | null { // Synchronous get for simplicity
|
|
525
|
+
console.log(`Persistence [${this.uniqueStoreId}]: Loading state...`);
|
|
307
526
|
return this.data ? structuredClone(this.data) : null;
|
|
308
527
|
}
|
|
309
528
|
|
|
310
529
|
async set(instanceId: string, state: T): Promise<boolean> {
|
|
311
|
-
console.log(`Persistence: Saving state for instance ${instanceId}...`);
|
|
530
|
+
console.log(`Persistence [${this.uniqueStoreId}]: Saving state for instance ${instanceId}...`);
|
|
312
531
|
this.data = structuredClone(state); // Store a clone
|
|
313
532
|
// Simulate external change notification for *other* instances
|
|
314
533
|
this.subscribers.forEach((callback, subId) => {
|
|
315
534
|
// Only notify other instances, not the one that just saved
|
|
316
535
|
if (subId !== instanceId) {
|
|
317
|
-
|
|
536
|
+
// Ensure to queue the microtask for async notification to prevent synchronous re-entry issues
|
|
537
|
+
queueMicrotask(() => callback(structuredClone(this.data!))); // Pass a clone to prevent mutation
|
|
318
538
|
}
|
|
319
539
|
});
|
|
320
540
|
return true;
|
|
321
541
|
}
|
|
322
542
|
|
|
323
543
|
subscribe(instanceId: string, callback: (state: T) => void): () => void {
|
|
324
|
-
console.log(`Persistence: Subscribing to external changes for instance ${instanceId}`);
|
|
544
|
+
console.log(`Persistence [${this.uniqueStoreId}]: Subscribing to external changes for instance ${instanceId}`);
|
|
325
545
|
this.subscribers.set(instanceId, callback);
|
|
326
546
|
return () => {
|
|
327
|
-
console.log(`Persistence: Unsubscribing for instance ${instanceId}`);
|
|
547
|
+
console.log(`Persistence [${this.uniqueStoreId}]: Unsubscribing for instance ${instanceId}`);
|
|
328
548
|
this.subscribers.delete(instanceId);
|
|
329
549
|
};
|
|
330
550
|
}
|
|
331
551
|
}
|
|
332
552
|
|
|
333
553
|
interface UserConfig {
|
|
334
|
-
theme: 'light' | 'dark';
|
|
554
|
+
theme: 'light' | 'dark' | 'system';
|
|
335
555
|
fontSize: number;
|
|
336
556
|
}
|
|
337
557
|
|
|
338
558
|
// Create a persistence instance, possibly with some pre-existing data
|
|
339
|
-
|
|
559
|
+
// The 'my-user-config' string acts as the unique identifier for this particular data set in persistence
|
|
560
|
+
const userConfigPersistence = new InMemoryPersistence<UserConfig>('my-user-config', { theme: 'dark', fontSize: 18 });
|
|
340
561
|
|
|
341
562
|
// Initialize the store with persistence
|
|
342
563
|
const store = new ReactiveDataStore<UserConfig>(
|
|
343
564
|
{ theme: 'light', fontSize: 16 }, // Initial state if no persisted data found (or if persistence is not used)
|
|
344
|
-
userConfigPersistence // Pass your persistence implementation here
|
|
565
|
+
userConfigPersistence, // Pass your persistence implementation here
|
|
566
|
+
// You can also pass persistence options like retries and delay
|
|
567
|
+
undefined, // Use default DELETE_SYMBOL
|
|
568
|
+
{ persistenceMaxRetries: 5, persistenceRetryDelay: 2000 }
|
|
345
569
|
);
|
|
346
570
|
|
|
347
571
|
// Optionally, listen for persistence readiness (important for UIs that depend on loaded state)
|
|
348
572
|
const storeReadyPromise = new Promise<void>(resolve => {
|
|
349
|
-
store.
|
|
573
|
+
store.on('persistence:ready', (data) => {
|
|
350
574
|
console.log(`Store is ready and persistence is initialized! Timestamp: ${new Date(data.timestamp).toLocaleTimeString()}`);
|
|
351
575
|
resolve();
|
|
352
576
|
});
|
|
@@ -370,7 +594,7 @@ console.log('Current theme:', store.get().theme);
|
|
|
370
594
|
// Simulate an external change (e.g., another tab or process updating the state)
|
|
371
595
|
// Note: The `instanceId` here should be different from the store's `store.id()`
|
|
372
596
|
// to simulate an external change and trigger the store's internal persistence subscription.
|
|
373
|
-
await userConfigPersistence.set(
|
|
597
|
+
await userConfigPersistence.set(uuidv4(), { theme: 'system', fontSize: 20 });
|
|
374
598
|
// The store will automatically update its state and notify its listeners due to the internal subscription.
|
|
375
599
|
console.log('Current theme after external update:', store.get().theme);
|
|
376
600
|
// Output: Current theme after external update: system
|
|
@@ -442,8 +666,8 @@ store.use({
|
|
|
442
666
|
if (typeof update.counter === 'number') {
|
|
443
667
|
return { counter: state.counter + update.counter };
|
|
444
668
|
}
|
|
445
|
-
// Return the original update or
|
|
446
|
-
return
|
|
669
|
+
// Return the original update or undefined if no transformation is needed for other paths
|
|
670
|
+
return undefined;
|
|
447
671
|
},
|
|
448
672
|
});
|
|
449
673
|
|
|
@@ -453,7 +677,7 @@ Middleware: Incoming update: { counter: 5 }
|
|
|
453
677
|
*/
|
|
454
678
|
console.log('State after counter set:', store.get());
|
|
455
679
|
/* Output will show:
|
|
456
|
-
counter:
|
|
680
|
+
counter: 0 (initial) + 5 (update) = 5 (due to CounterIncrementMiddleware)
|
|
457
681
|
lastAction updated by TimestampActionMiddleware,
|
|
458
682
|
logs updated by TimestampActionMiddleware,
|
|
459
683
|
version: 1 (incremented by VersionIncrementMiddleware)
|
|
@@ -474,7 +698,8 @@ console.log('State after manual action:', store.get());
|
|
|
474
698
|
const temporaryLoggerId = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) });
|
|
475
699
|
await store.set({ counter: 1 });
|
|
476
700
|
// Output: Temporary logger saw: { counter: 1 }
|
|
477
|
-
store.
|
|
701
|
+
const removed = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) })(); // Remove the temporary logger
|
|
702
|
+
console.log('Middleware removed:', removed);
|
|
478
703
|
await store.set({ counter: 1 }); // TemporaryLogger will not be called now
|
|
479
704
|
```
|
|
480
705
|
|
|
@@ -643,6 +868,352 @@ try {
|
|
|
643
868
|
}
|
|
644
869
|
```
|
|
645
870
|
|
|
871
|
+
### Artifacts (Dependency Injection)
|
|
872
|
+
|
|
873
|
+
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.
|
|
874
|
+
|
|
875
|
+
This example uses the `ArtifactContainer` directly for clarity, but in React applications, you'd typically use the `createStore` factory, which automatically integrates with `ArtifactContainer` and exposes resolution via `useStore().resolve()`.
|
|
876
|
+
|
|
877
|
+
```typescript
|
|
878
|
+
import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-store/artifacts';
|
|
879
|
+
|
|
880
|
+
interface AppState {
|
|
881
|
+
user: { id: string; name: string; };
|
|
882
|
+
config: { apiUrl: string; logLevel: string; };
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Mock DataStore interface for standalone ArtifactContainer usage
|
|
886
|
+
let currentState: AppState = {
|
|
887
|
+
user: { id: 'user-1', name: 'Alice' },
|
|
888
|
+
config: { apiUrl: '/api/v1', logLevel: 'info' }
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
const store = new ReactiveDataStore<AppState>(currentState);
|
|
892
|
+
|
|
893
|
+
const container = new ArtifactContainer(store);
|
|
894
|
+
|
|
895
|
+
// --- Artifact Definitions ---
|
|
896
|
+
|
|
897
|
+
// Artifact 1: Simple Logger (Transient) - new instance every time
|
|
898
|
+
container.register({
|
|
899
|
+
key: 'logger',
|
|
900
|
+
scope: ArtifactScope.Transient,
|
|
901
|
+
factory: () => {
|
|
902
|
+
console.log('Logger artifact created (Transient)');
|
|
903
|
+
return {
|
|
904
|
+
log: (message: string) => console.log(`[LOG] ${message}`)
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
// Artifact 2: API Client (Singleton) - depends on state.config.apiUrl
|
|
910
|
+
const apiClientCleanup = () => console.log('API Client connection closed.');
|
|
911
|
+
container.register({
|
|
912
|
+
key: 'apiClient',
|
|
913
|
+
scope: ArtifactScope.Singleton,
|
|
914
|
+
factory: async ({ use, current }) => {
|
|
915
|
+
const apiUrl = await use(({ select }) => select((state: AppState) => state.config.apiUrl));
|
|
916
|
+
console.log(`API Client created/re-created for URL: ${apiUrl}`);
|
|
917
|
+
if (current) {
|
|
918
|
+
console.log('Re-creating API client. Old instance:', current);
|
|
919
|
+
}
|
|
920
|
+
return [{
|
|
921
|
+
fetchUser: (id: string) => `Fetching ${id} from ${apiUrl}`,
|
|
922
|
+
sendData: (data: any) => `Sending ${JSON.stringify(data)} to ${apiUrl}`
|
|
923
|
+
}, apiClientCleanup];
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
// Artifact 3: User Service (Singleton) - depends on 'apiClient' and state.user.name
|
|
928
|
+
const userServiceCleanup = () => console.log('User Service resources released.');
|
|
929
|
+
container.register({
|
|
930
|
+
key: 'userService',
|
|
931
|
+
scope: ArtifactScope.Singleton,
|
|
932
|
+
factory: async ({ use, current }) => {
|
|
933
|
+
const apiClient = await use(({ resolve }) => resolve('apiClient'));
|
|
934
|
+
const userName = await use(({ select }) => select((state: AppState) => state.user.name));
|
|
935
|
+
console.log(`User Service created/re-created for user: ${userName}`);
|
|
936
|
+
if (current) {
|
|
937
|
+
console.log('Re-creating User Service. Old instance:', current);
|
|
938
|
+
}
|
|
939
|
+
return [{
|
|
940
|
+
getUserProfile: () => apiClient.fetchUser(mockGet().user.id),
|
|
941
|
+
updateUserName: (newName: string) => `Updating user name to ${newName} via API. Current: ${userName}`
|
|
942
|
+
}, userServiceCleanup];
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
// --- Usage ---
|
|
947
|
+
|
|
948
|
+
async function runDemo() {
|
|
949
|
+
console.log('\n--- Initial Artifact Resolution ---');
|
|
950
|
+
const logger1 = await container.resolve('logger');
|
|
951
|
+
logger1.log('Application started.');
|
|
952
|
+
|
|
953
|
+
const apiClient1 = await container.resolve('apiClient');
|
|
954
|
+
console.log(apiClient1.fetchUser('123'));
|
|
955
|
+
|
|
956
|
+
const userService1 = await container.resolve('userService');
|
|
957
|
+
console.log(userService1.getUserProfile());
|
|
958
|
+
|
|
959
|
+
// Transient artifact resolves a new instance
|
|
960
|
+
const logger2 = await container.resolve('logger');
|
|
961
|
+
expect(logger1).not.toBe(logger2); // Will be different instances
|
|
962
|
+
|
|
963
|
+
// Singleton artifacts resolve the same instance
|
|
964
|
+
const apiClient2 = await container.resolve('apiClient');
|
|
965
|
+
expect(apiClient1).toBe(apiClient2);
|
|
966
|
+
|
|
967
|
+
console.log('\n--- Simulate State Change (config.apiUrl) ---');
|
|
968
|
+
store.set({ config:{ apiUrl: '/api/v2'}})
|
|
969
|
+
|
|
970
|
+
// After config.apiUrl changes, apiClient (and userService) should be re-created
|
|
971
|
+
const apiClient3 = await container.resolve('apiClient'); // This will trigger re-creation
|
|
972
|
+
console.log(apiClient3.fetchUser('123'));
|
|
973
|
+
expect(apiClient3).not.toBe(apiClient1); // Should be a new instance
|
|
974
|
+
|
|
975
|
+
// userService should also be re-created because apiClient, its dependency, was re-created
|
|
976
|
+
const userService2 = await container.resolve('userService');
|
|
977
|
+
console.log(userService2.getUserProfile());
|
|
978
|
+
expect(userService2).not.toBe(userService1); // Should be a new instance
|
|
979
|
+
|
|
980
|
+
console.log('\n--- Simulate State Change (user.name) ---');
|
|
981
|
+
simulateStateUpdate(
|
|
982
|
+
{ ...currentState, user: { ...currentState.user, name: 'Bob' } },
|
|
983
|
+
['user.name']
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
// Only userService (which depends on user.name) should be re-created this time, not apiClient
|
|
987
|
+
const apiClient4 = await container.resolve('apiClient');
|
|
988
|
+
expect(apiClient4).toBe(apiClient3); // Still the same API client instance
|
|
989
|
+
|
|
990
|
+
const userService3 = await container.resolve('userService');
|
|
991
|
+
console.log(userService3.getUserProfile());
|
|
992
|
+
expect(userService3).not.toBe(userService2); // New user service instance
|
|
993
|
+
|
|
994
|
+
console.log('\n--- Unregistering Artifacts ---');
|
|
995
|
+
await container.unregister('userService');
|
|
996
|
+
// Output: User Service resources released. (cleanup called)
|
|
997
|
+
await container.unregister('apiClient');
|
|
998
|
+
// Output: API Client connection closed. (cleanup called)
|
|
999
|
+
|
|
1000
|
+
try {
|
|
1001
|
+
await container.resolve('userService');
|
|
1002
|
+
} catch (e: any) {
|
|
1003
|
+
console.error('Expected error:', e.message);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
runDemo();
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
### React Integration
|
|
1011
|
+
|
|
1012
|
+
The `@asaidimu/utils-store/react-store` module provides a `createStore` factory function that integrates the core `ReactiveDataStore` with React's context and `useSyncExternalStore` hook. This makes it easy to consume state, dispatch actions, and resolve artifacts in your React components.
|
|
1013
|
+
|
|
1014
|
+
```typescript
|
|
1015
|
+
// store.ts (example for an E-commerce dashboard)
|
|
1016
|
+
import { ArtifactScope, createStore, ActionMap, ArtifactsMap } from '@asaidimu/utils-store';
|
|
1017
|
+
|
|
1018
|
+
export interface Product {
|
|
1019
|
+
id: number; name: string; price: number; stock: number; image: string;
|
|
1020
|
+
}
|
|
1021
|
+
export interface CartItem extends Product { quantity: number; }
|
|
1022
|
+
export interface Order { id: string; items: CartItem[]; total: number; date: Date; }
|
|
1023
|
+
|
|
1024
|
+
export interface ECommerceState extends Record<string, any> {
|
|
1025
|
+
products: Product[];
|
|
1026
|
+
cart: CartItem[];
|
|
1027
|
+
orders: Order[];
|
|
1028
|
+
topSellers: { id: number; name: string; sales: number }[];
|
|
1029
|
+
activeUsers: number;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const initialState: ECommerceState = {
|
|
1033
|
+
products: [
|
|
1034
|
+
{ id: 1, name: 'Wireless Mouse', price: 25.99, stock: 150, image: 'https://placehold.co/600x400/white/black?text=Mouse' },
|
|
1035
|
+
{ id: 2, name: 'Mechanical Keyboard', price: 79.99, stock: 100, image: 'https://placehold.co/600x400/white/black?text=Keyboard' },
|
|
1036
|
+
],
|
|
1037
|
+
cart: [], orders: [], topSellers: [], activeUsers: 1428,
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
// 1. Define Artifacts
|
|
1041
|
+
export const artifacts = {
|
|
1042
|
+
logger: {
|
|
1043
|
+
scope: ArtifactScope.Singleton,
|
|
1044
|
+
factory: () => ((...args:any[]) => console.log('Artifact Logger:', ...args))
|
|
1045
|
+
},
|
|
1046
|
+
analytics: {
|
|
1047
|
+
scope: ArtifactScope.Transient,
|
|
1048
|
+
factory: () => ({
|
|
1049
|
+
trackEvent: (name: string, data: object) => console.log(`[Analytics] ${name}`, data)
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
} as const satisfies ArtifactsMap<ECommerceState>
|
|
1053
|
+
|
|
1054
|
+
// 2. Define Actions
|
|
1055
|
+
export const actions = {
|
|
1056
|
+
addToCart: async ({ state, resolve }, product: Product) => {
|
|
1057
|
+
const log = await resolve("logger");
|
|
1058
|
+
const analytics = await resolve("analytics");
|
|
1059
|
+
analytics.trackEvent('add_to_cart', { product: product.name });
|
|
1060
|
+
|
|
1061
|
+
const existingItem = state.cart.find((item) => item.id === product.id);
|
|
1062
|
+
if (existingItem) {
|
|
1063
|
+
return {
|
|
1064
|
+
cart: state.cart.map((item) =>
|
|
1065
|
+
item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item
|
|
1066
|
+
),
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
const result = { cart: [...state.cart, { ...product, quantity: 1 }] };
|
|
1070
|
+
log("Item added:", product.name);
|
|
1071
|
+
return result
|
|
1072
|
+
},
|
|
1073
|
+
removeFromCart: ({ state }, productId: number) => ({
|
|
1074
|
+
cart: state.cart.filter((item) => item.id !== productId),
|
|
1075
|
+
}),
|
|
1076
|
+
checkout: ({ state }) => {
|
|
1077
|
+
const total = state.cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
|
1078
|
+
const newOrder: Order = {
|
|
1079
|
+
id: crypto.randomUUID(), items: state.cart, total, date: new Date(),
|
|
1080
|
+
};
|
|
1081
|
+
return {
|
|
1082
|
+
cart: [], orders: [newOrder, ...state.orders],
|
|
1083
|
+
};
|
|
1084
|
+
},
|
|
1085
|
+
// Simulated real-time updates for the dashboard example
|
|
1086
|
+
updateStock: ({ state }) => ({
|
|
1087
|
+
products: state.products.map(p => ({
|
|
1088
|
+
...p,
|
|
1089
|
+
stock: Math.max(0, p.stock + Math.floor(Math.random() * 10) - 5)
|
|
1090
|
+
}))
|
|
1091
|
+
}),
|
|
1092
|
+
updateActiveUsers: ({ state }) => ({
|
|
1093
|
+
activeUsers: state.activeUsers + Math.floor(Math.random() * 20) - 10,
|
|
1094
|
+
}),
|
|
1095
|
+
addRandomOrder: ({ state }) => {
|
|
1096
|
+
const randomProduct = state.products[Math.floor(Math.random() * state.products.length)];
|
|
1097
|
+
const quantity = Math.floor(Math.random() * 3) + 1;
|
|
1098
|
+
const newOrder: Order = {
|
|
1099
|
+
id: crypto.randomUUID(),
|
|
1100
|
+
items: [{ ...randomProduct, quantity }],
|
|
1101
|
+
total: randomProduct.price * quantity,
|
|
1102
|
+
date: new Date(),
|
|
1103
|
+
};
|
|
1104
|
+
return {
|
|
1105
|
+
orders: [newOrder, ...state.orders],
|
|
1106
|
+
};
|
|
1107
|
+
},
|
|
1108
|
+
} as const satisfies ActionMap<ECommerceState, typeof artifacts>
|
|
1109
|
+
|
|
1110
|
+
// 3. Create the React Store Hook
|
|
1111
|
+
export const useStore = createStore(
|
|
1112
|
+
{
|
|
1113
|
+
state: initialState,
|
|
1114
|
+
artifacts,
|
|
1115
|
+
actions,
|
|
1116
|
+
},
|
|
1117
|
+
{ enableMetrics: true, enableConsoleLogging: true }
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
// App.tsx (React Component)
|
|
1121
|
+
import { useEffect, useMemo } from 'react';
|
|
1122
|
+
import { useStore } from './store'; // Import the custom hook
|
|
1123
|
+
|
|
1124
|
+
function ProductCatalog() {
|
|
1125
|
+
const { select, actions } = useStore();
|
|
1126
|
+
const products = select((state) => state.products); // Reactive selector for products
|
|
1127
|
+
|
|
1128
|
+
return (
|
|
1129
|
+
<div>
|
|
1130
|
+
<h2>Products</h2>
|
|
1131
|
+
{products.map((product) => (
|
|
1132
|
+
<div key={product.id}>
|
|
1133
|
+
{product.name} - ${product.price} ({product.stock} in stock)
|
|
1134
|
+
<button onClick={() => actions.addToCart(product)}>Add to Cart</button>
|
|
1135
|
+
</div>
|
|
1136
|
+
))}
|
|
1137
|
+
</div>
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
function ShoppingCart() {
|
|
1142
|
+
const { select, actions } = useStore();
|
|
1143
|
+
const cart = select((state) => state.cart); // Reactive selector for cart
|
|
1144
|
+
const total = useMemo(() => cart.reduce((sum, item) => sum + item.price * item.quantity, 0), [cart]);
|
|
1145
|
+
|
|
1146
|
+
return (
|
|
1147
|
+
<div>
|
|
1148
|
+
<h2>Shopping Cart</h2>
|
|
1149
|
+
{cart.length === 0 ? (
|
|
1150
|
+
<p>Your cart is empty.</p>
|
|
1151
|
+
) : (
|
|
1152
|
+
<ul>
|
|
1153
|
+
{cart.map((item) => (
|
|
1154
|
+
<li key={item.id}>
|
|
1155
|
+
{item.name} x {item.quantity} - ${item.price * item.quantity}
|
|
1156
|
+
<button onClick={() => actions.removeFromCart(item.id)}>Remove</button>
|
|
1157
|
+
</li>
|
|
1158
|
+
))}
|
|
1159
|
+
</ul>
|
|
1160
|
+
)}
|
|
1161
|
+
<p>Total: ${total.toFixed(2)}</p>
|
|
1162
|
+
{cart.length > 0 && <button onClick={() => actions.checkout()}>Checkout</button>}
|
|
1163
|
+
</div>
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function DebugPanel() {
|
|
1168
|
+
const { resolve, watch, state } = useStore();
|
|
1169
|
+
const [logger, loggerReady] = resolve('logger'); // Reactive artifact resolution
|
|
1170
|
+
const [analytics, analyticsReady] = resolve('analytics');
|
|
1171
|
+
|
|
1172
|
+
const isLoadingAddToCart = watch('addToCart'); // Watch specific action loading state
|
|
1173
|
+
|
|
1174
|
+
return (
|
|
1175
|
+
<div>
|
|
1176
|
+
<h3>Debug Panel</h3>
|
|
1177
|
+
<p>Add to Cart Loading: {isLoadingAddToCart ? 'Yes' : 'No'}</p>
|
|
1178
|
+
<p>Store is ready: {useStore().isReady ? 'Yes' : 'No'}</p>
|
|
1179
|
+
<button onClick={() => loggerReady && logger('Debug message from UI')}>Log via Artifact</button>
|
|
1180
|
+
<button onClick={() => analyticsReady && analytics.trackEvent('ui_event', { component: 'DebugPanel' })}>Track UI Event</button>
|
|
1181
|
+
<pre>Full State: {JSON.stringify(state(), null, 2)}</pre>
|
|
1182
|
+
</div>
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function App() {
|
|
1187
|
+
const { actions } = useStore();
|
|
1188
|
+
|
|
1189
|
+
// Simulate real-time updates
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
const stockInterval = setInterval(() => actions.updateStock(), 2000);
|
|
1192
|
+
const usersInterval = setInterval(() => actions.updateActiveUsers(), 3000);
|
|
1193
|
+
const ordersInterval = setInterval(() => actions.addRandomOrder(), 5000);
|
|
1194
|
+
|
|
1195
|
+
return () => {
|
|
1196
|
+
clearInterval(stockInterval);
|
|
1197
|
+
clearInterval(usersInterval);
|
|
1198
|
+
clearInterval(ordersInterval);
|
|
1199
|
+
};
|
|
1200
|
+
}, [actions]);
|
|
1201
|
+
|
|
1202
|
+
return (
|
|
1203
|
+
<div>
|
|
1204
|
+
<h1>E-Commerce App</h1>
|
|
1205
|
+
<ProductCatalog />
|
|
1206
|
+
<hr />
|
|
1207
|
+
<ShoppingCart />
|
|
1208
|
+
<hr />
|
|
1209
|
+
<DebugPanel />
|
|
1210
|
+
</div>
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
export default App;
|
|
1215
|
+
```
|
|
1216
|
+
|
|
646
1217
|
### Store Observer (Debugging & Observability)
|
|
647
1218
|
|
|
648
1219
|
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.
|
|
@@ -674,6 +1245,8 @@ const observer = new StoreObserver(store, {
|
|
|
674
1245
|
updates: true, // Log all update lifecycle events (start/complete)
|
|
675
1246
|
middleware: true, // Log middleware start/complete/error/blocked/executed events
|
|
676
1247
|
transactions: true, // Log transaction start/complete/error events
|
|
1248
|
+
actions: true, // Log action start/complete/error events
|
|
1249
|
+
selectors: true, // Log selector accessed/changed events
|
|
677
1250
|
},
|
|
678
1251
|
performanceThresholds: {
|
|
679
1252
|
updateTime: 50, // Warn in console if an update takes > 50ms
|
|
@@ -693,8 +1266,8 @@ await store.set({ messages: ['Hello World!'] });
|
|
|
693
1266
|
await store.set({ settings: { debugMode: false } });
|
|
694
1267
|
|
|
695
1268
|
// Simulate a slow update to trigger performance warning
|
|
696
|
-
await new Promise(resolve => setTimeout(resolve, 60)); // Artificially delay
|
|
697
|
-
await store.set({ messages: ['Another message', 'And another'] });
|
|
1269
|
+
// await new Promise(resolve => setTimeout(resolve, 60)); // Artificially delay
|
|
1270
|
+
// await store.set({ messages: ['Another message', 'And another'] });
|
|
698
1271
|
// This last set will cause a console warning for "Slow update detected" if enableConsoleLogging is true.
|
|
699
1272
|
|
|
700
1273
|
// 1. Get Event History
|
|
@@ -706,7 +1279,7 @@ events.slice(0, 5).forEach(event => console.log(`Type: ${event.type}, Data: ${JS
|
|
|
706
1279
|
// 2. Get State History
|
|
707
1280
|
console.log('\n--- State History (Most Recent First) ---');
|
|
708
1281
|
const stateSnapshots = observer.getStateHistory();
|
|
709
|
-
stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}: Messages: ${snapshot.messages.join(', ')}, User Status: ${snapshot.user.status}`));
|
|
1282
|
+
stateSnapshots.forEach((snapshot, index) => console.log(`State #${index}: Messages: ${snapshot.state.messages.join(', ')}, User Status: ${snapshot.state.user.status}`));
|
|
710
1283
|
|
|
711
1284
|
// 3. Get Recent Changes (Diffs)
|
|
712
1285
|
console.log('\n--- Recent State Changes (Diffs) ---');
|
|
@@ -745,7 +1318,7 @@ if (timeTravel.canRedo()) {
|
|
|
745
1318
|
console.log('After redo 1:', store.get().messages); // Output: ['First message']
|
|
746
1319
|
}
|
|
747
1320
|
|
|
748
|
-
console.log('Time-Travel history length:', timeTravel.
|
|
1321
|
+
console.log('Time-Travel history length:', timeTravel.length()); // Reflects `maxStateHistory` + initial state
|
|
749
1322
|
|
|
750
1323
|
// 5. Custom Debugging Middleware (provided by StoreObserver for convenience)
|
|
751
1324
|
// Example: A logging middleware that logs every update
|
|
@@ -769,7 +1342,7 @@ const validationMiddlewareId = store.use({ name: 'DebugValidation', block: true,
|
|
|
769
1342
|
|
|
770
1343
|
try {
|
|
771
1344
|
await store.set({ messages: ['m1','m2','m3','m4','m5','m6'] }); // This will be blocked
|
|
772
|
-
} catch (e) {
|
|
1345
|
+
} catch (e: any) {
|
|
773
1346
|
console.warn(`Caught expected error from validation middleware: ${e.message}`);
|
|
774
1347
|
}
|
|
775
1348
|
console.log('Current messages after failed validation:', store.get().messages); // Should be the state before this set.
|
|
@@ -788,7 +1361,7 @@ await store.set({ messages: ['Final message after disconnect'] });
|
|
|
788
1361
|
|
|
789
1362
|
### Event System
|
|
790
1363
|
|
|
791
|
-
The store emits various events during its lifecycle, allowing for advanced monitoring, logging, and integration with external systems. You can subscribe to these events using `store.
|
|
1364
|
+
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.
|
|
792
1365
|
|
|
793
1366
|
```typescript
|
|
794
1367
|
import { ReactiveDataStore, type StoreEvent } from '@asaidimu/utils-store';
|
|
@@ -801,58 +1374,93 @@ interface MyState {
|
|
|
801
1374
|
const store = new ReactiveDataStore<MyState>({ value: 0, status: 'idle' });
|
|
802
1375
|
|
|
803
1376
|
// Subscribe to 'update:start' event - triggered before an update begins processing.
|
|
804
|
-
store.
|
|
805
|
-
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⚡ Update started
|
|
1377
|
+
store.on('update:start', (data) => {
|
|
1378
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⚡ Update started. Action ID: ${data.actionId || 'N/A'}`);
|
|
806
1379
|
});
|
|
807
1380
|
|
|
808
1381
|
// Subscribe to 'update:complete' event - triggered after an update is fully applied or blocked.
|
|
809
|
-
store.
|
|
1382
|
+
store.on('update:complete', (data) => {
|
|
810
1383
|
if (data.blocked) {
|
|
811
1384
|
console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] ✋ Update blocked. Error:`, data.error?.message);
|
|
812
1385
|
} else {
|
|
813
|
-
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Update complete. Changed paths: ${data.
|
|
1386
|
+
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)`);
|
|
814
1387
|
}
|
|
815
1388
|
});
|
|
816
1389
|
|
|
817
1390
|
// Subscribe to middleware lifecycle events
|
|
818
|
-
store.
|
|
819
|
-
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ▶ Middleware "${data.name}" (${data.type}) started.`);
|
|
1391
|
+
store.on('middleware:start', (data) => {
|
|
1392
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ▶ Middleware "${data.name}" (${data.type || 'transform'}) started.`);
|
|
820
1393
|
});
|
|
821
1394
|
|
|
822
|
-
store.
|
|
823
|
-
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ◀ Middleware "${data.name}" (${data.type}) completed in ${data.duration?.toFixed(2)}ms.`);
|
|
1395
|
+
store.on('middleware:complete', (data) => {
|
|
1396
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ◀ Middleware "${data.name}" (${data.type || 'transform'}) completed in ${data.duration?.toFixed(2)}ms.`);
|
|
824
1397
|
});
|
|
825
1398
|
|
|
826
|
-
store.
|
|
1399
|
+
store.on('middleware:error', (data) => {
|
|
827
1400
|
console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Middleware "${data.name}" failed:`, data.error);
|
|
828
1401
|
});
|
|
829
1402
|
|
|
830
|
-
store.
|
|
1403
|
+
store.on('middleware:blocked', (data) => {
|
|
831
1404
|
console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] 🛑 Middleware "${data.name}" blocked an update.`);
|
|
832
1405
|
});
|
|
833
1406
|
|
|
834
|
-
store.
|
|
1407
|
+
store.on('middleware:executed', (data) => {
|
|
835
1408
|
// This event captures detailed execution info for all middlewares, useful for aggregate metrics.
|
|
836
1409
|
console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 📊 Middleware executed: "${data.name}" - Duration: ${data.duration?.toFixed(2)}ms, Blocked: ${data.blocked}`);
|
|
837
1410
|
});
|
|
838
1411
|
|
|
839
1412
|
// Subscribe to transaction lifecycle events
|
|
840
|
-
store.
|
|
1413
|
+
store.on('transaction:start', (data) => {
|
|
841
1414
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction started.`);
|
|
842
1415
|
});
|
|
843
1416
|
|
|
844
|
-
store.
|
|
1417
|
+
store.on('transaction:complete', (data) => {
|
|
845
1418
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction complete.`);
|
|
846
1419
|
});
|
|
847
1420
|
|
|
848
|
-
store.
|
|
1421
|
+
store.on('transaction:error', (data) => {
|
|
849
1422
|
console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] 📦 Transaction failed:`, data.error);
|
|
850
1423
|
});
|
|
851
1424
|
|
|
852
1425
|
// Subscribe to persistence events
|
|
853
|
-
store.
|
|
1426
|
+
store.on('persistence:ready', (data) => {
|
|
854
1427
|
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 💾 Persistence layer is ready.`);
|
|
855
1428
|
});
|
|
1429
|
+
store.on('persistence:queued', (data) => {
|
|
1430
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ⏳ Persistence task queued: ${data.taskId}, queue size: ${data.queueSize}`);
|
|
1431
|
+
});
|
|
1432
|
+
store.on('persistence:success', (data) => {
|
|
1433
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✅ Persistence success for task: ${data.taskId}, took ${data.duration?.toFixed(2)}ms`);
|
|
1434
|
+
});
|
|
1435
|
+
store.on('persistence:retry', (data) => {
|
|
1436
|
+
console.warn(`[${new Date(data.timestamp).toLocaleTimeString()}] 🔄 Persistence retry for task: ${data.taskId}, attempt ${data.attempt}/${data.maxRetries}`);
|
|
1437
|
+
});
|
|
1438
|
+
store.on('persistence:failed', (data) => {
|
|
1439
|
+
console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] ❌ Persistence failed permanently for task: ${data.taskId}`, data.error);
|
|
1440
|
+
});
|
|
1441
|
+
|
|
1442
|
+
|
|
1443
|
+
// Subscribe to action events
|
|
1444
|
+
store.on('action:start', (data) => {
|
|
1445
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 🚀 Action "${data.name}" (ID: ${data.actionId}) started with params:`, data.params);
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
store.on('action:complete', (data) => {
|
|
1449
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] ✔️ Action "${data.name}" (ID: ${data.actionId}) completed in ${data.duration?.toFixed(2)}ms.`);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
store.on('action:error', (data) => {
|
|
1453
|
+
console.error(`[${new Date(data.timestamp).toLocaleTimeString()}] 🔥 Action "${data.name}" (ID: ${data.actionId}) failed:`, data.error);
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
// Subscribe to selector events
|
|
1457
|
+
store.on('selector:accessed', (data) => {
|
|
1458
|
+
console.debug(`[${new Date(data.timestamp).toLocaleTimeString()}] 👀 Selector (ID: ${data.selectorId}) accessed paths: ${data.accessedPaths.join(', ')}`);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
store.on('selector:changed', (data) => {
|
|
1462
|
+
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 📢 Selector (ID: ${data.selectorId}) changed. New result:`, data.newResult);
|
|
1463
|
+
});
|
|
856
1464
|
|
|
857
1465
|
|
|
858
1466
|
// Add a transform middleware to demonstrate `middleware:start/complete/executed`
|
|
@@ -906,58 +1514,73 @@ src/store/
|
|
|
906
1514
|
├── index.ts # Main entry point (exports all public APIs)
|
|
907
1515
|
├── types.ts # TypeScript interfaces and types for the store
|
|
908
1516
|
├── store.ts # `ReactiveDataStore` - The main orchestrator
|
|
909
|
-
├── state.ts # `
|
|
1517
|
+
├── state.ts # `StateManager` - Manages the immutable state and diffing
|
|
910
1518
|
├── merge.ts # `createMerge` - Deep merging utility with deletion support
|
|
911
1519
|
├── diff.ts # `createDiff`, `createDerivePaths` - Change detection utilities
|
|
912
1520
|
├── middleware.ts # `MiddlewareEngine` - Manages and executes middleware pipeline
|
|
913
1521
|
├── transactions.ts # `TransactionManager` - Handles atomic state transactions
|
|
914
1522
|
├── persistence.ts # `PersistenceHandler` - Integrates with `SimplePersistence` layer
|
|
915
1523
|
├── metrics.ts # `MetricsCollector` - Gathers runtime performance metrics
|
|
916
|
-
├──
|
|
917
|
-
|
|
1524
|
+
├── selector.ts # `SelectorManager` - Manages reactive selectors for derived state
|
|
1525
|
+
├── observer.ts # `StoreObserver` - Debugging and introspection utilities
|
|
1526
|
+
├── artifacts.ts # `ArtifactContainer` - Dependency Injection system for services
|
|
1527
|
+
├── actions.ts # `ActionManager` - Manages registration and dispatch of named actions
|
|
1528
|
+
└── react-store/ # React integration module (hooks, context)
|
|
1529
|
+
├── index.ts
|
|
1530
|
+
├── types.ts
|
|
1531
|
+
├── store.ts # `createStore` - Factory for React hooks
|
|
1532
|
+
└── execution.ts # `ActionTracker` - Tracks action execution history for React integration
|
|
918
1533
|
```
|
|
919
1534
|
|
|
920
1535
|
### Core Components
|
|
921
1536
|
|
|
922
|
-
* **`ReactiveDataStore<T
|
|
923
|
-
* **`
|
|
924
|
-
* **`MiddlewareEngine<T
|
|
925
|
-
* **`PersistenceHandler<T
|
|
926
|
-
* **`TransactionManager<T
|
|
927
|
-
* **`MetricsCollector
|
|
928
|
-
* **`
|
|
929
|
-
* **`
|
|
930
|
-
* **`
|
|
1537
|
+
* **`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`.
|
|
1538
|
+
* **`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.
|
|
1539
|
+
* **`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.
|
|
1540
|
+
* **`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.
|
|
1541
|
+
* **`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.
|
|
1542
|
+
* **`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.
|
|
1543
|
+
* **`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.
|
|
1544
|
+
* **`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.
|
|
1545
|
+
* **`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.
|
|
1546
|
+
* **`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.
|
|
1547
|
+
* **`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.
|
|
1548
|
+
* **`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.
|
|
931
1549
|
|
|
932
1550
|
### Data Flow
|
|
933
1551
|
|
|
934
1552
|
The `ReactiveDataStore` handles state updates in a robust, queued, and event-driven manner:
|
|
935
1553
|
|
|
936
|
-
1. **`store.set(update)` call**:
|
|
937
|
-
* If
|
|
1554
|
+
1. **`store.set(update)` or `store.dispatch(actionName, actionFn, ...)` call**:
|
|
1555
|
+
* 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.
|
|
1556
|
+
* All `set` calls are automatically queued to prevent race conditions during concurrent updates, ensuring sequential processing. The `store.state().pendingChanges` reflects the queue.
|
|
938
1557
|
* An `update:start` event is immediately emitted.
|
|
939
1558
|
2. **Middleware Execution**:
|
|
940
|
-
* The `MiddlewareEngine` first executes all `blocking` middlewares (registered via `store.use({ block: true, ... })`). If any blocking middleware returns `false` or throws an error, the update is immediately halted. An `update:complete` event with `blocked: true` is emitted, and the process stops, with the state remaining unchanged.
|
|
1559
|
+
* 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.
|
|
941
1560
|
* If not blocked, `transform` middlewares (registered via `store.use({ action: ... })`) are executed sequentially. Each transform middleware receives the current state and the incoming partial update, and can return a new `DeepPartial<T>` that is then merged into the effective update payload.
|
|
942
1561
|
* Detailed lifecycle events (`middleware:start`, `middleware:complete`, `middleware:error`, `middleware:blocked`, `middleware:executed`) are emitted during this phase, providing granular insight into middleware behavior.
|
|
943
1562
|
3. **State Application**:
|
|
944
|
-
* The `
|
|
1563
|
+
* The `StateManager` receives the (potentially transformed) final `DeepPartial` update.
|
|
945
1564
|
* It internally uses the `createMerge` utility to immutably construct the new full state object.
|
|
946
|
-
* It then performs a `createDiff` comparison between the *previous* state and the *new* state to precisely identify all `changedPaths` (an array of
|
|
947
|
-
* If changes are detected, the `
|
|
1565
|
+
* It then performs a `createDiff` comparison between the *previous* state and the *new* state to precisely identify all `changedPaths` (an array of `StateDelta` objects).
|
|
1566
|
+
* 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`.
|
|
948
1567
|
4. **Listener Notification**:
|
|
949
|
-
* Any external subscribers (registered with `store.subscribe()`) whose registered paths match or are parent paths of the `changedPaths` are efficiently notified with the latest state. The `MetricsCollector` tracks `listenerExecutions` during this phase.
|
|
1568
|
+
* 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.
|
|
1569
|
+
* 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.
|
|
1570
|
+
* `ArtifactContainer` also receives change notifications for state paths its artifacts depend on, triggering re-evaluation for `Singleton` scoped artifacts.
|
|
950
1571
|
5. **Persistence Handling**:
|
|
951
|
-
* The `PersistenceHandler` receives the `changedPaths` and the new state. If a `SimplePersistence` implementation was configured during store initialization, it attempts to save the new state using `persistence.set()`.
|
|
1572
|
+
* 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.
|
|
952
1573
|
* The `PersistenceHandler` also manages loading initial state and reacting to external state changes (e.g., from other browser tabs or processes) through `persistence.subscribe()`.
|
|
953
1574
|
6. **Completion & Queue Processing**:
|
|
954
|
-
* An `update:complete` event is emitted, containing crucial information about the update's duration, the `
|
|
955
|
-
*
|
|
1575
|
+
* An `update:complete` event is emitted, containing crucial information about the update's duration, the `StateDelta[]`, and any blocking errors.
|
|
1576
|
+
* 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`.
|
|
1577
|
+
* The update queue automatically processes the next pending update.
|
|
956
1578
|
|
|
957
1579
|
### Extension Points
|
|
958
1580
|
|
|
959
1581
|
* **Custom Middleware**: Developers can inject their own `Middleware` (for transformation) and `BlockingMiddleware` (for validation/prevention) functions using `store.use()`. This allows for highly customizable update logic, centralized logging, complex validation, authorization, or triggering specific side effects.
|
|
960
1582
|
* **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.
|
|
1583
|
+
* **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.
|
|
961
1584
|
|
|
962
1585
|
---
|
|
963
1586
|
|
|
@@ -995,9 +1618,8 @@ From the `src/store` directory, the following `pnpm` scripts are available:
|
|
|
995
1618
|
|
|
996
1619
|
* `pnpm test`: Runs all unit tests using [Vitest](https://vitest.dev/).
|
|
997
1620
|
* `pnpm test:watch`: Runs tests in watch mode for continuous development.
|
|
1621
|
+
* `pnpm dev`: Starts the Vite development server for the UI example.
|
|
998
1622
|
* `pnpm lint`: Lints the codebase using [ESLint](https://eslint.org/).
|
|
999
|
-
* `pnpm format`: Formats the code using [Prettier](https://prettier.io/).
|
|
1000
|
-
* `pnpm build`: Compiles TypeScript to JavaScript (CommonJS and ES Modules) and generates declaration files (`.d.ts`).
|
|
1001
1623
|
|
|
1002
1624
|
### Testing
|
|
1003
1625
|
|
|
@@ -1040,9 +1662,14 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
|
|
|
1040
1662
|
### Troubleshooting
|
|
1041
1663
|
|
|
1042
1664
|
* **"Update not triggering listeners"**:
|
|
1043
|
-
* Ensure you are subscribing to the correct path. `store.
|
|
1665
|
+
* 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).
|
|
1044
1666
|
* 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.
|
|
1045
1667
|
* Verify your `DeepPartial` update correctly targets the intended part of the state.
|
|
1668
|
+
* **"Reactive Selector not re-evaluating"**:
|
|
1669
|
+
* Ensure the state path(s) accessed by the selector are actually changing. The selector re-evaluates only when its dependencies change.
|
|
1670
|
+
* If using `select(() => state.someArray.length)`, it might not re-evaluate if array elements change but `length` remains the same.
|
|
1671
|
+
* Check for strict equality issues: if the new computed value is strictly equal to the old one, no re-evaluation notification will occur.
|
|
1672
|
+
* Avoid complex operations (array methods, conditionals, arithmetic) within selectors as they might bypass the static path analysis and lead to unexpected behavior or errors.
|
|
1046
1673
|
* **"State not rolling back after transaction error"**:
|
|
1047
1674
|
* Ensure the error is thrown *within* the `transaction` callback function. Errors caught and handled inside the callback, or thrown outside of it, will not trigger the rollback mechanism.
|
|
1048
1675
|
* Promises within the transaction *must* be `await`ed so the `TransactionManager` can capture potential rejections and manage the atomic operation correctly.
|
|
@@ -1052,29 +1679,35 @@ If you find a bug or have a feature request, please open an issue on the [GitHub
|
|
|
1052
1679
|
* Ensure your `transform` middleware returns a `DeepPartial<T>` or `void`/`Promise<void>`, and `blocking` middleware returns `boolean`/`Promise<boolean>`.
|
|
1053
1680
|
* **"Performance warnings in console"**:
|
|
1054
1681
|
* 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.
|
|
1682
|
+
* **"Artifact not resolving or re-evaluating"**:
|
|
1683
|
+
* Check for `Circular dependency` errors in the console during `resolve`.
|
|
1684
|
+
* 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`.
|
|
1685
|
+
* For `Transient` artifacts, a new instance is created on every `resolve`.
|
|
1055
1686
|
|
|
1056
1687
|
### FAQ
|
|
1057
1688
|
|
|
1058
1689
|
**Q: How are arrays handled during updates?**
|
|
1059
1690
|
A: Arrays are treated as primitive values. When you provide an array in a `DeepPartial` update (e.g., `set({ items: [new Array] })`), the entire array at that path is replaced with the new array, not merged element-by-element. This ensures predictable behavior and avoids complex, potentially ambiguous partial array merging logic.
|
|
1060
1691
|
|
|
1061
|
-
**Q: What is `
|
|
1062
|
-
A: `
|
|
1692
|
+
**Q: What is `DELETE_SYMBOL`?**
|
|
1693
|
+
A: `DELETE_SYMBOL` (exported from `@asaidimu/utils-store`) is a special global symbol (`Symbol.for("delete")`) used to explicitly remove a property from the state during a `set` operation. If you pass `DELETE_SYMBOL` as the value for a key in your `DeepPartial` update, that key will be removed from the resulting state object. This provides a clear semantic for deletion.
|
|
1063
1694
|
|
|
1064
1695
|
**Q: How do I debug my store's state changes?**
|
|
1065
1696
|
A: The `StoreObserver` class is your primary tool. Instantiate it with your `ReactiveDataStore` instance. It provides methods to get event history, state snapshots, and even time-travel debugging capabilities. Enabling `enableConsoleLogging: true` in `StoreObserver` options provides immediate, formatted console output during development, showing update lifecycles, middleware execution, and transaction events.
|
|
1066
1697
|
|
|
1067
1698
|
**Q: What is `SimplePersistence<T>`?**
|
|
1068
|
-
A: It's a minimal interface that defines the contract for any persistence layer you wish to integrate. It requires `get()`, `set(instanceId, state)`, and `subscribe(instanceId, listener)` methods. You need to provide an implementation of this interface to enable state saving and loading.
|
|
1699
|
+
A: It's a minimal interface that defines the contract for any persistence layer you wish to integrate. It requires `get()`, `set(instanceId, state)`, and an optional `subscribe(instanceId, listener)` methods. You need to provide an implementation of this interface to enable state saving and loading. You can use concrete implementations from `@asaidimu/utils-persistence` or create your own.
|
|
1700
|
+
|
|
1701
|
+
**Q: How does the Artifact system work?**
|
|
1702
|
+
A: The Artifact system is a dependency injection container. You register `ArtifactFactory` functions with a `key` and a `scope` (`Singleton` or `Transient`). Factories receive a `context` with `state()` (current state snapshot) and `use()` (to declare dependencies on other artifacts or specific state paths). The `use()` method is crucial for building the dependency graph, allowing the system to automatically re-evaluate `Singleton` artifacts when their dependencies change. In React, you use `useStore().resolve()` to access artifacts reactively.
|
|
1069
1703
|
|
|
1070
|
-
**Q:
|
|
1071
|
-
A:
|
|
1704
|
+
**Q: What's the difference between `set` and `dispatch`?**
|
|
1705
|
+
A: `set` is the foundational method for updating the state, handling the core logic of applying changes, running middleware, and notifying subscribers. `dispatch` builds upon `set` by adding an "action" concept. It wraps `set` calls, providing a semantic name, optional parameters, and lifecycle callbacks (`action:start`, `action:complete`, `action:error`). This makes `dispatch` ideal for orchestrating complex, named operations and provides better observability, which can be correlated across the update lifecycle via a shared `actionId`. Use `set` for simple, direct updates; use `dispatch` for logical operations that should be tracked as distinct "actions."
|
|
1072
1706
|
|
|
1073
1707
|
### Changelog / Roadmap
|
|
1074
1708
|
|
|
1075
1709
|
* **Changelog**: For detailed version history, including new features, bug fixes, and breaking changes, please refer to the project's [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/src/store/CHANGELOG.md) file.
|
|
1076
1710
|
* **Roadmap**: Future plans for `@asaidimu/utils-store` may include:
|
|
1077
|
-
* Official framework-specific integrations (e.g., React hooks library for easier consumption).
|
|
1078
1711
|
* More advanced query/selector capabilities with built-in memoization for derived state.
|
|
1079
1712
|
* Built-in serialization/deserialization options for persistence, perhaps with schema validation.
|
|
1080
1713
|
* Higher-order middlewares for common patterns (e.g., async data fetching, debouncing updates).
|
|
@@ -1089,3 +1722,5 @@ This project is licensed under the [MIT License](https://github.com/asaidimu/erp
|
|
|
1089
1722
|
* Inspired by modern state management patterns such as Redux, Zustand, and Vuex, emphasizing immutability and explicit state changes.
|
|
1090
1723
|
* Leverages the `@asaidimu/events` package for robust internal event bus capabilities.
|
|
1091
1724
|
* Utilizes the `uuid` library for generating unique instance IDs.
|
|
1725
|
+
|
|
1726
|
+
---
|