state-utilities: remove unused stateMiddleware utility
- Removed stateMiddleware utility (431 lines): - Deleted apps/web/src/utils/stateMiddleware.ts - Deleted apps/web/src/utils/stateMiddleware.test.ts (251 lines) - Completely unused in production code (only used in test file) - Previously removed from Library Store in Action 4.1.2.7 - Library Store now only contains UI state (filters), no middleware needed - Created audit documentation: apps/web/src/docs/STATEMIDDLEWARE_UTILITY_AUDIT.md - Action 4.6.1.4 complete
This commit is contained in:
parent
9fa4cc682c
commit
11bc456697
4 changed files with 61 additions and 687 deletions
|
|
@ -1487,13 +1487,12 @@ Critical path dependencies:
|
|||
- **Rollback**: Restore from git
|
||||
- **Status**: ✅ Complete - Deleted 6 files: stateCleanup.ts, stateCleanup.test.ts, stateVersioning.ts, stateVersioning.test.ts, stateVersioning.example.ts, statePersistence.ts. Verified no imports in production code.
|
||||
|
||||
- [ ] **Action 4.6.1.4**: Simplify stateMiddleware if still needed
|
||||
- [x] **Action 4.6.1.4**: Simplify stateMiddleware if still needed
|
||||
- **Scope**: `apps/web/src/utils/stateMiddleware.ts` - Simplify or remove if redundant
|
||||
- **Dependencies**: Action 4.6.1.2 complete ✅, Action 4.1.2.7 complete (Library Store migration)
|
||||
- **Dependencies**: Action 4.6.1.2 complete ✅, Action 4.1.2.7 complete ✅ (Library Store migration)
|
||||
- **Risk**: MEDIUM
|
||||
- **Validation**: Middleware simplified or removed
|
||||
- **Validation**: ✅ Middleware removed - Completely unused in production code. Only used in its own test file. Previously removed from Library Store in Action 4.1.2.7. Deleted `stateMiddleware.ts` (431 lines) and `stateMiddleware.test.ts` (251 lines). No imports found. See `apps/web/src/docs/STATEMIDDLEWARE_UTILITY_AUDIT.md`
|
||||
- **Rollback**: Restore original middleware
|
||||
- **Note**: Currently only used by LibraryStore. Should be removed after Library Store migrates to React Query (Action 4.1.2.7). Deferring simplification until migration is complete.
|
||||
|
||||
- [ ] **Action 4.6.1.5**: Update stateInvalidation to work with React Query
|
||||
- **Scope**: `apps/web/src/utils/stateInvalidation.ts` - Update to invalidate React Query cache
|
||||
|
|
|
|||
58
apps/web/src/docs/STATEMIDDLEWARE_UTILITY_AUDIT.md
Normal file
58
apps/web/src/docs/STATEMIDDLEWARE_UTILITY_AUDIT.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# StateMiddleware Utility Audit
|
||||
|
||||
**Date**: 2025-01-27
|
||||
**Action**: 4.6.1.4
|
||||
**Status**: ✅ Complete
|
||||
|
||||
## Summary
|
||||
|
||||
The `stateMiddleware` utility (`apps/web/src/utils/stateMiddleware.ts`) is **completely unused** in production code. It was previously used by the Library Store but was removed when domain data was migrated to React Query (Action 4.1.2.7).
|
||||
|
||||
## Usage Analysis
|
||||
|
||||
### Imports
|
||||
- ❌ **No imports found** in production code
|
||||
- ✅ **Only used in test file**: `apps/web/src/utils/stateMiddleware.test.ts`
|
||||
- ✅ Only mentioned in comments/docs indicating it was removed
|
||||
|
||||
### Functions Exported
|
||||
1. `stateMiddleware<T>()` - Zustand middleware for logging, analytics, error handling
|
||||
2. `createWithMiddleware<T>()` - Helper to create a store with state middleware
|
||||
|
||||
**Both functions are unused in production code.**
|
||||
|
||||
## Historical Context
|
||||
|
||||
- Previously used by Library Store for logging, analytics, and error handling on domain data
|
||||
- Removed in Action 4.1.2.7 when Library Store domain data was migrated to React Query
|
||||
- React Query handles data fetching and caching, making this middleware unnecessary for domain data
|
||||
- UI state stores (Library Store now only has filters) don't need this level of middleware complexity
|
||||
|
||||
## Current State
|
||||
|
||||
The Library Store now only contains UI state (filters):
|
||||
```typescript
|
||||
export const useLibraryStore = create<LibraryStore>()(
|
||||
devtools(
|
||||
persist(
|
||||
(set) => ({
|
||||
filters: {},
|
||||
setFilters: (filters) => set({ filters }),
|
||||
}),
|
||||
// ...
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
No middleware needed for simple UI state.
|
||||
|
||||
## Recommendation
|
||||
|
||||
✅ **Safe to remove** - The utility is completely unused in production code and can be deleted along with:
|
||||
- `apps/web/src/utils/stateMiddleware.ts` (431 lines)
|
||||
- `apps/web/src/utils/stateMiddleware.test.ts` (test file, 251 lines)
|
||||
|
||||
## Next Steps
|
||||
|
||||
- **Action 4.6.1.4**: Simplify stateMiddleware if still needed ✅ (confirmed unused, can be removed)
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* Tests for State Middleware
|
||||
* FE-STATE-010: Test logging, analytics, and error handling
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
stateMiddleware,
|
||||
setAnalyticsHandler,
|
||||
setErrorHandler,
|
||||
type AnalyticsEvent,
|
||||
} from './stateMiddleware';
|
||||
|
||||
interface TestState {
|
||||
count: number;
|
||||
name: string;
|
||||
increment: () => void;
|
||||
setName: (name: string) => void;
|
||||
throwError: () => void;
|
||||
asyncAction: () => Promise<string>;
|
||||
}
|
||||
|
||||
describe('stateMiddleware', () => {
|
||||
let analyticsEvents: AnalyticsEvent[] = [];
|
||||
let errorEvents: Array<{ error: Error; context: unknown }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
analyticsEvents = [];
|
||||
errorEvents = [];
|
||||
|
||||
setAnalyticsHandler((event) => {
|
||||
analyticsEvents.push(event);
|
||||
});
|
||||
|
||||
setErrorHandler((error, context) => {
|
||||
errorEvents.push({ error, context });
|
||||
});
|
||||
});
|
||||
|
||||
it('should log state changes in development', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
const useTestStore = create<TestState>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
name: '',
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
setName: (name: string) => set({ name }),
|
||||
throwError: () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
asyncAction: async () => {
|
||||
return 'success';
|
||||
},
|
||||
}),
|
||||
{ storeName: 'TestStore', enableLogging: true },
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
store.increment();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should track analytics events', () => {
|
||||
const useTestStore = create<TestState>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
name: '',
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
setName: (name: string) => set({ name }),
|
||||
throwError: () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
asyncAction: async () => {
|
||||
return 'success';
|
||||
},
|
||||
}),
|
||||
{ storeName: 'TestStore', enableAnalytics: true },
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
store.increment();
|
||||
|
||||
expect(analyticsEvents.length).toBeGreaterThan(0);
|
||||
expect(analyticsEvents.some((e) => e.type === 'state_change')).toBe(true);
|
||||
expect(analyticsEvents.some((e) => e.type === 'action_called')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle errors', () => {
|
||||
const useTestStore = create<TestState>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
name: '',
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
setName: (name: string) => set({ name }),
|
||||
throwError: () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
asyncAction: async () => {
|
||||
return 'success';
|
||||
},
|
||||
}),
|
||||
{ storeName: 'TestStore', enableErrorHandling: true },
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
|
||||
expect(() => store.throwError()).toThrow('Test error');
|
||||
expect(errorEvents.length).toBeGreaterThan(0);
|
||||
expect(errorEvents[0].error.message).toBe('Test error');
|
||||
});
|
||||
|
||||
it('should track async action performance', async () => {
|
||||
// Clear previous events
|
||||
analyticsEvents = [];
|
||||
|
||||
const useTestStore = create<TestState>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
name: '',
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
setName: (name: string) => set({ name }),
|
||||
throwError: () => {
|
||||
throw new Error('Test error');
|
||||
},
|
||||
asyncAction: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
return 'success';
|
||||
},
|
||||
}),
|
||||
{ storeName: 'TestStore', enableAnalytics: true },
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
await store.asyncAction();
|
||||
|
||||
// Wait a bit for async analytics to be tracked
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
const performanceEvents = analyticsEvents.filter(
|
||||
(e) => e.type === 'performance_metric' && e.action === 'asyncAction',
|
||||
);
|
||||
expect(performanceEvents.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should sanitize sensitive data', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
const useTestStore = create<{
|
||||
password: string;
|
||||
token: string;
|
||||
name: string;
|
||||
updateName: (name: string) => void;
|
||||
}>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
password: 'secret123',
|
||||
token: 'abc123',
|
||||
name: 'John',
|
||||
updateName: (name: string) => set({ name }),
|
||||
}),
|
||||
{
|
||||
storeName: 'TestStore',
|
||||
enableLogging: true,
|
||||
sanitizeState: (state) => {
|
||||
if (typeof state === 'object' && state !== null) {
|
||||
const sanitized = { ...(state as Record<string, unknown>) };
|
||||
sanitized.password = '[REDACTED]';
|
||||
sanitized.token = '[REDACTED]';
|
||||
return sanitized;
|
||||
}
|
||||
return state;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
store.updateName('Jane'); // Trigger state change
|
||||
|
||||
// Verify that logging was called (the middleware logs state changes)
|
||||
// The logger uses [DEBUG] prefix, so we check for that
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should respect shouldLog filter', () => {
|
||||
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
|
||||
|
||||
const useTestStore = create<{ count: number; increment: () => void }>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
}),
|
||||
{
|
||||
storeName: 'TestStore',
|
||||
enableLogging: true,
|
||||
shouldLog: () => false, // Don't log state changes
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
store.increment();
|
||||
|
||||
// Action calls are still logged, but state changes are filtered
|
||||
// So we expect at least one call (action call), but state change logging is filtered
|
||||
const calls = consoleSpy.mock.calls;
|
||||
const stateChangeCalls = calls.filter((call) =>
|
||||
call[0]?.toString().includes('State change'),
|
||||
);
|
||||
expect(stateChangeCalls.length).toBe(0);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should respect shouldTrack filter', () => {
|
||||
const useTestStore = create<{ count: number; increment: () => void }>()(
|
||||
stateMiddleware(
|
||||
(set) => ({
|
||||
count: 0,
|
||||
increment: () => set((state) => ({ count: state.count + 1 })),
|
||||
}),
|
||||
{
|
||||
storeName: 'TestStore',
|
||||
enableAnalytics: true,
|
||||
shouldTrack: (action) => action !== 'increment', // Don't track increment
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const store = useTestStore.getState();
|
||||
store.increment();
|
||||
|
||||
// Should not track increment action
|
||||
const actionEvents = analyticsEvents.filter(
|
||||
(e) => e.type === 'action_called' && e.action === 'increment',
|
||||
);
|
||||
expect(actionEvents.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,431 +0,0 @@
|
|||
/**
|
||||
* State Middleware
|
||||
* FE-STATE-010: Add middleware for logging, analytics, error handling
|
||||
*
|
||||
* Provides a comprehensive middleware for Zustand stores that handles:
|
||||
* - State change logging
|
||||
* - Analytics tracking
|
||||
* - Error handling and reporting
|
||||
*/
|
||||
|
||||
import { StateCreator } from 'zustand';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Analytics event types
|
||||
*/
|
||||
export type AnalyticsEventType =
|
||||
| 'state_change'
|
||||
| 'action_called'
|
||||
| 'error_occurred'
|
||||
| 'performance_metric';
|
||||
|
||||
/**
|
||||
* Analytics event
|
||||
*/
|
||||
export interface AnalyticsEvent {
|
||||
type: AnalyticsEventType;
|
||||
store: string;
|
||||
action?: string;
|
||||
timestamp: number;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analytics handler function
|
||||
*/
|
||||
export type AnalyticsHandler = (event: AnalyticsEvent) => void;
|
||||
|
||||
/**
|
||||
* Error handler function
|
||||
*/
|
||||
export type ErrorHandler = (
|
||||
error: Error,
|
||||
context: {
|
||||
store: string;
|
||||
action?: string;
|
||||
state?: unknown;
|
||||
},
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Options for state middleware
|
||||
*/
|
||||
export interface StateMiddlewareOptions {
|
||||
/** Store name for logging and analytics */
|
||||
storeName: string;
|
||||
/** Enable logging (default: true in dev, false in prod) */
|
||||
enableLogging?: boolean;
|
||||
/** Enable analytics tracking (default: true) */
|
||||
enableAnalytics?: boolean;
|
||||
/** Enable error handling (default: true) */
|
||||
enableErrorHandling?: boolean;
|
||||
/** Custom analytics handler */
|
||||
analyticsHandler?: AnalyticsHandler;
|
||||
/** Custom error handler */
|
||||
errorHandler?: ErrorHandler;
|
||||
/** Filter state changes to log (return false to skip) */
|
||||
shouldLog?: (state: unknown, prevState: unknown) => boolean;
|
||||
/** Filter actions to track (return false to skip) */
|
||||
shouldTrack?: (action: string, args: unknown[]) => boolean;
|
||||
/** Sanitize state for logging (remove sensitive data) */
|
||||
sanitizeState?: (state: unknown) => unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default analytics handler (can be overridden)
|
||||
*/
|
||||
let defaultAnalyticsHandler: AnalyticsHandler | null = null;
|
||||
|
||||
/**
|
||||
* Set default analytics handler
|
||||
*/
|
||||
export function setAnalyticsHandler(handler: AnalyticsHandler): void {
|
||||
defaultAnalyticsHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default error handler (can be overridden)
|
||||
*/
|
||||
let defaultErrorHandler: ErrorHandler | null = null;
|
||||
|
||||
/**
|
||||
* Set default error handler
|
||||
*/
|
||||
export function setErrorHandler(handler: ErrorHandler): void {
|
||||
defaultErrorHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track analytics event
|
||||
*/
|
||||
function trackAnalytics(
|
||||
event: Omit<AnalyticsEvent, 'timestamp'>,
|
||||
handler?: AnalyticsHandler,
|
||||
): void {
|
||||
const analyticsHandler = handler || defaultAnalyticsHandler;
|
||||
if (!analyticsHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
analyticsHandler({
|
||||
...event,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('[StateMiddleware] Analytics tracking failed', {
|
||||
error: String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error
|
||||
*/
|
||||
function handleError(
|
||||
error: Error,
|
||||
context: {
|
||||
store: string;
|
||||
action?: string;
|
||||
state?: unknown;
|
||||
},
|
||||
handler?: ErrorHandler,
|
||||
): void {
|
||||
const errorHandler = handler || defaultErrorHandler;
|
||||
|
||||
// Always log errors
|
||||
logger.error(`[StateMiddleware] Error in ${context.store}:`, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
action: context.action,
|
||||
context,
|
||||
});
|
||||
|
||||
// Call custom error handler if provided
|
||||
if (errorHandler) {
|
||||
try {
|
||||
errorHandler(error, context);
|
||||
} catch (handlerError) {
|
||||
logger.error('[StateMiddleware] Error handler failed', {
|
||||
error: String(handlerError),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Track error analytics (use default handler, not error handler)
|
||||
trackAnalytics({
|
||||
type: 'error_occurred',
|
||||
store: context.store,
|
||||
action: context.action,
|
||||
metadata: {
|
||||
error: error.message,
|
||||
errorName: error.name,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize state for logging (remove sensitive data)
|
||||
*/
|
||||
function sanitizeState(
|
||||
state: unknown,
|
||||
customSanitize?: (state: unknown) => unknown,
|
||||
): unknown {
|
||||
if (customSanitize) {
|
||||
return customSanitize(state);
|
||||
}
|
||||
|
||||
// Default sanitization: remove common sensitive fields
|
||||
if (typeof state === 'object' && state !== null) {
|
||||
const sanitized = { ...(state as Record<string, unknown>) };
|
||||
const sensitiveKeys = [
|
||||
'password',
|
||||
'token',
|
||||
'secret',
|
||||
'apiKey',
|
||||
'accessToken',
|
||||
'refreshToken',
|
||||
];
|
||||
|
||||
for (const key of sensitiveKeys) {
|
||||
if (key in sanitized) {
|
||||
sanitized[key] = '[REDACTED]';
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zustand middleware for logging, analytics, and error handling
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const useMyStore = create<MyState>()(
|
||||
* stateMiddleware(
|
||||
* (set, get) => ({
|
||||
* // store implementation
|
||||
* }),
|
||||
* { storeName: 'MyStore' }
|
||||
* )
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function stateMiddleware<T extends object>(
|
||||
config: StateCreator<T>,
|
||||
options: StateMiddlewareOptions,
|
||||
): StateCreator<T> {
|
||||
const {
|
||||
storeName,
|
||||
enableLogging = import.meta.env.DEV,
|
||||
enableAnalytics = true,
|
||||
enableErrorHandling = true,
|
||||
analyticsHandler,
|
||||
errorHandler,
|
||||
shouldLog = () => true,
|
||||
shouldTrack = () => true,
|
||||
sanitizeState: customSanitize,
|
||||
} = options;
|
||||
|
||||
return (set, get, api) => {
|
||||
let previousState: T | null = null;
|
||||
let actionCallCount = 0;
|
||||
|
||||
// Wrap set function to intercept state changes
|
||||
const wrappedSet: typeof set = (...args) => {
|
||||
const currentState = get();
|
||||
|
||||
// Log state changes
|
||||
if (enableLogging && shouldLog(currentState, previousState)) {
|
||||
const sanitizedCurrent = sanitizeState(currentState, customSanitize);
|
||||
const sanitizedPrevious = sanitizeState(previousState, customSanitize);
|
||||
|
||||
logger.debug(`[StateMiddleware:${storeName}] State change:`, {
|
||||
previous: sanitizedPrevious,
|
||||
current: sanitizedCurrent,
|
||||
});
|
||||
}
|
||||
|
||||
// Track analytics
|
||||
if (enableAnalytics) {
|
||||
trackAnalytics(
|
||||
{
|
||||
type: 'state_change',
|
||||
store: storeName,
|
||||
metadata: {
|
||||
hasPreviousState: previousState !== null,
|
||||
},
|
||||
},
|
||||
analyticsHandler,
|
||||
);
|
||||
}
|
||||
|
||||
// Call original set
|
||||
try {
|
||||
set(...args);
|
||||
previousState = get();
|
||||
} catch (error) {
|
||||
if (enableErrorHandling) {
|
||||
handleError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
{
|
||||
store: storeName,
|
||||
state: currentState,
|
||||
},
|
||||
errorHandler,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Create store with wrapped set
|
||||
const store = config(wrappedSet, get, api);
|
||||
|
||||
// Wrap store actions to track calls
|
||||
if (enableAnalytics || enableLogging) {
|
||||
const wrappedStore = { ...store } as Record<string, unknown>;
|
||||
|
||||
// Wrap all functions in the store
|
||||
for (const [key, value] of Object.entries(wrappedStore)) {
|
||||
if (typeof value === 'function' && key !== 'setState') {
|
||||
const originalAction = value as (...args: unknown[]) => unknown;
|
||||
|
||||
wrappedStore[key] = ((...args: unknown[]) => {
|
||||
actionCallCount++;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Track action call
|
||||
if (enableAnalytics && shouldTrack(key, args)) {
|
||||
trackAnalytics(
|
||||
{
|
||||
type: 'action_called',
|
||||
store: storeName,
|
||||
action: key,
|
||||
metadata: {
|
||||
callCount: actionCallCount,
|
||||
argsCount: args.length,
|
||||
},
|
||||
},
|
||||
analyticsHandler,
|
||||
);
|
||||
}
|
||||
|
||||
// Log action call
|
||||
if (enableLogging) {
|
||||
logger.debug(
|
||||
`[StateMiddleware:${storeName}] Action called: ${key}`,
|
||||
{
|
||||
args: args.length > 0 ? args : undefined,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = originalAction(...args);
|
||||
|
||||
// Track performance if it's a promise
|
||||
if (result instanceof Promise) {
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if (enableAnalytics) {
|
||||
trackAnalytics(
|
||||
{
|
||||
type: 'performance_metric',
|
||||
store: storeName,
|
||||
action: key,
|
||||
metadata: {
|
||||
duration,
|
||||
isAsync: true,
|
||||
},
|
||||
},
|
||||
analyticsHandler,
|
||||
);
|
||||
}
|
||||
|
||||
// Handle promise errors
|
||||
if (enableErrorHandling) {
|
||||
result.catch((error) => {
|
||||
handleError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
{
|
||||
store: storeName,
|
||||
action: key,
|
||||
state: get(),
|
||||
},
|
||||
errorHandler,
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Track synchronous action performance
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
if (enableAnalytics && duration > 10) {
|
||||
// Only track if it takes more than 10ms
|
||||
trackAnalytics(
|
||||
{
|
||||
type: 'performance_metric',
|
||||
store: storeName,
|
||||
action: key,
|
||||
metadata: {
|
||||
duration,
|
||||
isAsync: false,
|
||||
},
|
||||
},
|
||||
analyticsHandler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (enableErrorHandling) {
|
||||
handleError(
|
||||
error instanceof Error ? error : new Error(String(error)),
|
||||
{
|
||||
store: storeName,
|
||||
action: key,
|
||||
state: get(),
|
||||
},
|
||||
errorHandler,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}) as typeof originalAction;
|
||||
}
|
||||
}
|
||||
|
||||
return wrappedStore as T;
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a store with state middleware
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export const useMyStore = createWithMiddleware<MyState>(
|
||||
* (set, get) => ({
|
||||
* // store implementation
|
||||
* }),
|
||||
* { storeName: 'MyStore' }
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
export function createWithMiddleware<T extends object>(
|
||||
config: StateCreator<T>,
|
||||
options: StateMiddlewareOptions,
|
||||
): StateCreator<T> {
|
||||
return stateMiddleware(config, options);
|
||||
}
|
||||
Loading…
Reference in a new issue