@asaidimu/react-store 1.4.11 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,75 +3,183 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@asaidimu/react-store.svg)](https://www.npmjs.com/package/@asaidimu/react-store)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A performant, type-safe state management solution for React with built-in persistence, observability, and middleware support.
6
+ A performant, type-safe state management solution for React with built-in persistence, extensive observability, and a robust middleware system.
7
7
 
8
8
  ⚠️ **Beta Warning**
9
9
  This package is currently in **beta**. The API is subject to rapid changes and should not be considered stable. Breaking changes may occur frequently without notice as we iterate and improve. We’ll update this warning once the package reaches a stable release. Use at your own risk and share feedback or report issues to help us improve!
10
10
 
11
- ## Features
11
+ ---
12
12
 
13
- - **Reactive State Management**: Automatically track dependencies and optimize renders.
14
- - **Type-Safe**: Full TypeScript support with strict type checking.
15
- - **Middleware Pipeline**: Transform or block updates with regular and blocking middleware.
16
- - **Transactions**: Atomic state updates with rollback capabilities.
17
- - **Persistence**: Seamless integration with IndexedDB or WebStorage (localStorage/sessionStorage), including cross-tab synchronization.
18
- - **Observability**: Built-in metrics, event logging, state history, and time-travel debugging.
19
- - **Remote Observability**: Send metrics to external systems like OpenTelemetry, Prometheus, or Grafana Cloud.
20
- - **Performance Optimized**: Smart selector caching and efficient update propagation.
21
- - **React 18+ Ready**: Built with modern React APIs for maximum compatibility.
13
+ ### Table of Contents
22
14
 
23
- ## Installation
15
+ * [Overview & Features](#overview--features)
16
+ * [Installation & Setup](#installation--setup)
17
+ * [Usage Documentation](#usage-documentation)
18
+ * [Creating a Store](#creating-a-store)
19
+ * [Using in Components](#using-in-components)
20
+ * [Handling Deletions](#handling-deletions)
21
+ * [Persistence](#persistence)
22
+ * [Middleware](#middleware)
23
+ * [Observability](#observability)
24
+ * [Remote Observability](#remote-observability)
25
+ * [Transaction Support](#transaction-support)
26
+ * [Event System](#event-system)
27
+ * [Advanced Hook Properties](#advanced-hook-properties)
28
+ * [Project Architecture](#project-architecture)
29
+ * [Development & Contributing](#development--contributing)
30
+ * [Additional Information](#additional-information)
31
+ * [Best Practices](#best-practices)
32
+ * [API Reference](#api-reference)
33
+ * [Comparison with Other State Management Solutions](#comparison-with-other-state-management-solutions)
34
+ * [Changelog](#changelog)
35
+ * [License](#license)
36
+ * [Acknowledgments](#acknowledgments)
37
+
38
+ ---
39
+
40
+ ## Overview & Features
41
+
42
+ `@asaidimu/react-store` provides an efficient and predictable way to manage complex application state in React applications. It goes beyond basic state management by integrating features typically found in separate libraries, such as data persistence and comprehensive observability tools, directly into its core. This allows developers to build robust, high-performance applications with deep insights into state changes and application behavior.
43
+
44
+ Designed with modern React in mind, it leverages `useSyncExternalStore` for optimal performance and reactivity, ensuring components re-render only when relevant parts of the state change. Its flexible design supports a variety of use cases, from simple counter applications to complex data flows requiring atomic updates and cross-tab synchronization.
45
+
46
+ ### Key Features
47
+
48
+ * 📊 **Reactive State Management**: Automatically tracks dependencies to optimize component renders and ensure efficient updates.
49
+ * 🛡️ **Type-Safe**: Built with TypeScript from the ground up, providing strict type checking and a safer development experience.
50
+ * ⚙️ **Middleware Pipeline**: Implement custom logic to transform, validate, or log state changes before they are applied. Supports both transforming and blocking middleware.
51
+ * 📦 **Transaction Support**: Group multiple state updates into a single atomic operation, with automatic rollback if any part of the transaction fails.
52
+ * 💾 **Built-in Persistence**: Seamlessly integrate with web storage mechanisms like `IndexedDB` and `WebStorage` (localStorage/sessionStorage), including cross-tab synchronization.
53
+ * 🔍 **Deep Observability**: Gain profound insights into your application's state with built-in metrics, detailed event logging, state history, and time-travel debugging capabilities.
54
+ * 📈 **Remote Observability**: Extend monitoring by sending collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud.
55
+ * ⚡ **Performance Optimized**: Features intelligent selector caching and debounced actions to prevent rapid successive calls and ensure smooth application performance.
56
+ * ⚛️ **React 18+ Ready**: Fully compatible with the latest React versions, leveraging modern APIs for enhanced performance and development ergonomics.
57
+ * 🗑️ **Explicit Deletions**: Use `Symbol.for("delete")` to explicitly remove properties from nested state objects.
58
+
59
+ ## Installation & Setup
60
+
61
+ ### Prerequisites
62
+
63
+ * Node.js (v18 or higher recommended)
64
+ * React (v18 or higher recommended)
65
+ * A package manager like `bun`, `npm`, or `yarn`.
66
+
67
+ ### Installation Steps
68
+
69
+ To add `@asaidimu/react-store` to your project, run one of the following commands:
24
70
 
25
71
  ```bash
26
72
  bun install @asaidimu/react-store
73
+ # or
74
+ npm install @asaidimu/react-store
75
+ # or
76
+ yarn add @asaidimu/react-store
77
+ ```
78
+
79
+ ### Configuration
80
+
81
+ No global configuration is required. All options are passed during store creation.
82
+
83
+ ### Verification
84
+
85
+ You can verify the installation by importing `createStore` and setting up a basic store:
86
+
87
+ ```typescript
88
+ import { createStore } from '@asaidimu/react-store';
89
+
90
+ const myStore = createStore({
91
+ state: { value: 'hello' },
92
+ actions: {
93
+ setValue: (state, newValue: string) => ({ value: newValue }),
94
+ },
95
+ });
96
+
97
+ console.log(myStore().select(s => s.value)); // Should output 'hello'
27
98
  ```
28
99
 
29
- ## Basic Usage
100
+ If no errors are thrown, the package is correctly installed.
101
+
102
+ ## Usage Documentation
30
103
 
31
104
  ### Creating a Store
32
105
 
106
+ Define your application state and actions, then create a store using `createStore`.
107
+
33
108
  ```tsx
109
+ // src/stores/myStore.ts
34
110
  import { createStore } from '@asaidimu/react-store';
35
111
 
36
- const counterStore = createStore({
37
- state: {
38
- count: 0,
39
- user: { name: 'Guest', loggedIn: false },
40
- },
112
+ interface AppState {
113
+ count: number;
114
+ user: { name: string; loggedIn: boolean; email?: string };
115
+ settings: { theme: 'light' | 'dark'; notifications: boolean };
116
+ }
117
+
118
+ const initialState: AppState = {
119
+ count: 0,
120
+ user: { name: 'Guest', loggedIn: false },
121
+ settings: { theme: 'light', notifications: true },
122
+ };
123
+
124
+ const myStore = createStore({
125
+ state: initialState,
41
126
  actions: {
42
127
  increment: (state, amount: number) => ({ count: state.count + amount }),
43
- login: async (state, username: string) => {
44
- await fetch('/api/login');
45
- return { user: { name: username, loggedIn: true } };
128
+ login: async (state, username: string, email: string) => {
129
+ // Simulate an asynchronous API call
130
+ await new Promise(resolve => setTimeout(resolve, 500));
131
+ return { user: { name: username, loggedIn: true, email } };
46
132
  },
133
+ toggleTheme: (state) => ({
134
+ settings: { theme: state.settings.theme === 'light' ? 'dark' : 'light' },
135
+ }),
47
136
  },
137
+ }, {
138
+ debounceTime: 100, // Debounce actions by 100ms
139
+ enableConsoleLogging: true, // Enable console logs for store events
140
+ logEvents: { updates: true, transactions: true, middleware: true },
141
+ performanceThresholds: { updateTime: 20, middlewareTime: 5 }, // Warn for slow operations
48
142
  });
49
143
 
50
- export const useCounter = counterStore;
144
+ // Export the hook for use in components
145
+ export const useMyStore = myStore;
51
146
  ```
52
147
 
53
148
  ### Using in Components
54
149
 
150
+ Consume your store's state and actions within your React components using the exported hook.
151
+
55
152
  ```tsx
56
- function Counter() {
57
- const { select, actions, isReady } = useCounter();
153
+ // src/components/MyComponent.tsx
154
+ import React from 'react';
155
+ import { useMyStore } from '../stores/myStore';
156
+
157
+ function MyComponent() {
158
+ const { select, actions, isReady } = useMyStore();
58
159
 
160
+ // Select specific parts of the state for granular re-renders
59
161
  const count = select((state) => state.count);
60
162
  const userName = select((state) => state.user.name);
163
+ const theme = select((state) => state.settings.theme);
61
164
 
165
+ // isReady indicates if persistence has loaded initial state
62
166
  if (!isReady) {
63
- return <div>Loading persistence...</div>;
167
+ return <div>Loading store data...</div>;
64
168
  }
65
169
 
66
170
  return (
67
- <div>
68
- <p>Count: {count}</p>
69
- <p>User: {userName}</p>
70
- <button onClick={() => actions.increment(1)}>+</button>
71
- <button onClick={() => actions.login('user123')}>Login</button>
171
+ <div style={{ background: theme === 'dark' ? '#333' : '#FFF', color: theme === 'dark' ? '#FFF' : '#333' }}>
172
+ <h1>Welcome, {userName}!</h1>
173
+ <p>Current Count: {count}</p>
174
+ <button onClick={() => actions.increment(1)}>Increment</button>
175
+ <button onClick={() => actions.increment(5)}>Increment by 5 (debounced)</button>
176
+ <button onClick={() => actions.login('Alice', 'alice@example.com')}>Login as Alice</button>
177
+ <button onClick={() => actions.toggleTheme()}>Toggle Theme</button>
72
178
  </div>
73
179
  );
74
180
  }
181
+
182
+ export default MyComponent;
75
183
  ```
76
184
 
77
185
  ### Handling Deletions
@@ -79,75 +187,223 @@ function Counter() {
79
187
  To remove a property from the state, use the `Symbol.for("delete")` symbol in your action’s return value. The store’s internal `merge` function will remove the specified key from the state.
80
188
 
81
189
  #### Example
82
- ```tsx
83
- const useStore = createStore({
84
- state: { a: 1, b: 2 },
190
+
191
+ ```typescript
192
+ import { createStore } from '@asaidimu/react-store';
193
+
194
+ const deleteStore = createStore({
195
+ state: {
196
+ id: 'product-123',
197
+ name: 'Fancy Gadget',
198
+ details: {
199
+ color: 'blue',
200
+ weight: '1kg',
201
+ dimensions: { width: 10, height: 20 }
202
+ },
203
+ tags: ['electronics', 'new']
204
+ },
85
205
  actions: {
86
- removeA: (state) => ({ a: Symbol.for("delete") }),
206
+ removeDetails: (state) => ({ details: Symbol.for("delete") }),
207
+ removeDimensions: (state) => ({ details: { dimensions: Symbol.for("delete") } }),
208
+ removeTag: (state, tagToRemove: string) => ({
209
+ tags: state.tags.filter(tag => tag !== tagToRemove)
210
+ }),
211
+ clearAllExceptId: (state) => ({
212
+ name: Symbol.for("delete"),
213
+ details: Symbol.for("delete"),
214
+ tags: Symbol.for("delete")
215
+ })
87
216
  },
88
217
  });
89
218
 
90
- const { actions } = useStore();
91
- actions.removeA(); // State becomes { b: 2 }
92
- ```
219
+ async function runDeleteExample() {
220
+ const { select, actions } = deleteStore();
221
+
222
+ console.log("Initial state:", select(s => s));
223
+ // Initial state: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg', dimensions: { width: 10, height: 20 } }, tags: ['electronics', 'new'] }
224
+
225
+ await actions.removeDimensions();
226
+ console.log("After removing dimensions:", select(s => s));
227
+ // After removing dimensions: { id: 'product-123', name: 'Fancy Gadget', details: { color: 'blue', weight: '1kg' }, tags: ['electronics', 'new'] }
93
228
 
94
- ## Advanced Features
229
+ await actions.removeDetails();
230
+ console.log("After removing details:", select(s => s));
231
+ // After removing details: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics', 'new'] }
232
+
233
+ await actions.removeTag('new');
234
+ console.log("After removing 'new' tag:", select(s => s));
235
+ // After removing 'new' tag: { id: 'product-123', name: 'Fancy Gadget', tags: ['electronics'] }
236
+
237
+ await actions.clearAllExceptId();
238
+ console.log("After clearing all except ID:", select(s => s));
239
+ // After clearing all except ID: { id: 'product-123' }
240
+ }
241
+
242
+ runDeleteExample();
243
+ ```
95
244
 
96
245
  ### Persistence
97
- Persist state across sessions or synchronize across tabs with built-in adapters.
246
+
247
+ Persist your store's state across browser sessions or synchronize it across multiple tabs.
98
248
 
99
249
  ```tsx
100
250
  import { createStore, WebStoragePersistence, IndexedDBPersistence } from '@asaidimu/react-store';
101
251
 
102
- const persistence = new WebStoragePersistence('my-store'); // Uses localStorage by default
103
- const useStore = createStore(
252
+ // 1. Using WebStoragePersistence (localStorage by default)
253
+ // Data persists even if the browser tab is closed and reopened.
254
+ const localStorePersistence = new WebStoragePersistence('my-app-state-key');
255
+ const useLocalStore = createStore(
104
256
  {
105
- state: { count: 0 },
257
+ state: { sessionCount: 0, lastVisited: new Date().toISOString() },
106
258
  actions: {
107
- increment: (state) => ({ count: state.count + 1 }),
259
+ incrementSessionCount: (state) => ({ sessionCount: state.sessionCount + 1 }),
260
+ updateLastVisited: () => ({ lastVisited: new Date().toISOString() }),
108
261
  },
109
262
  },
110
- { persistence },
263
+ { persistence: localStorePersistence },
111
264
  );
112
265
 
113
- function App() {
114
- const { store, isReady } = useStore();
115
- if (!isReady) return <div>Loading...</div>;
116
- // Use store...
117
- }
118
- ```
266
+ // 2. Using WebStoragePersistence (sessionStorage)
267
+ // Data only persists for the duration of the browser tab. Clears on tab close.
268
+ const sessionStoragePersistence = new WebStoragePersistence('my-session-state-key', true);
269
+ const useSessionStore = createStore(
270
+ {
271
+ state: { tabSpecificData: 'initial' },
272
+ actions: {
273
+ updateTabSpecificData: (state, newData: string) => ({ tabSpecificData: newData }),
274
+ },
275
+ },
276
+ { persistence: sessionStoragePersistence },
277
+ );
119
278
 
120
- - **IndexedDBPersistence**: High-capacity storage with cross-tab sync.
121
- - **WebStoragePersistence**: Lightweight storage with optional sessionStorage support and cross-tab notifications for localStorage.
122
- - **LocalStoragePersistence** (Deprecated): Alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead for future compatibility.
279
+ // 3. Using IndexedDBPersistence
280
+ // Ideal for larger amounts of data, offers robust cross-tab synchronization.
281
+ const indexedDBPersistence = new IndexedDBPersistence('user-profile-data');
282
+ const useUserProfileStore = createStore(
283
+ {
284
+ state: { userId: '', preferences: { language: 'en', darkMode: false } },
285
+ actions: {
286
+ setUserId: (state, id: string) => ({ userId: id }),
287
+ toggleDarkMode: (state) => ({ preferences: { darkMode: !state.preferences.darkMode } }),
288
+ },
289
+ },
290
+ { persistence: indexedDBPersistence },
291
+ );
123
292
 
124
- #### Using sessionStorage
125
- ```tsx
126
- const persistence = new WebStoragePersistence('my-store', true); // Uses sessionStorage
293
+ function AppWithPersistence() {
294
+ const { select: selectLocal, actions: actionsLocal, isReady: localReady } = useLocalStore();
295
+ const { select: selectProfile, actions: actionsProfile, isReady: profileReady } = useUserProfileStore();
296
+
297
+ const sessionCount = selectLocal(s => s.sessionCount);
298
+ const darkMode = selectProfile(s => s.preferences.darkMode);
299
+
300
+ React.useEffect(() => {
301
+ if (localReady) {
302
+ actionsLocal.incrementSessionCount();
303
+ actionsLocal.updateLastVisited();
304
+ }
305
+ if (profileReady && !selectProfile(s => s.userId)) {
306
+ actionsProfile.setUserId('user-' + Math.random().toString(36).substring(2, 9));
307
+ }
308
+ }, [localReady, profileReady]);
309
+
310
+ if (!localReady || !profileReady) {
311
+ return <div>Loading persisted data...</div>;
312
+ }
313
+
314
+ return (
315
+ <div>
316
+ <h3>Local Store</h3>
317
+ <p>Session Count: {sessionCount}</p>
318
+
319
+ <h3>User Profile Store (IndexedDB)</h3>
320
+ <p>Dark Mode: {darkMode ? 'Enabled' : 'Disabled'}</p>
321
+ <button onClick={() => actionsProfile.toggleDarkMode()}>Toggle Dark Mode</button>
322
+ </div>
323
+ );
324
+ }
127
325
  ```
128
326
 
129
327
  ### Middleware
130
328
 
131
- ```tsx
132
- const logger = (state, update) => {
133
- console.log('Update:', update);
329
+ Middleware functions can intercept and modify or block state updates. They are executed in the order they are added.
330
+
331
+ ```typescript
332
+ import { createStore, Middleware, BlockingMiddleware } from '@asaidimu/react-store';
333
+
334
+ interface CartState {
335
+ items: Array<{ id: string; name: string; quantity: number; price: number }>;
336
+ total: number;
337
+ }
338
+
339
+ const calculateTotalMiddleware: Middleware<CartState> = (state, update) => {
340
+ if (update.items) {
341
+ const newItems = update.items as CartState['items'];
342
+ const newTotal = newItems.reduce((sum, item) => sum + (item.quantity * item.price), 0);
343
+ return { ...update, total: newTotal };
344
+ }
134
345
  return update;
135
346
  };
136
347
 
137
- const validator = (state, update) => {
138
- if (update.count > 100) {
139
- console.warn('Count too high!');
140
- return false;
348
+ const validateItemMiddleware: BlockingMiddleware<CartState> = (state, update) => {
349
+ if (update.items) {
350
+ for (const item of update.items as CartState['items']) {
351
+ if (item.quantity < 0) {
352
+ console.warn('Blocked: Item quantity cannot be negative.');
353
+ return false; // Blocks the update
354
+ }
355
+ }
141
356
  }
142
- return true;
357
+ return true; // Allows the update
143
358
  };
144
359
 
145
- const useStore = createStore({
146
- state: { count: 0 },
147
- actions: { increment: (state) => ({ count: state.count + 1 }) },
148
- middleware: { logger },
149
- blockingMiddleware: { validator },
360
+ const useCartStore = createStore({
361
+ state: { items: [], total: 0 },
362
+ actions: {
363
+ addItem: (state, item: { id: string; name: string; price: number }) => {
364
+ const existingItem = state.items.find(i => i.id === item.id);
365
+ if (existingItem) {
366
+ return {
367
+ items: state.items.map(i =>
368
+ i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
369
+ ),
370
+ };
371
+ }
372
+ return {
373
+ items: [...state.items, { ...item, quantity: 1 }],
374
+ };
375
+ },
376
+ updateQuantity: (state, id: string, quantity: number) => ({
377
+ items: state.items.map(item => (item.id === id ? { ...item, quantity } : item)),
378
+ }),
379
+ },
380
+ middleware: { calculateTotal: calculateTotalMiddleware },
381
+ blockingMiddleware: { validateItem: validateItemMiddleware },
150
382
  });
383
+
384
+ function CartComponent() {
385
+ const { select, actions } = useCartStore();
386
+ const items = select(s => s.items);
387
+ const total = select(s => s.total);
388
+
389
+ return (
390
+ <div>
391
+ <h2>Shopping Cart</h2>
392
+ <ul>
393
+ {items.map(item => (
394
+ <li key={item.id}>
395
+ {item.name} ({item.quantity}) - ${item.price} each
396
+ <button onClick={() => actions.updateQuantity(item.id, item.quantity - 1)}>-</button>
397
+ <button onClick={() => actions.updateQuantity(item.id, item.quantity + 1)}>+</button>
398
+ </li>
399
+ ))}
400
+ </ul>
401
+ <p>Total: ${total.toFixed(2)}</p>
402
+ <button onClick={() => actions.addItem({ id: 'apple', name: 'Apple', price: 1.50 })}>Add Apple</button>
403
+ <button onClick={() => actions.updateQuantity('apple', -1)}>Set Apple Quantity to -1 (Blocked)</button>
404
+ </div>
405
+ );
406
+ }
151
407
  ```
152
408
 
153
409
  ### Observability
@@ -155,266 +411,633 @@ const useStore = createStore({
155
411
  Enable metrics and debugging via the `store` and `observer` objects:
156
412
 
157
413
  ```tsx
158
- const useStore = createStore(
414
+ import { createStore } from '@asaidimu/react-store';
415
+
416
+ const useObservedStore = createStore(
159
417
  {
160
- state: { count: 0 },
161
- actions: { increment: (state) => ({ count: state.count + 1 }) },
418
+ state: { task: '', completed: false },
419
+ actions: {
420
+ addTask: (state, taskName: string) => ({ task: taskName, completed: false }),
421
+ completeTask: (state) => ({ completed: true }),
422
+ },
162
423
  },
163
424
  {
164
- enableMetrics: true,
165
- enableConsoleLogging: true,
166
- logEvents: { updates: true, middleware: false },
167
- debounceTime: 500, // Debounce actions by 500ms
425
+ enableMetrics: true, // Crucial for enabling the 'observer' object
426
+ enableConsoleLogging: true, // Log events directly to browser console
427
+ logEvents: { updates: true, middleware: true, transactions: true }, // Which event types to log
428
+ performanceThresholds: {
429
+ updateTime: 50, // Warn if updates take longer than 50ms
430
+ middlewareTime: 20 // Warn if middleware takes longer than 20ms
431
+ },
432
+ maxEvents: 500, // Max number of events to keep in history
433
+ maxStateHistory: 50, // Max number of state snapshots for time travel
434
+ debounceTime: 300, // Debounce actions by 300ms to prevent rapid calls
168
435
  },
169
436
  );
170
437
 
171
438
  function DebugPanel() {
172
- const { observer } = useStore();
173
- const metrics = observer.getPerformanceMetrics();
174
- const timeTravel = observer.createTimeTravel();
439
+ const { actions, observer, actionTracker } = useObservedStore();
440
+
441
+ // Access performance metrics
442
+ const metrics = observer?.getPerformanceMetrics();
443
+
444
+ // Access state history for time travel
445
+ const timeTravel = observer?.createTimeTravel();
446
+
447
+ // Access action execution history
448
+ const actionHistory = actionTracker.getExecutions();
175
449
 
176
450
  return (
177
451
  <div>
178
- <p>Updates: {metrics.updateCount}</p>
179
- <button onClick={() => timeTravel.undo()}>Undo</button>
180
- <button onClick={() => timeTravel.redo()}>Redo</button>
452
+ <h2>Debug Panel</h2>
453
+ {observer && (
454
+ <>
455
+ <h3>Performance Metrics</h3>
456
+ <p>Update Count: {metrics?.updateCount}</p>
457
+ <p>Avg Update Time: {metrics?.averageUpdateTime?.toFixed(2)}ms</p>
458
+ <p>Largest Update Size (paths): {metrics?.largestUpdateSize}</p>
459
+
460
+ <h3>Time Travel</h3>
461
+ <button onClick={() => timeTravel?.undo()} disabled={!timeTravel?.canUndo()}>Undo</button>
462
+ <button onClick={() => timeTravel?.redo()} disabled={!timeTravel?.canRedo()}>Redo</button>
463
+ <p>State History: {timeTravel?.getHistoryLength()}</p>
464
+
465
+ <h3>Action History</h3>
466
+ <ul>
467
+ {actionHistory.slice(0, 5).map(exec => (
468
+ <li key={exec.id}>
469
+ <strong>{exec.name}</strong> ({exec.status}) - {exec.duration.toFixed(2)}ms
470
+ </li>
471
+ ))}
472
+ </ul>
473
+ </>
474
+ )}
475
+ <button onClick={() => actions.addTask('Learn React Store')}>Add Task</button>
476
+ <button onClick={() => actions.completeTask()}>Complete Task</button>
181
477
  </div>
182
478
  );
183
479
  }
184
480
  ```
185
481
 
186
- **Note**: The `debounceTime` option configures the debouncing applied to actions, preventing issues from rapid successive calls.
187
-
188
482
  ### Remote Observability
189
483
 
190
- Send metrics to external systems:
484
+ Send collected metrics and traces to external systems like OpenTelemetry, Prometheus, or Grafana Cloud for centralized monitoring.
191
485
 
192
486
  ```tsx
193
487
  import { createStore, useRemoteObservability } from '@asaidimu/react-store';
488
+ import React, { useEffect } from 'react';
489
+
490
+ const useRemoteStore = createStore(
491
+ {
492
+ state: { apiCallsMade: 0, lastApiError: null },
493
+ actions: {
494
+ simulateApiCall: async (state) => {
495
+ // Simulate an error 10% of the time
496
+ if (Math.random() < 0.1) {
497
+ throw new Error('API request failed');
498
+ }
499
+ return { apiCallsMade: state.apiCallsMade + 1, lastApiError: null };
500
+ },
501
+ handleApiError: (state, error: string) => ({ lastApiError: error })
502
+ },
503
+ },
504
+ {
505
+ enableMetrics: true, // Required for RemoteObservability
506
+ enableConsoleLogging: false,
507
+ collectCategories: {
508
+ performance: true,
509
+ errors: true,
510
+ stateChanges: true,
511
+ middleware: true,
512
+ },
513
+ reportingInterval: 10000, // Send metrics every 10 seconds
514
+ batchSize: 10, // Send after 10 metrics or interval, whichever comes first
515
+ immediateReporting: false, // Don't send immediately after each metric
516
+ }
517
+ );
194
518
 
195
- const useStore = createStore({ state: { count: 0 }, actions: {} });
196
- function Monitoring() {
197
- const { store } = useStore();
198
- const { remote, addOpenTelemetryDestination } = useRemoteObservability(store, {
199
- serviceName: 'my-app',
200
- environment: 'dev',
519
+ function MonitoringIntegration() {
520
+ const { store, observer } = useRemoteStore();
521
+ const { remote, addOpenTelemetryDestination, addPrometheusDestination, addGrafanaCloudDestination } = useRemoteObservability(store, {
522
+ serviceName: 'my-react-app',
523
+ environment: 'development',
524
+ instanceId: `web-client-${Math.random().toString(36).substring(2, 9)}`,
201
525
  });
202
526
 
203
527
  useEffect(() => {
528
+ // Add OpenTelemetry Collector as a destination
204
529
  addOpenTelemetryDestination({
205
- endpoint: 'https://otel-collector.example.com',
206
- apiKey: 'your-api-key',
530
+ endpoint: 'http://localhost:4318', // Default OpenTelemetry HTTP endpoint
531
+ apiKey: 'your-otel-api-key',
532
+ resource: { 'app.version': '1.0.0', 'host.name': 'frontend-server' }
533
+ });
534
+
535
+ // Add Prometheus Pushgateway as a destination
536
+ addPrometheusDestination({
537
+ pushgatewayUrl: 'http://localhost:9091', // Default Prometheus Pushgateway
538
+ jobName: 'react-store-metrics',
539
+ username: 'promuser', // Optional basic auth
540
+ password: 'prompassword',
207
541
  });
542
+
543
+ // Add Grafana Cloud Loki as a destination (for logs/traces)
544
+ addGrafanaCloudDestination({
545
+ url: 'https://loki-prod-us-central1.grafana.net', // Example Loki endpoint
546
+ apiKey: 'your-grafana-cloud-api-key',
547
+ });
548
+
549
+ // Report current store metrics periodically (in addition to event-driven metrics)
550
+ const interval = setInterval(() => {
551
+ observer?.reportCurrentMetrics();
552
+ }, 5000); // Report every 5 seconds
553
+
554
+ return () => clearInterval(interval);
208
555
  }, []);
209
556
 
210
- return null;
557
+ return null; // This component doesn't render anything visually
211
558
  }
559
+
560
+ // In your App component:
561
+ // <MonitoringIntegration />
562
+ // <button onClick={() => useRemoteStore().actions.simulateApiCall().catch(e => useRemoteStore().actions.handleApiError(e.message))}>
563
+ // Simulate API Call
564
+ // </button>
212
565
  ```
213
566
 
214
- ## API Reference
567
+ ### Transaction Support
215
568
 
216
- ### `createStore(definition, options)`
569
+ Group related updates that should succeed or fail together. If an error occurs within the transaction, all changes made during that transaction are automatically rolled back.
217
570
 
218
571
  ```typescript
219
- type StoreDefinition<T, R extends Actions<T>> = {
220
- state: T;
221
- actions: R;
222
- middleware?: Record<string, Middleware<T>>;
223
- blockingMiddleware?: Record<string, BlockingMiddleware<T>>;
224
- };
572
+ import { createStore } from '@asaidimu/react-store';
225
573
 
226
- interface StoreOptions<T> {
227
- enableMetrics?: boolean;
228
- enableConsoleLogging?: boolean;
229
- maxEvents?: number;
230
- maxStateHistory?: number;
231
- logEvents?: { updates?: boolean; middleware?: boolean; transactions?: boolean };
232
- performanceThresholds?: { updateTime?: number; middlewareTime?: number };
233
- persistence?: DataStorePersistence<T>;
234
- debounceTime?: number;
574
+ interface BankState {
575
+ checking: number;
576
+ savings: number;
577
+ transactions: string[];
235
578
  }
236
579
 
237
- const useStore = createStore(definition, options);
580
+ const useBankStore = createStore<BankState, any>({
581
+ state: { checking: 1000, savings: 500, transactions: [] },
582
+ actions: {
583
+ transferFunds: async (state, fromAccount: 'checking' | 'savings', toAccount: 'checking' | 'savings', amount: number) => {
584
+ if (amount <= 0) {
585
+ throw new Error('Transfer amount must be positive.');
586
+ }
587
+
588
+ const newChecking = fromAccount === 'checking' ? state.checking - amount : state.checking + amount;
589
+ const newSavings = fromAccount === 'savings' ? state.savings - amount : state.savings + amount;
590
+
591
+ if ((fromAccount === 'checking' && newChecking < 0) || (fromAccount === 'savings' && newSavings < 0)) {
592
+ throw new Error('Insufficient funds.');
593
+ }
594
+
595
+ // Simulate a complex operation that might fail
596
+ if (amount > 700 && fromAccount === 'checking') {
597
+ throw new Error('Large transfers from checking require additional verification.');
598
+ }
599
+
600
+ const newTransactions = [...state.transactions, `Transfer ${amount} from ${fromAccount} to ${toAccount}`];
601
+ return {
602
+ checking: newChecking,
603
+ savings: newSavings,
604
+ transactions: newTransactions,
605
+ };
606
+ },
607
+ },
608
+ });
609
+
610
+ function BankApp() {
611
+ const { select, actions } = useBankStore();
612
+ const checkingBalance = select(s => s.checking);
613
+ const savingsBalance = select(s => s.savings);
614
+ const transactions = select(s => s.transactions);
615
+
616
+ const handleTransfer = async (from: 'checking' | 'savings', to: 'checking' | 'savings', amount: number) => {
617
+ try {
618
+ await actions.transferFunds(from, to, amount);
619
+ alert(`Successfully transferred ${amount} from ${from} to ${to}.`);
620
+ } catch (error) {
621
+ alert(`Transfer failed: ${error instanceof Error ? error.message : String(error)}`);
622
+ // State is automatically rolled back if an error occurs within the transaction
623
+ }
624
+ };
625
+
626
+ return (
627
+ <div>
628
+ <h2>Bank Accounts</h2>
629
+ <p>Checking: ${checkingBalance.toFixed(2)}</p>
630
+ <p>Savings: ${savingsBalance.toFixed(2)}</p>
631
+ <h3>Recent Transactions</h3>
632
+ <ul>
633
+ {transactions.map((t, i) => <li key={i}>{t}</li>)}
634
+ </ul>
635
+ <button onClick={() => handleTransfer('checking', 'savings', 100)}>Transfer $100 (Checking to Savings)</button>
636
+ <button onClick={() => handleTransfer('savings', 'checking', 200)}>Transfer $200 (Savings to Checking)</button>
637
+ <button onClick={() => handleTransfer('checking', 'savings', 800)}>Transfer $800 (Will Fail)</button>
638
+ <button onClick={() => handleTransfer('checking', 'savings', 1500)}>Transfer $1500 (Insufficient Funds)</button>
639
+ </div>
640
+ );
641
+ }
642
+ ```
643
+
644
+ ### Event System
645
+
646
+ The store emits various events during its lifecycle, which you can subscribe to for logging, analytics, or custom side effects.
647
+
648
+ ```typescript
649
+ import { createStore } from '@asaidimu/react-store';
650
+ import React, { useEffect } from 'react';
651
+
652
+ const useEventStore = createStore(
653
+ {
654
+ state: { data: 'initial', processedCount: 0 },
655
+ actions: {
656
+ processData: (state, newData: string) => ({ data: newData, processedCount: state.processedCount + 1 }),
657
+ triggerError: () => { throw new Error("Action failed intentionally"); }
658
+ },
659
+ middleware: {
660
+ myLoggingMiddleware: (state, update) => {
661
+ console.log('Middleware processing:', update);
662
+ return update;
663
+ }
664
+ }
665
+ }
666
+ );
667
+
668
+ function EventMonitor() {
669
+ const { store } = useEventStore();
670
+ const [eventLogs, setEventLogs] = React.useState<string[]>([]);
671
+
672
+ useEffect(() => {
673
+ const addLog = (message: string) => {
674
+ setEventLogs(prev => [`${new Date().toLocaleTimeString()}: ${message}`, ...prev].slice(0, 10));
675
+ };
676
+
677
+ // Subscribe to specific store events
678
+ const unsubscribeUpdateStart = store.onStoreEvent('update:start', (data) => {
679
+ addLog(`Update Started (timestamp: ${data.timestamp})`);
680
+ });
681
+
682
+ const unsubscribeUpdateComplete = store.onStoreEvent('update:complete', (data) => {
683
+ if (data.blocked) {
684
+ addLog(`Update BLOCKED by middleware or error. Error: ${data.error?.message || 'unknown'}`);
685
+ } else {
686
+ addLog(`Update Completed in ${data.duration?.toFixed(2)}ms. Paths changed: ${data.changedPaths?.join(', ')}`);
687
+ }
688
+ });
689
+
690
+ const unsubscribeMiddlewareStart = store.onStoreEvent('middleware:start', (data) => {
691
+ addLog(`Middleware '${data.name}' started (${data.type})`);
692
+ });
693
+
694
+ const unsubscribeMiddlewareError = store.onStoreEvent('middleware:error', (data) => {
695
+ addLog(`Middleware '${data.name}' encountered an error: ${data.error.message}`);
696
+ });
697
+
698
+ const unsubscribeTransactionStart = store.onStoreEvent('transaction:start', () => {
699
+ addLog(`Transaction Started`);
700
+ });
701
+
702
+ const unsubscribeTransactionError = store.onStoreEvent('transaction:error', (data) => {
703
+ addLog(`Transaction Failed: ${data.error.message}`);
704
+ });
705
+
706
+ const unsubscribePersistenceReady = store.onStoreEvent('persistence:ready', () => {
707
+ addLog(`Persistence is READY.`);
708
+ });
709
+
710
+ // Cleanup subscriptions on component unmount
711
+ return () => {
712
+ unsubscribeUpdateStart();
713
+ unsubscribeUpdateComplete();
714
+ unsubscribeMiddlewareStart();
715
+ unsubscribeMiddlewareError();
716
+ unsubscribeTransactionStart();
717
+ unsubscribeTransactionError();
718
+ unsubscribePersistenceReady();
719
+ };
720
+ }, [store]); // Re-subscribe if store instance changes (unlikely)
721
+
722
+ const { actions } = useEventStore();
723
+
724
+ return (
725
+ <div>
726
+ <h3>Store Event Log</h3>
727
+ <button onClick={() => actions.processData('new data')}>Process Data</button>
728
+ <button onClick={() => actions.triggerError().catch(() => {})}>Trigger Action Error</button>
729
+ <button onClick={() => store.transaction(() => { actions.processData('transaction data'); throw new Error('Transaction error'); }).catch(() => {})}>
730
+ Simulate Transaction Error
731
+ </button>
732
+ <ul style={{ maxHeight: '200px', overflowY: 'auto', border: '1px solid #ccc', padding: '10px' }}>
733
+ {eventLogs.map((log, index) => <li key={index} style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>{log}</li>)}
734
+ </ul>
735
+ </div>
736
+ );
737
+ }
238
738
  ```
239
739
 
240
- **Returns**: A `useStore` hook with:
241
- - `store`: `ReactiveDataStore` instance.
242
- - `observer`: `StoreObservability` instance (if `enableMetrics` is true).
243
- - `select`: Memoized selector for state slices.
244
- - `actions`: Action dispatchers.
245
- - `actionTracker`: Tracks action executions.
246
- - `state`: Getter for full state.
247
- - `isReady`: Boolean indicating persistence initialization completion.
248
-
249
- ### `ReactiveDataStore` (via `store`)
250
-
251
- - `get(): T`
252
- - `set(update: StateUpdater<T>): Promise<void>`
253
- - `subscribe(path: string | string[], listener: (state: T) => void): () => void`
254
- - `transaction<R>(operation: () => R | Promise<R>): Promise<R>`
255
- - `use(middleware: Middleware<T>, name?: string)`
256
- - `useBlockingMiddleware(middleware: BlockingMiddleware<T>, name?: string)`
257
- - `isReady(): boolean`
258
- - `onStoreEvent(event: StoreEvent, listener): () => void`
259
-
260
- ### `StoreObservability` (via `observer`)
261
-
262
- - `getEventHistory(): DebugEvent[]`
263
- - `getStateHistory(): T[]`
264
- - `getRecentChanges(limit?: number): { timestamp, changedPaths, from, to }[]`
265
- - `getPerformanceMetrics(): StoreMetrics`
266
- - `createTimeTravel(): { undo, redo, canUndo, canRedo }`
267
- - `createLoggingMiddleware(options)`
268
- - `createValidationMiddleware(validator)`
269
-
270
- ### `RemoteObservability`
271
-
272
- - Extend `StoreObservability` with `useRemoteObservability(store, options)`:
273
- - `addDestination(destination: RemoteDestination)`
274
- - `beginTrace(name: string)`
275
- - `beginSpan(traceId, name, labels)`
276
- - `endSpan(traceId, spanName)`
277
- - `endTrace(traceId)`
278
-
279
- ### Persistence Adapters
280
-
281
- - **`IndexedDBPersistence(storeId: string)`**
282
- - **`WebStoragePersistence(storageKey: string, session?: boolean)`**
283
- - Methods: `set(id, state)`, `get()`, `subscribe(id, callback)`, `clear()`
284
- - **`LocalStoragePersistence(storageKey: string)`** (Deprecated)
285
- - Alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
286
-
287
- ## Advanced Hook Properties
288
-
289
- The hook returned by `createStore` (e.g., `useCounter()` in examples) provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
740
+ ### Advanced Hook Properties
741
+
742
+ The hook returned by `createStore` provides several properties for advanced usage and debugging, beyond the commonly used `select`, `actions`, and `isReady`:
290
743
 
291
744
  ```tsx
292
- function MyComponent() {
745
+ function MyAdvancedComponent() {
293
746
  const {
294
- select, // Function to select state parts
295
- actions, // Object containing your defined actions
747
+ select, // Function to select state parts (memoized)
748
+ actions, // Object containing your defined actions (debounced)
296
749
  isReady, // Boolean indicating if persistence is ready
297
750
  store, // Direct access to the ReactiveDataStore instance
298
751
  observer, // StoreObservability instance (if `enableMetrics` was true)
299
752
  actionTracker, // Instance of ActionTracker for monitoring action executions
300
753
  state, // A hook `() => T` to get the entire reactive state object
301
- } = useCounter();
754
+ } = useMyStore(); // Assuming useMyStore is defined from createStore
302
755
 
303
- // Example: Accessing the full state (use with caution for performance)
756
+ // Example: Accessing the full state (use with caution for performance, `select` is preferred)
304
757
  const fullCurrentState = state();
758
+ console.log("Full reactive state:", fullCurrentState);
759
+
760
+ // Example: Accessing observer methods (if enabled)
761
+ if (observer) {
762
+ console.log("Performance metrics:", observer.getPerformanceMetrics());
763
+ console.log("Recent state changes:", observer.getRecentChanges(3));
764
+ }
765
+
766
+ // Example: Accessing action history
767
+ console.log("Action executions:", actionTracker.getExecutions());
305
768
 
306
- // Example: Accessing observer methods or action history
307
- // if (observer) console.log(observer.getPerformanceMetrics());
308
- // console.log(actionTracker.getHistory());
769
+ return (
770
+ <div>
771
+ {/* ... your component content ... */}
772
+ </div>
773
+ );
309
774
  }
310
775
  ```
311
776
 
312
- - `store`: Provides direct access to the underlying `ReactiveDataStore` instance. This can be useful for advanced control or accessing methods not directly exposed by other parts of the hook (e.g., `store.getPerformanceMetrics()` if `observer` is not enabled).
313
- - `observer`: If `enableMetrics` was true during store creation, this is the `StoreObservability` instance. It offers methods like `getPerformanceMetrics()`, `getStateHistory()`, and `createTimeTravel()` for time-travel debugging.
314
- - `actionTracker`: An `ActionTracker` instance that records information about dispatched actions, including their parameters, status (success/error), and duration. Its history can be valuable for debugging action flows.
315
- - `state`: A hook that, when called (e.g., `state()`), returns the entire current state object. Be mindful that using this will cause your component to re-render on *any* change to the store, unlike the more granular `select` hook. Prefer `select` for optimal component performance.
316
-
317
- ## Best Practices
318
-
319
- 1. **Granular Selectors**:
320
- ```tsx
321
- const count = select((state) => state.count); // Good
322
- const allState = select((state) => state); // Avoid
323
- ```
324
-
325
- 2. **Action Design**:
326
- ```tsx
327
- actions: {
328
- fetchData: async (state, id: string) => {
329
- const data = await fetch(`/api/${id}`).then((res) => res.json());
330
- return { data };
331
- },
332
- }
333
- ```
334
-
335
- 3. **Persistence**:
336
- - Use unique `storeId` or `storageKey` to avoid conflicts.
337
- - Check `isReady` for UI dependent on persisted state:
338
- ```tsx
339
- if (!isReady) return <div>Loading...</div>;
340
- ```
341
-
342
- 4. **Middleware**:
343
- ```tsx
344
- const apiMonitor = (state, update) => {
345
- if (update.apiCalls) telemetry.log(update.apiCalls);
346
- return update;
347
- };
348
- ```
349
-
350
- ## Example with Persistence and Observability
777
+ ## Project Architecture
351
778
 
352
- ```tsx
353
- import { createStore, WebStoragePersistence } from '@asaidimu/react-store';
779
+ `@asaidimu/react-store` is structured to provide a modular yet integrated state management solution.
354
780
 
355
- const persistence = new WebStoragePersistence('counter-store');
356
- const useCounter = createStore(
357
- {
358
- state: { count: 0 },
359
- actions: {
360
- increment: (state) => ({ count: state.count + 1 }),
361
- },
362
- middleware: {
363
- log: (state, update) => {
364
- console.log('Update:', update);
365
- return update;
366
- },
367
- },
368
- },
369
- {
370
- persistence,
371
- enableMetrics: true,
372
- enableConsoleLogging: true,
373
- },
374
- );
781
+ ```
782
+ .
783
+ ├── src/
784
+ │ ├── hooks/
785
+ │ │ └── observability.ts # React hook for remote observability
786
+ │ ├── persistence/
787
+ │ │ ├── indexedb.ts # IndexedDB persistence adapter
788
+ │ │ ├── local-storage.ts # WebStorage (localStorage/sessionStorage) persistence adapter
789
+ │ │ └── types.ts # Interface for persistence adapters
790
+ │ ├── state/
791
+ │ │ ├── diff.ts # Utility for deep diffing state objects
792
+ │ │ ├── merge.ts # Utility for immutable deep merging state objects
793
+ │ │ ├── observability.ts # Core observability logic for ReactiveDataStore
794
+ │ │ └── store.ts # Core ReactiveDataStore implementation (the state machine)
795
+ │ ├── store/
796
+ │ │ ├── compare.ts # Utilities for fast comparison (e.g., array hashing)
797
+ │ │ ├── execution.ts # Action tracking interface and class
798
+ │ │ ├── hash.ts # Utilities for hashing objects (e.g., for selectors)
799
+ │ │ ├── index.ts # Main `createStore` React hook
800
+ │ │ ├── paths.ts # Utility for building selector paths
801
+ │ │ └── selector.ts # Selector memoization manager
802
+ │ ├── types.ts # Core TypeScript types for the library
803
+ │ └── utils/
804
+ │ ├── destinations.ts # Concrete remote observability destinations (OTel, Prometheus, Grafana)
805
+ │ └── remote-observability.ts # Remote observability extension for StoreObservability
806
+ ├── index.ts # Main entry point for the library
807
+ ├── package.json
808
+ └── tsconfig.json
809
+ ```
375
810
 
376
- function App() {
377
- const { select, actions, isReady, observer } = useCounter();
378
- const count = select((state) => state.count);
811
+ ### Core Components
379
812
 
380
- if (!isReady) return <div>Loading...</div>;
813
+ * **`ReactiveDataStore` (`src/state/store.ts`)**: The heart of the library. It manages the immutable state, processes updates (including debouncing), handles middleware, transactions, and interacts with persistence adapters. It also emits detailed internal events for observability.
814
+ * **`StoreObservability` (`src/state/observability.ts`)**: An extension built on top of `ReactiveDataStore`'s event system. It provides debugging features like event history, state snapshots for time-travel, performance metrics, and utilities to create logging/validation middleware.
815
+ * **`createStore` Hook (`src/store/index.ts`)**: The primary React-facing API. It instantiates `ReactiveDataStore` and `StoreObservability`, wraps actions with debouncing and tracking, and provides the `select` hook powered by `useSyncExternalStore` for efficient component updates.
816
+ * **Persistence Adapters (`src/persistence/`)**: Implement the `DataStorePersistence` interface. `WebStoragePersistence` (for localStorage/sessionStorage) and `IndexedDBPersistence` provide concrete storage solutions with cross-tab synchronization.
817
+ * **`RemoteObservability` (`src/utils/remote-observability.ts`)**: Extends `StoreObservability` to enable sending metrics, logs, and traces to external monitoring systems. It defines a pluggable `RemoteDestination` interface and provides out-of-the-box implementations.
381
818
 
382
- return (
383
- <div>
384
- <p>Count: {count}</p>
385
- <button onClick={() => actions.increment()}>+1</button>
386
- <p>Updates: {observer.getPerformanceMetrics().updateCount}</p>
387
- </div>
388
- );
819
+ ### Data Flow
820
+
821
+ 1. **Action Dispatch**: A React component calls an action (e.g., `actions.increment(1)`). Actions are debounced by default.
822
+ 2. **Action Execution Tracking**: The `ActionTracker` records the action's details (name, params, start time).
823
+ 3. **State Update Request**: The action, after potential debouncing, initiates a `store.set()` call with a partial state update or a function.
824
+ 4. **Transaction Context**: If within a `store.transaction()`, the state is snapshotted for potential rollback.
825
+ 5. **Blocking Middleware**: Updates first pass through `blockingMiddleware`. If any middleware returns `false` or throws, the update is halted, and the state is not modified.
826
+ 6. **Transform Middleware**: If not blocked, updates then pass through transforming `middleware`. These functions can modify the partial update.
827
+ 7. **State Merging**: The final transformed update is immutably merged into the current state using the `merge` utility. `Symbol.for("delete")` is handled here for property removal.
828
+ 8. **Change Detection**: The `diff` utility identifies which paths in the state have truly changed.
829
+ 9. **Persistence**: If changes occurred, the new state is saved via the configured `DataStorePersistence` adapter (e.g., `localStorage`, `IndexedDB`). External changes from persistence are also subscribed to and applied.
830
+ 10. **Listener Notification**: Only `React.useSyncExternalStore` subscribers whose selected paths have changed are notified, triggering re-renders of relevant components.
831
+ 11. **Observability Events**: Throughout this flow, the `ReactiveDataStore` emits fine-grained events (`update:start`, `middleware:complete`, `transaction:error`, etc.) that `StoreObservability` captures for debugging, metrics, and remote reporting.
832
+
833
+ ### Extension Points
834
+
835
+ * **Custom Middleware**: Easily add your own `Middleware` or `BlockingMiddleware` functions for custom logic.
836
+ * **Custom Persistence Adapters**: Implement the `DataStorePersistence<T>` interface to integrate with any storage solution (e.g., a backend API, WebSockets, or a custom in-memory store).
837
+ * **Remote Observability Destinations**: Create new `RemoteDestination` implementations to send metrics and traces to any external observability platform not already supported.
838
+
839
+ ## Development & Contributing
840
+
841
+ We welcome contributions! Please follow the guidelines below.
842
+
843
+ ### Development Setup
844
+
845
+ 1. **Clone the repository:**
846
+ ```bash
847
+ git clone https://github.com/asaidimu/react-store.git
848
+ cd react-store
849
+ ```
850
+ 2. **Install dependencies:**
851
+ This project uses `bun` as the package manager.
852
+ ```bash
853
+ bun install
854
+ ```
855
+
856
+ ### Scripts
857
+
858
+ * `bun ci`: Installs dependencies (for CI/CD environments).
859
+ * `bun test`: Runs all unit tests using `Vitest`.
860
+ * `bun test:ci`: Runs tests in CI mode (single run).
861
+ * `bun clean`: Removes the `dist` directory.
862
+ * `bun prebuild`: Cleans `dist` and runs a sync script (internal).
863
+ * `bun build`: Compiles the TypeScript source into `dist/` for CJS and ESM formats, generates type definitions, and minifies.
864
+ * `bun dev`: Starts a development server (likely for a UI example).
865
+ * `bun postbuild`: Copies `README.md`, `LICENSE.md`, and `dist.package.json` into the `dist` folder.
866
+
867
+ ### Testing
868
+
869
+ Tests are written using `Vitest` and `React Testing Library`.
870
+
871
+ To run tests:
872
+
873
+ ```bash
874
+ bun test
875
+ # or to run in watch mode
876
+ bun test --watch
877
+ ```
878
+
879
+ ### Contributing Guidelines
880
+
881
+ 1. **Fork** the repository and create your branch from `main`.
882
+ 2. **Code Standards**: Ensure your code adheres to existing coding styles (TypeScript, ESLint, Prettier are configured).
883
+ 3. **Tests**: Add unit and integration tests for new features or bug fixes. Ensure all tests pass.
884
+ 4. **Commits**: Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
885
+ 5. **Pull Requests**: Submit a pull request to the `main` branch. Provide a clear description of your changes.
886
+
887
+ ### Issue Reporting
888
+
889
+ For bugs, feature requests, or questions, please open an issue on the [GitHub Issues page](https://github.com/asaidimu/react-store/issues).
890
+
891
+ ## Additional Information
892
+
893
+ ### Best Practices
894
+
895
+ 1. **Granular Selectors**: Always use `select((state) => state.path.to.value)` instead of `select((state) => state)` to prevent unnecessary re-renders of components.
896
+ 2. **Action Design**: Keep actions focused on a single responsibility. Use `async` actions for asynchronous operations and return partial updates upon completion.
897
+ 3. **Persistence**:
898
+ * Use unique `storeId` or `storageKey` for each distinct store to avoid data conflicts.
899
+ * Always check the `isReady` flag for UI elements that depend on the initial state loaded from persistence.
900
+ 4. **Middleware**: Leverage middleware for cross-cutting concerns like logging, analytics, or complex validation logic.
901
+ 5. **`Symbol.for("delete")`**: Use this explicit symbol for property removal to maintain clarity and avoid accidental data mutations.
902
+
903
+ ### API Reference
904
+
905
+ #### `createStore(definition, options)`
906
+
907
+ The main entry point for creating a store.
908
+
909
+ ```typescript
910
+ type StoreDefinition<T, R extends Actions<T>> = {
911
+ state: T; // Initial state object
912
+ actions: R; // Object mapping action names to action functions
913
+ middleware?: Record<string, Middleware<T>>; // Optional transforming middleware
914
+ blockingMiddleware?: Record<string, BlockingMiddleware<T>>; // Optional blocking middleware
915
+ };
916
+
917
+ interface StoreOptions<T> {
918
+ enableMetrics?: boolean; // Enable StoreObservability features (default: false)
919
+ enableConsoleLogging?: boolean; // Log store events to console (default: false)
920
+ maxEvents?: number; // Maximum number of events to keep in history (default: 500)
921
+ maxStateHistory?: number; // Maximum number of state snapshots for time travel (default: 20)
922
+ logEvents?: { // Which event categories to log (defaults to all true if enableConsoleLogging is true)
923
+ updates?: boolean;
924
+ middleware?: boolean;
925
+ transactions?: boolean;
926
+ };
927
+ performanceThresholds?: { // Thresholds for logging slow operations (in ms)
928
+ updateTime?: number; // default: 50ms
929
+ middlewareTime?: number; // default: 20ms
930
+ };
931
+ persistence?: DataStorePersistence<T>; // Optional persistence adapter instance
932
+ debounceTime?: number; // Time in milliseconds to debounce actions (default: 250ms)
389
933
  }
934
+
935
+ const useStore = createStore(definition, options);
390
936
  ```
391
937
 
392
- ## Comparison with Other State Management Solutions
938
+ **Returns**: A `useStore` hook which, when called in a component, returns an object with:
939
+ * `store`: Direct access to the `ReactiveDataStore` instance.
940
+ * `observer`: The `StoreObservability` instance (available if `enableMetrics` is `true`). Provides debug and monitoring utilities.
941
+ * `select`: A memoized selector function to extract specific state slices. Re-renders components only when selected data changes.
942
+ * `actions`: An object containing your defined actions. These actions are debounced and tracked.
943
+ * `actionTracker`: An instance of `ActionTracker` for monitoring the execution history of your actions.
944
+ * `state`: A hook `() => T` that returns the entire current state object. Use sparingly as it will cause re-renders on *any* state change.
945
+ * `isReady`: A boolean indicating whether the store's persistence layer (if configured) has finished loading its initial state.
946
+
947
+ #### `ReactiveDataStore` (accessed via `useStore().store`)
948
+
949
+ * `get(clone?: boolean): T`: Retrieves the current state. Pass `true` to get a deep clone (recommended for mutations outside of actions).
950
+ * `set(update: StateUpdater<T>): Promise<void>`: Updates the state with a partial object or a function returning a partial object.
951
+ * `subscribe(path: string | string[], listener: (state: T) => void): () => void`: Subscribes a listener to changes at a specific path or array of paths. Returns an unsubscribe function.
952
+ * `transaction<R>(operation: () => R | Promise<R>): Promise<R>`: Executes a function as an atomic transaction. Rolls back all changes if an error occurs.
953
+ * `use(middleware: Middleware<T>, name?: string): string`: Adds a transforming middleware. Returns its ID.
954
+ * `useBlockingMiddleware(middleware: BlockingMiddleware<T>, name?: string): string`: Adds a blocking middleware. Returns its ID.
955
+ * `removeMiddleware(id: string): boolean`: Removes a middleware by its ID.
956
+ * `isReady(): boolean`: Checks if the persistence layer has loaded its initial state.
957
+ * `onStoreEvent(event: StoreEvent, listener: (data: any) => void): () => void`: Subscribes to internal store events (e.g., `'update:complete'`, `'middleware:error'`).
958
+
959
+ #### `StoreObservability` (accessed via `useStore().observer`)
960
+
961
+ * `getEventHistory(): DebugEvent[]`: Retrieves a history of all captured store events.
962
+ * `getStateHistory(): T[]`: Returns a history of state snapshots, enabling time-travel.
963
+ * `getRecentChanges(limit?: number): Array<{ timestamp: number; changedPaths: string[]; from: Partial<T>; to: Partial<T>; }>`: Provides a simplified view of recent state changes.
964
+ * `getPerformanceMetrics(): StoreMetrics`: Returns an object containing performance statistics (e.g., `updateCount`, `averageUpdateTime`).
965
+ * `createTimeTravel(): { canUndo: () => boolean; canRedo: () => boolean; undo: () => Promise<void>; redo: () => Promise<void>; getHistoryLength: () => number; clear: () => void; }`: Returns controls for time-travel debugging.
966
+ * `createLoggingMiddleware(options?: object): Middleware<T>`: A factory for a simple logging middleware.
967
+ * `createValidationMiddleware(validator: (state: T, update: DeepPartial<T>) => boolean | { valid: boolean; reason?: string }): BlockingMiddleware<T>`: A factory for a schema validation middleware.
968
+ * `clearHistory(): void`: Clears the event and state history.
969
+ * `disconnect(): void`: Cleans up all listeners and resources.
970
+
971
+ #### `RemoteObservability` (accessed via `useRemoteObservability` hook)
972
+
973
+ Extends `StoreObservability` with methods for sending metrics and traces externally.
393
974
 
394
- `@asaidimu/react-store` is a competitor in the React state management ecosystem, offering a unique blend of reactivity, persistence, and observability. Here’s how it stacks up against popular alternatives:
975
+ * `addDestination(destination: RemoteDestination): boolean`: Adds a remote destination for metrics.
976
+ * `removeDestination(id: string): boolean`: Removes a remote destination by ID.
977
+ * `getDestinations(): Array<{ id: string; name: string }> `: Gets a list of configured destinations.
978
+ * `testAllConnections(): Promise<Record<string, boolean>>`: Tests connectivity to all destinations.
979
+ * `beginTrace(name: string): string`: Starts a new performance trace, returning its ID.
980
+ * `beginSpan(traceId: string, name: string, labels?: Record<string, string>): string`: Starts a new span within a trace, returning its ID.
981
+ * `endSpan(traceId: string, spanName: string): void`: Ends a specific span within a trace.
982
+ * `endTrace(traceId: string): void`: Ends a performance trace and sends it to remote destinations.
983
+ * `trackMetric(metric: RemoteMetricsPayload['metrics'][0]): void`: Manually add a metric to the batch for reporting.
984
+
985
+ #### Persistence Adapters
986
+
987
+ All adapters implement `DataStorePersistence<T>`:
988
+
989
+ * `set(id:string, state: T): boolean | Promise<boolean>`: Persists data.
990
+ * `get(): T | null | Promise<T | null>`: Retrieves data.
991
+ * `subscribe(id:string, callback: (state:T) => void): () => void`: Subscribes to external changes.
992
+ * `clear(): boolean | Promise<boolean>`: Clears persisted data.
993
+
994
+ ##### `IndexedDBPersistence(storeId: string)`
995
+
996
+ * **`storeId`**: A unique identifier for the IndexedDB object store (e.g., `'user-data'`).
997
+
998
+ ##### `WebStoragePersistence(storageKey: string, session?: boolean)`
999
+
1000
+ * **`storageKey`**: The key under which data is stored (e.g., `'app-config'`).
1001
+ * **`session`**: Optional. If `true`, uses `sessionStorage`; otherwise, uses `localStorage` (default: `false`).
1002
+
1003
+ ##### `LocalStoragePersistence(storageKey: string)` (Deprecated)
1004
+
1005
+ * This is an alias for `WebStoragePersistence`. Use `WebStoragePersistence` instead.
1006
+
1007
+ ### Comparison with Other State Management Solutions
1008
+
1009
+ `@asaidimu/react-store` aims to provide a comprehensive, all-in-one solution for React state management. Here's a comparison to popular alternatives:
395
1010
 
396
1011
  | **Feature** | **@asaidimu/react-store** | **Redux** | **Zustand** | **MobX** | **Recoil** |
397
- |-------------------------|---------------------------|--------------------|--------------------|--------------------|--------------------|
1012
+ | :--------------------- | :------------------------ | :----------------- | :----------------- | :----------------- | :----------------- |
398
1013
  | **Dev Experience** | Intuitive hook-based API with rich tooling. | Verbose setup with reducers and middleware. | Minimalist, hook-friendly API. | Reactive, class-based approach. | Atom-based, React-native feel. |
399
1014
  | **Learning Curve** | Moderate (middleware, observability add complexity). | Steep (boilerplate-heavy). | Low (simple API). | Moderate (reactive concepts). | Low to moderate (atom model). |
400
1015
  | **API Complexity** | Medium (rich feature set balanced with simplicity). | High (many concepts: actions, reducers, etc.). | Low (straightforward). | Medium (proxies, decorators). | Medium (atom/selectors). |
401
1016
  | **Scalability** | High (transactions, persistence, remote metrics). | High (structured but verbose). | High (small but flexible). | High (reactive scaling). | High (granular atoms). |
402
1017
  | **Extensibility** | Excellent (middleware, custom persistence, observability). | Good (middleware, enhancers). | Good (middleware-like). | Moderate (custom reactions). | Moderate (custom selectors). |
403
1018
  | **Performance** | Optimized (selectors, reactive updates). | Good (predictable but manual optimization). | Excellent (minimal overhead). | Good (reactive overhead). | Good (granular updates). |
404
- | **Bundle Size** | Moderate (includes observability, persistence). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
1019
+ | **Bundle Size** | Moderate (includes observability, persistence, remote observability). | Large (core + toolkit). | Tiny (~1KB). | Moderate (~20KB). | Moderate (~10KB). |
405
1020
  | **Persistence** | Built-in (IndexedDB, WebStorage, cross-tab). | Manual (via middleware). | Manual (via middleware). | Manual (custom). | Manual (custom). |
406
1021
  | **Observability** | Excellent (metrics, time-travel, remote). | Good (dev tools). | Basic (via plugins). | Good (reactive logs). | Basic (via plugins). |
407
- | **React Integration** | Native (hooks) | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
1022
+ | **React Integration** | Native (hooks, `useSyncExternalStore`) | Manual (React-Redux). | Native (hooks). | Native (observers). | Native (atoms). |
1023
+
1024
+ #### Where `@asaidimu/react-store` Shines
1025
+ * **All-in-One**: It aims to be a single solution for state management, persistence, and observability, reducing the need for multiple external dependencies.
1026
+ * **Flexibility**: The robust middleware system and transaction support make it highly adaptable to complex business logic and data flows.
1027
+ * **Modern React**: It leverages `useSyncExternalStore` for direct integration with React's concurrency model, ensuring efficient and up-to-date component renders.
1028
+
1029
+ #### Trade-Offs
1030
+ * **Bundle Size**: While comprehensive, it naturally has a larger bundle size compared to minimalist alternatives like Zustand, as it includes a wider range of features out-of-the-box.
1031
+ * **Learning Curve**: The rich feature set might present a slightly steeper learning curve for developers new to advanced state management concepts, though the API strives for simplicity.
1032
+
1033
+ ### Changelog
1034
+
1035
+ For a detailed history of changes and new features, please refer to the [CHANGELOG.md](./CHANGELOG.md) file.
408
1036
 
409
- ### Where `@asaidimu/react-store` Shines
410
- - **All-in-One**: Combines state management, persistence, and observability without external dependencies.
411
- - **Flexibility**: Middleware and transactions make it adaptable to complex use cases.
412
- - **Modern React**: Leverages `useSyncExternalStore` for optimal performance and compatibility.
1037
+ ### License
413
1038
 
414
- ### Trade-Offs
415
- - Larger bundle size compared to minimalist solutions like Zustand.
416
- - Slightly steeper learning curve due to advanced features.
1039
+ This project is licensed under the MIT License. See the [LICENSE.md](./LICENSE.md) file for full details.
417
1040
 
418
- ## License
1041
+ ### Acknowledgments
419
1042
 
420
- MIT © [Saidimu](https://github.com/asaidimu)
1043
+ Developed by [Saidimu](https://github.com/asaidimu).