@asaidimu/utils-store 5.0.0 → 6.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
@@ -22,7 +22,6 @@ A comprehensive, type-safe, and reactive state management library for TypeScript
22
22
  - [Middleware System](#middleware-system)
23
23
  - [Transaction Support](#transaction-support)
24
24
  - [Artifacts (Dependency Injection)](#artifacts-dependency-injection)
25
- - [React Integration](#react-integration)
26
25
  - [Store Observer (Debugging & Observability)](#store-observer-debugging--observability)
27
26
  - [Event System](#event-system)
28
27
  - [Project Architecture](#project-architecture)
@@ -49,7 +48,6 @@ This library offers robust tools to handle your state with confidence, enabling
49
48
  * 📦 **Atomic Transaction Support**: Group multiple state updates into a single, atomic operation. If any update within the transaction fails or an error is thrown, the entire transaction is rolled back to the state before the transaction began, guaranteeing data integrity. Supports both synchronous and asynchronous operations.
50
49
  * 💾 **Optional Persistence Layer**: Seamlessly integrate with any `SimplePersistence<T>` implementation (e.g., for local storage, IndexedDB, or backend synchronization) to load an initial state and save subsequent changes. The store emits a `persistence:ready` event and listens for external updates, handling persistence queueing and retries.
51
50
  * 🧩 **Artifacts (Dependency Injection)**: A flexible dependency injection system to manage services, utilities, or complex objects that your actions and other artifacts depend on. Supports `Singleton` (re-evaluated on dependencies change) and `Transient` (new instance every time) scopes, and reactive resolution using `use(({select, resolve}) => ...)` context. Handles dependency graphs and circular dependency detection.
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
51
  * 👀 **Deep Observer & Debugging (`StoreObserver`)**: An optional but highly recommended class for unparalleled runtime introspection and debugging:
54
52
  * **Comprehensive Event History**: Captures a detailed log of all internal store events (`update:start`, `middleware:complete`, `transaction:error`, `persistence:ready`, `middleware:executed`, `action:start`, `selector:accessed`, etc.).
55
53
  * **State Snapshots**: Maintains a configurable history of your application's state over time, allowing for easy inspection of changes between updates and post-mortem analysis.
@@ -82,7 +80,6 @@ yarn add @asaidimu/utils-store
82
80
 
83
81
  * **Node.js**: (LTS version recommended) for development and compilation.
84
82
  * **TypeScript**: (v4.0+ recommended) for full type-safety during development. Modern TS features (ES2017+ for `async/await`, ES2020+ for `Symbol.for()` and `structuredClone()`) are utilized. `moduleResolution`: `Node16` or `Bundler` is recommended in `tsconfig.json`.
85
- * **React**: (v16.8+ for hooks) if using the `@asaidimu/utils-store/react` integration.
86
83
 
87
84
  ### Verification
88
85
 
@@ -698,7 +695,7 @@ console.log('State after manual action:', store.get());
698
695
  const temporaryLoggerId = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) });
699
696
  await store.set({ counter: 1 });
700
697
  // Output: Temporary logger saw: { counter: 1 }
701
- const removed = store.use({ name: 'TemporaryLogger', action: (s, u) => console.log('Temporary logger saw:', u) })(); // Remove the temporary logger
698
+ const removed = temporaryLoggerId(); // Remove the temporary logger
702
699
  console.log('Middleware removed:', removed);
703
700
  await store.set({ counter: 1 }); // TemporaryLogger will not be called now
704
701
  ```
@@ -872,10 +869,8 @@ try {
872
869
 
873
870
  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
871
 
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
872
  ```typescript
878
- import { ArtifactContainer, ArtifactScope } from '@asaidimu/utils-store/artifacts';
873
+ import { ReactiveDataStore, ArtifactContainer, ArtifactScope } from '@asaidimu/utils-store';
879
874
 
880
875
  interface AppState {
881
876
  user: { id: string; name: string; };
@@ -883,12 +878,10 @@ interface AppState {
883
878
  }
884
879
 
885
880
  // Mock DataStore interface for standalone ArtifactContainer usage
886
- let currentState: AppState = {
881
+ const store = new ReactiveDataStore<AppState>({
887
882
  user: { id: 'user-1', name: 'Alice' },
888
883
  config: { apiUrl: '/api/v1', logLevel: 'info' }
889
- };
890
-
891
- const store = new ReactiveDataStore<AppState>(currentState);
884
+ });
892
885
 
893
886
  const container = new ArtifactContainer(store);
894
887
 
@@ -911,16 +904,17 @@ const apiClientCleanup = () => console.log('API Client connection closed.');
911
904
  container.register({
912
905
  key: 'apiClient',
913
906
  scope: ArtifactScope.Singleton,
914
- factory: async ({ use, current }) => {
907
+ factory: async ({ use, onCleanup, current }) => {
915
908
  const apiUrl = await use(({ select }) => select((state: AppState) => state.config.apiUrl));
916
909
  console.log(`API Client created/re-created for URL: ${apiUrl}`);
917
910
  if (current) {
918
911
  console.log('Re-creating API client. Old instance:', current);
919
912
  }
920
- return [{
913
+ onCleanup(apiClientCleanup); // Register cleanup for this instance
914
+ return {
921
915
  fetchUser: (id: string) => `Fetching ${id} from ${apiUrl}`,
922
916
  sendData: (data: any) => `Sending ${JSON.stringify(data)} to ${apiUrl}`
923
- }, apiClientCleanup];
917
+ };
924
918
  }
925
919
  });
926
920
 
@@ -929,17 +923,18 @@ const userServiceCleanup = () => console.log('User Service resources released.')
929
923
  container.register({
930
924
  key: 'userService',
931
925
  scope: ArtifactScope.Singleton,
932
- factory: async ({ use, current }) => {
926
+ factory: async ({ use, onCleanup, current }) => {
933
927
  const apiClient = await use(({ resolve }) => resolve('apiClient'));
934
928
  const userName = await use(({ select }) => select((state: AppState) => state.user.name));
935
929
  console.log(`User Service created/re-created for user: ${userName}`);
936
930
  if (current) {
937
931
  console.log('Re-creating User Service. Old instance:', current);
938
932
  }
939
- return [{
940
- getUserProfile: () => apiClient.fetchUser(mockGet().user.id),
933
+ onCleanup(userServiceCleanup); // Register cleanup for this instance
934
+ return {
935
+ getUserProfile: () => apiClient.instance!.fetchUser(store.get().user.id),
941
936
  updateUserName: (newName: string) => `Updating user name to ${newName} via API. Current: ${userName}`
942
- }, userServiceCleanup];
937
+ };
943
938
  }
944
939
  });
945
940
 
@@ -948,48 +943,47 @@ container.register({
948
943
  async function runDemo() {
949
944
  console.log('\n--- Initial Artifact Resolution ---');
950
945
  const logger1 = await container.resolve('logger');
951
- logger1.log('Application started.');
946
+ logger1.instance!.log('Application started.');
952
947
 
953
948
  const apiClient1 = await container.resolve('apiClient');
954
- console.log(apiClient1.fetchUser('123'));
949
+ console.log(apiClient1.instance!.fetchUser('123'));
955
950
 
956
951
  const userService1 = await container.resolve('userService');
957
- console.log(userService1.getUserProfile());
952
+ console.log(userService1.instance!.getUserProfile());
958
953
 
959
954
  // Transient artifact resolves a new instance
960
955
  const logger2 = await container.resolve('logger');
961
- expect(logger1).not.toBe(logger2); // Will be different instances
956
+ console.log('Logger instances are different:', logger1.instance !== logger2.instance);
962
957
 
963
- // Singleton artifacts resolve the same instance
958
+ // Singleton artifacts resolve the same instance initially
964
959
  const apiClient2 = await container.resolve('apiClient');
965
- expect(apiClient1).toBe(apiClient2);
960
+ console.log('API Client instances are same:', apiClient1.instance === apiClient2.instance);
966
961
 
967
962
  console.log('\n--- Simulate State Change (config.apiUrl) ---');
968
- store.set({ config:{ apiUrl: '/api/v2'}})
963
+ await store.set({ config:{ apiUrl: '/api/v2'}})
964
+ // This state change invalidates 'apiClient' which depends on 'config.apiUrl'
965
+ // and then invalidates 'userService' which depends on 'apiClient'.
969
966
 
970
967
  // After config.apiUrl changes, apiClient (and userService) should be re-created
971
968
  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
969
+ console.log(apiClient3.instance!.fetchUser('123'));
970
+ console.log('API Client instances are different:', apiClient3.instance !== apiClient1.instance); // Should be a new instance
974
971
 
975
972
  // userService should also be re-created because apiClient, its dependency, was re-created
976
973
  const userService2 = await container.resolve('userService');
977
- console.log(userService2.getUserProfile());
978
- expect(userService2).not.toBe(userService1); // Should be a new instance
974
+ console.log(userService2.instance!.getUserProfile());
975
+ console.log('User Service instances are different:', userService2.instance !== userService1.instance); // Should be a new instance
979
976
 
980
977
  console.log('\n--- Simulate State Change (user.name) ---');
981
- simulateStateUpdate(
982
- { ...currentState, user: { ...currentState.user, name: 'Bob' } },
983
- ['user.name']
984
- );
978
+ await store.set({ user: { name: 'Bob' } });
985
979
 
986
980
  // Only userService (which depends on user.name) should be re-created this time, not apiClient
987
981
  const apiClient4 = await container.resolve('apiClient');
988
- expect(apiClient4).toBe(apiClient3); // Still the same API client instance
982
+ console.log('API Client instances are same:', apiClient4.instance === apiClient3.instance); // Still the same API client instance
989
983
 
990
984
  const userService3 = await container.resolve('userService');
991
- console.log(userService3.getUserProfile());
992
- expect(userService3).not.toBe(userService2); // New user service instance
985
+ console.log(userService3.instance!.getUserProfile());
986
+ console.log('User Service instances are different:', userService3.instance !== userService2.instance); // New user service instance
993
987
 
994
988
  console.log('\n--- Unregistering Artifacts ---');
995
989
  await container.unregister('userService');
@@ -1000,220 +994,13 @@ async function runDemo() {
1000
994
  try {
1001
995
  await container.resolve('userService');
1002
996
  } catch (e: any) {
1003
- console.error('Expected error:', e.message);
997
+ console.error('Expected error:', e.message); // Artifact not found
1004
998
  }
1005
999
  }
1006
1000
 
1007
1001
  runDemo();
1008
1002
  ```
1009
1003
 
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
-
1217
1004
  ### Store Observer (Debugging & Observability)
1218
1005
 
1219
1006
  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.
@@ -1509,29 +1296,6 @@ console.log('Final value:', store.get().value, 'Final status:', store.get().stat
1509
1296
 
1510
1297
  The `@asaidimu/utils-store` library is built with a modular, component-based architecture to promote maintainability, testability, and extensibility. Each core concern is encapsulated within its own class, with `ReactiveDataStore` acting as the central coordinator.
1511
1298
 
1512
- ```
1513
- src/store/
1514
- ├── index.ts # Main entry point (exports all public APIs)
1515
- ├── types.ts # TypeScript interfaces and types for the store
1516
- ├── store.ts # `ReactiveDataStore` - The main orchestrator
1517
- ├── state.ts # `StateManager` - Manages the immutable state and diffing
1518
- ├── merge.ts # `createMerge` - Deep merging utility with deletion support
1519
- ├── diff.ts # `createDiff`, `createDerivePaths` - Change detection utilities
1520
- ├── middleware.ts # `MiddlewareEngine` - Manages and executes middleware pipeline
1521
- ├── transactions.ts # `TransactionManager` - Handles atomic state transactions
1522
- ├── persistence.ts # `PersistenceHandler` - Integrates with `SimplePersistence` layer
1523
- ├── metrics.ts # `MetricsCollector` - Gathers runtime performance metrics
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
1533
- ```
1534
-
1535
1299
  ### Core Components
1536
1300
 
1537
1301
  * **`ReactiveDataStore<T>`**: The public API and primary entry point. It orchestrates interactions between all other internal components. It manages the update queue, ensures sequential processing of `set` calls, and exposes public methods like `get`, `dispatch`, `select`, `set`, `watch`, `transaction`, `use`, and `on`.