@okyrychenko-dev/react-action-guard 0.6.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,161 +1,228 @@
1
+ import { ReactNode } from 'react';
1
2
  import * as zustand from 'zustand';
2
3
  import { StoreApi } from 'zustand';
3
- export { ShallowStoreBindings, createShallowStore } from '@okyrychenko-dev/react-zustand-toolkit';
4
- import { ReactNode } from 'react';
4
+
5
+ type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
6
+ type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
7
+ interface GoogleAnalyticsConfig {
8
+ provider: "ga";
9
+ }
10
+ interface MixpanelConfig {
11
+ provider: "mixpanel";
12
+ }
13
+ interface AmplitudeConfig {
14
+ provider: "amplitude";
15
+ }
16
+ interface CustomAnalyticsConfig {
17
+ track: (event: string, data: AnalyticsEventData) => void;
18
+ }
19
+ type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
5
20
 
6
21
  /**
7
- * Options for useAsyncAction hook
22
+ * Configuration options for creating or updating a blocker.
23
+ *
24
+ * All fields are optional. Defaults will be applied for any missing values:
25
+ * - `scope`: "global"
26
+ * - `reason`: "Blocking UI"
27
+ * - `priority`: 50
28
+ * - `timestamp`: Current time
29
+ *
30
+ * @example
31
+ * Basic configuration
32
+ * ```ts
33
+ * const config: BlockerConfig = {
34
+ * scope: 'form',
35
+ * reason: 'Submitting data...',
36
+ * priority: 70
37
+ * };
38
+ * ```
39
+ *
40
+ * @example
41
+ * With timeout and callback
42
+ * ```ts
43
+ * const config: BlockerConfig = {
44
+ * scope: ['form', 'navigation'],
45
+ * reason: 'Critical operation in progress',
46
+ * priority: 90,
47
+ * timeout: 30000, // 30 seconds
48
+ * onTimeout: (id) => {
49
+ * console.warn(`Blocker ${id} timed out`);
50
+ * }
51
+ * };
52
+ * ```
53
+ *
54
+ * @public
55
+ * @since 0.6.0
8
56
  */
9
- interface UseAsyncActionOptions {
10
- /** Timeout in milliseconds after which the blocker will be automatically removed */
57
+ interface BlockerConfig {
58
+ scope?: string | ReadonlyArray<string>;
59
+ reason?: string;
60
+ /** Priority level (negative values are normalized to 0) */
61
+ priority?: number;
62
+ timestamp?: number;
63
+ /** Automatically remove the blocker after N milliseconds */
11
64
  timeout?: number;
12
65
  /** Callback invoked when the blocker is automatically removed due to timeout */
13
66
  onTimeout?: (blockerId: string) => void;
14
67
  }
15
68
  /**
16
- * Wraps async functions with automatic UI blocking during execution.
17
- *
18
- * Creates a wrapper function that automatically:
19
- * 1. Adds a blocker before executing the async function
20
- * 2. Executes the async function
21
- * 3. Removes the blocker after completion (success or failure)
69
+ * Internal representation of a blocker with all fields populated.
22
70
  *
23
- * Each invocation generates a unique blocker ID (actionId-counter), allowing
24
- * multiple concurrent executions of the same action without ID conflicts.
25
- * The blocker is removed even if the async function throws an error.
71
+ * This is the stored format in the Map. All optional fields from {@link BlockerConfig}
72
+ * are converted to required fields with default values applied.
26
73
  *
27
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
74
+ * @internal This type is primarily for internal use. Users should work with
75
+ * {@link BlockerConfig} for configuration and {@link BlockerInfo} for
76
+ * reading blocker data.
28
77
  *
29
- * @template T - The return type of the async function being wrapped
30
- * @param actionId - Base identifier for the action. Used in blocker ID and default reason.
31
- * Example: "saveForm", "fetchData", "submitOrder"
32
- * @param scope - Scope(s) to block during execution. Can be a single scope or array.
33
- * Defaults to "global" if not specified.
34
- * @param options - Optional configuration
35
- * @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
36
- * @param options.onTimeout - Callback invoked when blocker times out before async completes
78
+ * @since 0.6.0
79
+ */
80
+ interface StoredBlocker {
81
+ scope: string | ReadonlyArray<string>;
82
+ reason: string;
83
+ priority: number;
84
+ timestamp: number;
85
+ /** Timeout duration in milliseconds (if set) */
86
+ timeout?: number;
87
+ /** Internal timeout ID for cleanup */
88
+ timeoutId?: ReturnType<typeof setTimeout>;
89
+ /** Callback invoked when the blocker is automatically removed due to timeout */
90
+ onTimeout?: (blockerId: string) => void;
91
+ }
92
+ /**
93
+ * Complete blocker information including its unique identifier.
37
94
  *
38
- * @returns Wrapper function that accepts an async function and returns a Promise with the same type
95
+ * This type extends {@link StoredBlocker} with the blocker's ID, making it
96
+ * suitable for reading and displaying blocker information. Returned by
97
+ * {@link UIBlockingStoreActions.getBlockingInfo} and used by {@link useBlockingInfo}.
39
98
  *
40
99
  * @example
41
- * Basic form submission
42
- * ```tsx
43
- * function MyForm() {
44
- * const executeWithBlocking = useAsyncAction('saveForm', 'form');
45
- *
46
- * const handleSubmit = async (e) => {
47
- * e.preventDefault();
100
+ * Accessing blocker info
101
+ * ```ts
102
+ * const blockers: BlockerInfo[] = store.getState().getBlockingInfo('form');
103
+ * blockers.forEach(blocker => {
104
+ * console.log(`${blocker.id}: ${blocker.reason} (priority: ${blocker.priority})`);
105
+ * });
106
+ * ```
48
107
  *
49
- * await executeWithBlocking(async () => {
50
- * const response = await fetch('/api/save', {
51
- * method: 'POST',
52
- * body: JSON.stringify(formData)
53
- * });
54
- * return response.json();
55
- * });
108
+ * @public
109
+ * @since 0.6.0
110
+ */
111
+ interface BlockerInfo extends Omit<StoredBlocker, "timeoutId"> {
112
+ id: string;
113
+ }
114
+ /**
115
+ * Internal state structure of the UI blocking store.
56
116
  *
57
- * alert('Form saved!');
58
- * };
117
+ * Contains the core data structures for managing blockers and middleware.
118
+ * This state is combined with {@link UIBlockingStoreActions} to create the
119
+ * complete {@link UIBlockingStore} type.
59
120
  *
60
- * return <form onSubmit={handleSubmit}>...</form>;
61
- * }
62
- * ```
121
+ * @internal
122
+ * @since 0.6.0
123
+ */
124
+ interface UIBlockingStoreState {
125
+ activeBlockers: Map<string, StoredBlocker>;
126
+ middlewares: Map<string, Middleware>;
127
+ }
128
+ /**
129
+ * All available actions for managing UI blocking state.
63
130
  *
64
- * @example
65
- * With timeout and error handling
66
- * ```tsx
67
- * const executeWithBlocking = useAsyncAction(
68
- * 'fetchData',
69
- * ['global', 'data'],
70
- * {
71
- * timeout: 30000, // 30 seconds
72
- * onTimeout: (blockerId) => {
73
- * console.warn(`Operation ${blockerId} timed out`);
74
- * }
75
- * }
76
- * );
131
+ * These methods allow adding, removing, updating blockers, checking blocking status,
132
+ * and managing middleware. Combined with {@link UIBlockingStoreState} to form
133
+ * the complete {@link UIBlockingStore} type.
77
134
  *
78
- * const loadData = async () => {
79
- * try {
80
- * const data = await executeWithBlocking(async () => {
81
- * return await fetchDataFromAPI();
82
- * });
83
- * setData(data);
84
- * } catch (error) {
85
- * console.error('Failed to load data:', error);
86
- * // Blocker is automatically removed even on error
87
- * }
88
- * };
89
- * ```
135
+ * All actions are safe to call multiple times and handle edge cases gracefully.
90
136
  *
91
137
  * @example
92
- * Multiple concurrent actions
93
- * ```tsx
94
- * function FileUploader() {
95
- * const uploadWithBlocking = useAsyncAction('upload', 'uploads');
138
+ * Using store actions directly
139
+ * ```ts
140
+ * import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
96
141
  *
97
- * const uploadFiles = async (files) => {
98
- * // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
99
- * const promises = files.map(file =>
100
- * uploadWithBlocking(() => uploadFile(file))
101
- * );
142
+ * // Add a blocker
143
+ * uiBlockingStoreApi.getState().addBlocker('my-blocker', {
144
+ * scope: 'form',
145
+ * reason: 'Processing...'
146
+ * });
102
147
  *
103
- * await Promise.all(promises);
104
- * };
148
+ * // Check if blocked
149
+ * const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
105
150
  *
106
- * return <...>;
107
- * }
151
+ * // Remove blocker
152
+ * uiBlockingStoreApi.getState().removeBlocker('my-blocker');
108
153
  * ```
109
154
  *
155
+ * @public
156
+ * @since 0.6.0
157
+ */
158
+ interface UIBlockingStoreActions {
159
+ addBlocker: (id: string, config?: BlockerConfig) => void;
160
+ updateBlocker: (id: string, config?: Partial<BlockerConfig>) => void;
161
+ removeBlocker: (id: string) => void;
162
+ isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
163
+ getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
164
+ clearAllBlockers: VoidFunction;
165
+ clearBlockersForScope: (scope: string) => void;
166
+ registerMiddleware: (name: string, middleware: Middleware) => void;
167
+ unregisterMiddleware: (name: string) => void;
168
+ runMiddlewares: (context: MiddlewareContext) => Promise<void>;
169
+ }
170
+ /**
171
+ * Complete UI blocking store type combining state and actions.
172
+ *
173
+ * This is the full store interface used by hooks like {@link useUIBlockingStore}
174
+ * and accessible via {@link uiBlockingStoreApi}. It combines all state properties
175
+ * from {@link UIBlockingStoreState} with all action methods from {@link UIBlockingStoreActions}.
176
+ *
110
177
  * @example
111
- * Type-safe return value
112
- * ```tsx
113
- * interface UserData {
114
- * id: string;
115
- * name: string;
116
- * }
178
+ * Using the complete store
179
+ * ```ts
180
+ * import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
117
181
  *
118
- * function UserProfile() {
119
- * const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
182
+ * function MyComponent() {
183
+ * const store = useUIBlockingStore();
120
184
  *
121
- * const loadUser = async (userId: string) => {
122
- * const userData = await executeWithBlocking(async () => {
123
- * const response = await fetch(`/api/users/${userId}`);
124
- * return response.json(); // TypeScript knows this is UserData
125
- * });
185
+ * // Access state
186
+ * console.log(store.activeBlockers.size);
126
187
  *
127
- * // userData is typed as UserData
128
- * console.log(userData.name);
129
- * };
188
+ * // Call actions
189
+ * store.addBlocker('my-blocker', { scope: 'form' });
130
190
  *
131
- * return <...>;
191
+ * return <div>...</div>;
132
192
  * }
133
193
  * ```
134
194
  *
135
- * @see {@link useBlocker} for manual blocker management
136
- * @see {@link useIsBlocked} to check blocking state
137
- * @see {@link UseAsyncActionOptions} for available options
138
- *
139
195
  * @public
140
196
  * @since 0.6.0
141
197
  */
142
- declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions) => ((asyncFn: () => Promise<T>) => Promise<T>);
143
-
144
- type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
145
- type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
146
- interface GoogleAnalyticsConfig {
147
- provider: "ga";
148
- }
149
- interface MixpanelConfig {
150
- provider: "mixpanel";
151
- }
152
- interface AmplitudeConfig {
153
- provider: "amplitude";
154
- }
155
- interface CustomAnalyticsConfig {
156
- track: (event: string, data: AnalyticsEventData) => void;
198
+ type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
199
+ interface ShallowStoreBindings<StoreState> {
200
+ useStore: {
201
+ (): StoreState;
202
+ <T>(selector: (state: StoreState) => T): T;
203
+ };
204
+ useStoreApi: StoreApi<StoreState>;
157
205
  }
158
- type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
206
+
207
+ declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
208
+ setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
209
+ [x: string]: unknown;
210
+ [x: number]: unknown;
211
+ [x: symbol]: unknown;
212
+ type: string;
213
+ }) | undefined): void;
214
+ setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
215
+ [x: string]: unknown;
216
+ [x: number]: unknown;
217
+ [x: symbol]: unknown;
218
+ type: string;
219
+ }) | undefined): void;
220
+ devtools: {
221
+ cleanup: () => void;
222
+ };
223
+ };
224
+ declare function useUIBlockingStore(): UIBlockingStore;
225
+ declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
159
226
 
160
227
  type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
161
228
  type MiddlewareBlockerConfig = BlockerConfig;
@@ -243,7 +310,7 @@ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
243
310
  * @public
244
311
  * @since 0.6.0
245
312
  */
246
- declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
313
+ declare function createAnalyticsMiddleware(config: AnalyticsConfig): Middleware;
247
314
 
248
315
  /**
249
316
  * Configures and registers multiple middleware functions for the UI blocking store.
@@ -359,7 +426,7 @@ declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): vo
359
426
  * @public
360
427
  * @since 0.6.0
361
428
  */
362
- declare const loggerMiddleware: Middleware;
429
+ declare function loggerMiddleware(context: MiddlewareContext): void;
363
430
 
364
431
  interface PerformanceConfig {
365
432
  slowBlockThreshold?: number;
@@ -369,12 +436,12 @@ interface PerformanceConfig {
369
436
  /**
370
437
  * Creates middleware for monitoring blocker performance and detecting slow blocks.
371
438
  *
372
- * Tracks the duration of each blocker (from add to remove) and warns when a blocker
373
- * exceeds the configured threshold. Useful for identifying performance issues,
374
- * stuck blockers, or operations that take too long.
439
+ * Tracks the duration of each blocker (from add to remove) and invokes an optional
440
+ * callback when a blocker exceeds the configured threshold. Useful for identifying
441
+ * performance issues, stuck blockers, or operations that take too long.
375
442
  *
376
- * Automatically logs warnings to console for slow blocks and optionally invokes
377
- * a callback for custom handling (e.g., error reporting, analytics).
443
+ * The middleware is side-effect free by default; use `onSlowBlock` for custom
444
+ * reporting, logging, or analytics integration.
378
445
  *
379
446
  * @param config - Performance monitoring configuration
380
447
  * @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
@@ -392,7 +459,7 @@ interface PerformanceConfig {
392
459
  * const performanceMiddleware = createPerformanceMiddleware();
393
460
  *
394
461
  * configureMiddleware([performanceMiddleware]);
395
- * // Warns if any blocker lasts > 3 seconds
462
+ * // Track slow blocks via onSlowBlock if any blocker lasts > 3 seconds
396
463
  * ```
397
464
  *
398
465
  * @example
@@ -451,205 +518,293 @@ interface PerformanceConfig {
451
518
  * @public
452
519
  * @since 0.6.0
453
520
  */
454
- declare const createPerformanceMiddleware: (config?: PerformanceConfig) => Middleware;
521
+ declare function createPerformanceMiddleware(config?: PerformanceConfig): Middleware;
455
522
 
456
523
  /**
457
- * Configuration options for creating or updating a blocker.
524
+ * Props for the {@link UIBlockingProvider} component.
458
525
  *
459
- * All fields are optional. Defaults will be applied for any missing values:
460
- * - `scope`: "global"
461
- * - `reason`: "Blocking UI"
462
- * - `priority`: 50
463
- * - `timestamp`: Current time
526
+ * Configures an isolated UI blocking store instance for a subtree of your app.
527
+ * Useful for SSR, testing, micro-frontends, or any scenario where you need
528
+ * independent blocking state.
529
+ *
530
+ * @public
531
+ * @since 0.6.0
532
+ */
533
+ interface UIBlockingProviderProps {
534
+ children: ReactNode;
535
+ /** Enable Redux DevTools integration (default: true in development) */
536
+ enableDevtools?: boolean;
537
+ /** Name for Redux DevTools (default: "UIBlocking") */
538
+ devtoolsName?: string;
539
+ /** Initial middlewares to register */
540
+ middlewares?: ReadonlyArray<Middleware>;
541
+ }
542
+ /**
543
+ * Base provider implementation created using toolkit's createStoreProvider.
544
+ * @internal
545
+ */
546
+ declare const useUIBlockingContext: () => StoreApi<UIBlockingStore>;
547
+ declare const useIsInsideUIBlockingProvider: () => boolean;
548
+ declare const useOptionalUIBlockingContext: () => StoreApi<UIBlockingStore> | null;
549
+ declare function useUIBlockingStoreFromContext(): UIBlockingStore;
550
+ declare function useUIBlockingStoreFromContext<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
551
+ /**
552
+ * Provider component for isolated UI blocking state management.
553
+ *
554
+ * Creates an independent store instance for a subtree of your application.
555
+ * Each provider has its own isolated blocking state, separate from the global
556
+ * store and other provider instances.
557
+ *
558
+ * **Use cases:**
559
+ * - Server-Side Rendering (SSR) - Avoid state sharing between requests
560
+ * - Testing - Isolated state for each test
561
+ * - Micro-frontends - Independent blocking state per micro-app
562
+ * - Modals/Dialogs - Scoped blocking within a dialog
563
+ *
564
+ * Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
565
+ * built-in DevTools support and middleware registration.
566
+ *
567
+ * @param props - Provider configuration
568
+ * @param props.children - React children to wrap with the provider
569
+ * @param props.enableDevtools - Enable Redux DevTools integration. Defaults to true in development, false in production.
570
+ * @param props.devtoolsName - Name displayed in Redux DevTools. Defaults to "UIBlocking".
571
+ * @param props.middlewares - Array of middleware functions to register on store creation
464
572
  *
465
573
  * @example
466
- * Basic configuration
467
- * ```ts
468
- * const config: BlockerConfig = {
469
- * scope: 'form',
470
- * reason: 'Submitting data...',
471
- * priority: 70
472
- * };
574
+ * Basic SSR setup
575
+ * ```tsx
576
+ * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
577
+ *
578
+ * function App() {
579
+ * return (
580
+ * <UIBlockingProvider>
581
+ * <MyApplication />
582
+ * </UIBlockingProvider>
583
+ * );
584
+ * }
473
585
  * ```
474
586
  *
475
587
  * @example
476
- * With timeout and callback
477
- * ```ts
478
- * const config: BlockerConfig = {
479
- * scope: ['form', 'navigation'],
480
- * reason: 'Critical operation in progress',
481
- * priority: 90,
482
- * timeout: 30000, // 30 seconds
483
- * onTimeout: (id) => {
484
- * console.warn(`Blocker ${id} timed out`);
485
- * }
486
- * };
588
+ * With middleware and custom DevTools name
589
+ * ```tsx
590
+ * import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
591
+ *
592
+ * function App() {
593
+ * return (
594
+ * <UIBlockingProvider
595
+ * enableDevtools={true}
596
+ * devtoolsName="MyApp-UIBlocking"
597
+ * middlewares={[loggerMiddleware]}
598
+ * >
599
+ * <MyApplication />
600
+ * </UIBlockingProvider>
601
+ * );
602
+ * }
603
+ * ```
604
+ *
605
+ * @example
606
+ * Multiple providers for micro-frontends
607
+ * ```tsx
608
+ * function MicroFrontendApp() {
609
+ * return (
610
+ * <div>
611
+ * <UIBlockingProvider devtoolsName="MicroApp1">
612
+ * <MicroApp1 />
613
+ * </UIBlockingProvider>
614
+ *
615
+ * <UIBlockingProvider devtoolsName="MicroApp2">
616
+ * <MicroApp2 />
617
+ * </UIBlockingProvider>
618
+ * </div>
619
+ * );
620
+ * }
621
+ * ```
622
+ *
623
+ * @example
624
+ * Testing with isolated state
625
+ * ```tsx
626
+ * // In your test file
627
+ * import { render } from '@testing-library/react';
628
+ * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
629
+ *
630
+ * test('blocker behavior', () => {
631
+ * render(
632
+ * <UIBlockingProvider enableDevtools={false}>
633
+ * <ComponentUnderTest />
634
+ * </UIBlockingProvider>
635
+ * );
636
+ * // Each test gets isolated state
637
+ * });
487
638
  * ```
488
639
  *
640
+ * @see {@link useUIBlockingContext} to access the store from context
641
+ * @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
642
+ * @see {@link UIBlockingProviderProps} for prop details
643
+ *
489
644
  * @public
490
645
  * @since 0.6.0
491
646
  */
492
- interface BlockerConfig {
493
- scope?: string | ReadonlyArray<string>;
494
- reason?: string;
495
- /** Priority level (negative values are normalized to 0) */
496
- priority?: number;
497
- timestamp?: number;
498
- /** Automatically remove the blocker after N milliseconds */
499
- timeout?: number;
500
- /** Callback invoked when the blocker is automatically removed due to timeout */
501
- onTimeout?: (blockerId: string) => void;
502
- }
647
+ declare function UIBlockingProvider({ children, enableDevtools, devtoolsName, middlewares, }: UIBlockingProviderProps): ReactNode;
648
+
503
649
  /**
504
- * Internal representation of a blocker with all fields populated.
650
+ * Hook that resolves to either the context store or global store
505
651
  *
506
- * This is the stored format in the Map. All optional fields from {@link BlockerConfig}
507
- * are converted to required fields with default values applied.
652
+ * This hook automatically uses the store from UIBlockingProvider if available,
653
+ * otherwise falls back to the global store. This enables both patterns:
508
654
  *
509
- * @internal This type is primarily for internal use. Users should work with
510
- * {@link BlockerConfig} for configuration and {@link BlockerInfo} for
511
- * reading blocker data.
655
+ * 1. Global store (default behavior, no Provider needed)
656
+ * 2. Context store (for SSR, testing, micro-frontends)
512
657
  *
513
- * @since 0.6.0
658
+ * @returns The resolved store API
514
659
  */
515
- interface StoredBlocker {
516
- scope: string | ReadonlyArray<string>;
517
- reason: string;
518
- priority: number;
519
- timestamp: number;
520
- /** Timeout duration in milliseconds (if set) */
660
+ declare function useResolvedStoreApi(): StoreApi<UIBlockingStore>;
661
+ /**
662
+ * Hook to use the resolved store with a selector
663
+ *
664
+ * Automatically applies shallow comparison to prevent unnecessary re-renders.
665
+ *
666
+ * @param selector - Selector function to pick state from the store
667
+ * @returns Selected state value
668
+ */
669
+ declare function useResolvedValue<T>(selector: (state: UIBlockingStore) => T): T;
670
+
671
+ /**
672
+ * Options for useAsyncAction hook
673
+ */
674
+ interface UseAsyncActionOptions {
675
+ /** Timeout in milliseconds after which the blocker will be automatically removed */
521
676
  timeout?: number;
522
- /** Internal timeout ID for cleanup */
523
- timeoutId?: ReturnType<typeof setTimeout>;
524
677
  /** Callback invoked when the blocker is automatically removed due to timeout */
525
678
  onTimeout?: (blockerId: string) => void;
526
679
  }
527
680
  /**
528
- * Complete blocker information including its unique identifier.
681
+ * Wraps async functions with automatic UI blocking during execution.
529
682
  *
530
- * This type extends {@link StoredBlocker} with the blocker's ID, making it
531
- * suitable for reading and displaying blocker information. Returned by
532
- * {@link UIBlockingStoreActions.getBlockingInfo} and used by {@link useBlockingInfo}.
683
+ * Creates a wrapper function that automatically:
684
+ * 1. Adds a blocker before executing the async function
685
+ * 2. Executes the async function
686
+ * 3. Removes the blocker after completion (success or failure)
533
687
  *
534
- * @example
535
- * Accessing blocker info
536
- * ```ts
537
- * const blockers: BlockerInfo[] = store.getState().getBlockingInfo('form');
538
- * blockers.forEach(blocker => {
539
- * console.log(`${blocker.id}: ${blocker.reason} (priority: ${blocker.priority})`);
540
- * });
541
- * ```
688
+ * Each invocation generates a unique blocker ID (actionId-counter), allowing
689
+ * multiple concurrent executions of the same action without ID conflicts.
690
+ * The blocker is removed even if the async function throws an error.
542
691
  *
543
- * @public
544
- * @since 0.6.0
545
- */
546
- interface BlockerInfo extends StoredBlocker {
547
- id: string;
548
- }
549
- /**
550
- * Internal state structure of the UI blocking store.
692
+ * Works with both the global store and isolated provider instances (via UIBlockingProvider).
551
693
  *
552
- * Contains the core data structures for managing blockers and middleware.
553
- * This state is combined with {@link UIBlockingStoreActions} to create the
554
- * complete {@link UIBlockingStore} type.
694
+ * @template T - The return type of the async function being wrapped
695
+ * @param actionId - Base identifier for the action. Used in blocker ID and default reason.
696
+ * Example: "saveForm", "fetchData", "submitOrder"
697
+ * @param scope - Scope(s) to block during execution. Can be a single scope or array.
698
+ * Defaults to "global" if not specified.
699
+ * @param options - Optional configuration
700
+ * @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
701
+ * @param options.onTimeout - Callback invoked when blocker times out before async completes
702
+ *
703
+ * @returns Wrapper function that accepts an async function and returns a Promise with the same type
704
+ *
705
+ * @example
706
+ * Basic form submission
707
+ * ```tsx
708
+ * function MyForm() {
709
+ * const executeWithBlocking = useAsyncAction('saveForm', 'form');
710
+ *
711
+ * const handleSubmit = async (e) => {
712
+ * e.preventDefault();
555
713
  *
556
- * @internal
557
- * @since 0.6.0
558
- */
559
- interface UIBlockingStoreState {
560
- activeBlockers: Map<string, StoredBlocker>;
561
- middlewares: Map<string, Middleware>;
562
- }
563
- /**
564
- * All available actions for managing UI blocking state.
714
+ * await executeWithBlocking(async () => {
715
+ * const response = await fetch('/api/save', {
716
+ * method: 'POST',
717
+ * body: JSON.stringify(formData)
718
+ * });
719
+ * return response.json();
720
+ * });
565
721
  *
566
- * These methods allow adding, removing, updating blockers, checking blocking status,
567
- * and managing middleware. Combined with {@link UIBlockingStoreState} to form
568
- * the complete {@link UIBlockingStore} type.
722
+ * alert('Form saved!');
723
+ * };
569
724
  *
570
- * All actions are safe to call multiple times and handle edge cases gracefully.
725
+ * return <form onSubmit={handleSubmit}>...</form>;
726
+ * }
727
+ * ```
571
728
  *
572
729
  * @example
573
- * Using store actions directly
574
- * ```ts
575
- * import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
730
+ * With timeout and error handling
731
+ * ```tsx
732
+ * const executeWithBlocking = useAsyncAction(
733
+ * 'fetchData',
734
+ * ['global', 'data'],
735
+ * {
736
+ * timeout: 30000, // 30 seconds
737
+ * onTimeout: (blockerId) => {
738
+ * console.warn(`Operation ${blockerId} timed out`);
739
+ * }
740
+ * }
741
+ * );
576
742
  *
577
- * // Add a blocker
578
- * uiBlockingStoreApi.getState().addBlocker('my-blocker', {
579
- * scope: 'form',
580
- * reason: 'Processing...'
581
- * });
743
+ * const loadData = async () => {
744
+ * try {
745
+ * const data = await executeWithBlocking(async () => {
746
+ * return await fetchDataFromAPI();
747
+ * });
748
+ * setData(data);
749
+ * } catch (error) {
750
+ * console.error('Failed to load data:', error);
751
+ * // Blocker is automatically removed even on error
752
+ * }
753
+ * };
754
+ * ```
582
755
  *
583
- * // Check if blocked
584
- * const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
756
+ * @example
757
+ * Multiple concurrent actions
758
+ * ```tsx
759
+ * function FileUploader() {
760
+ * const uploadWithBlocking = useAsyncAction('upload', 'uploads');
585
761
  *
586
- * // Remove blocker
587
- * uiBlockingStoreApi.getState().removeBlocker('my-blocker');
588
- * ```
762
+ * const uploadFiles = async (files) => {
763
+ * // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
764
+ * const promises = files.map(file =>
765
+ * uploadWithBlocking(() => uploadFile(file))
766
+ * );
589
767
  *
590
- * @public
591
- * @since 0.6.0
592
- */
593
- interface UIBlockingStoreActions {
594
- addBlocker: (id: string, config?: BlockerConfig) => void;
595
- updateBlocker: (id: string, config?: Partial<BlockerConfig>) => void;
596
- removeBlocker: (id: string) => void;
597
- isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
598
- getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
599
- clearAllBlockers: VoidFunction;
600
- clearBlockersForScope: (scope: string) => void;
601
- registerMiddleware: (name: string, middleware: Middleware) => void;
602
- unregisterMiddleware: (name: string) => void;
603
- runMiddlewares: (context: MiddlewareContext) => Promise<void>;
604
- }
605
- /**
606
- * Complete UI blocking store type combining state and actions.
768
+ * await Promise.all(promises);
769
+ * };
607
770
  *
608
- * This is the full store interface used by hooks like {@link useUIBlockingStore}
609
- * and accessible via {@link uiBlockingStoreApi}. It combines all state properties
610
- * from {@link UIBlockingStoreState} with all action methods from {@link UIBlockingStoreActions}.
771
+ * return <...>;
772
+ * }
773
+ * ```
611
774
  *
612
775
  * @example
613
- * Using the complete store
614
- * ```ts
615
- * import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
776
+ * Type-safe return value
777
+ * ```tsx
778
+ * interface UserData {
779
+ * id: string;
780
+ * name: string;
781
+ * }
616
782
  *
617
- * function MyComponent() {
618
- * const store = useUIBlockingStore();
783
+ * function UserProfile() {
784
+ * const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
619
785
  *
620
- * // Access state
621
- * console.log(store.activeBlockers.size);
786
+ * const loadUser = async (userId: string) => {
787
+ * const userData = await executeWithBlocking(async () => {
788
+ * const response = await fetch(`/api/users/${userId}`);
789
+ * return response.json(); // TypeScript knows this is UserData
790
+ * });
622
791
  *
623
- * // Call actions
624
- * store.addBlocker('my-blocker', { scope: 'form' });
792
+ * // userData is typed as UserData
793
+ * console.log(userData.name);
794
+ * };
625
795
  *
626
- * return <div>...</div>;
796
+ * return <...>;
627
797
  * }
628
798
  * ```
629
799
  *
800
+ * @see {@link useBlocker} for manual blocker management
801
+ * @see {@link useIsBlocked} to check blocking state
802
+ * @see {@link UseAsyncActionOptions} for available options
803
+ *
630
804
  * @public
631
805
  * @since 0.6.0
632
806
  */
633
- type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
634
-
635
- /**
636
- * UI Blocking Store
637
- *
638
- * Global Zustand store for managing UI blocking state across the application.
639
- *
640
- * Features:
641
- * - Priority-based blocking system
642
- * - Scope-based blocking (global, specific areas, or multiple scopes)
643
- * - Automatic cleanup on unmount (when using hooks)
644
- * - DevTools integration for debugging
645
- * - Automatic shallow comparison for selectors
646
- *
647
- */
648
- declare const useUIBlockingStore: {
649
- (): UIBlockingStore;
650
- <T>(selector: (state: UIBlockingStore) => T): T;
651
- };
652
- declare const uiBlockingStoreApi: zustand.StoreApi<UIBlockingStore>;
807
+ declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
653
808
 
654
809
  /**
655
810
  * Automatically manages a UI blocker based on component lifecycle.
@@ -727,7 +882,7 @@ declare const uiBlockingStoreApi: zustand.StoreApi<UIBlockingStore>;
727
882
  * @public
728
883
  * @since 0.6.0
729
884
  */
730
- declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
885
+ declare function useBlocker(blockerId: string, config: BlockerConfig, isActive?: boolean): void;
731
886
 
732
887
  /**
733
888
  * Gets detailed information about all active blockers for a specific scope.
@@ -804,7 +959,7 @@ declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?:
804
959
  * @public
805
960
  * @since 0.6.0
806
961
  */
807
- declare const useBlockingInfo: (scope?: string) => ReadonlyArray<BlockerInfo>;
962
+ declare function useBlockingInfo(scope?: string): ReadonlyArray<BlockerInfo>;
808
963
 
809
964
  /**
810
965
  * Configuration for conditional blocker hook.
@@ -948,16 +1103,10 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
948
1103
  * Without state parameter (simple condition)
949
1104
  * ```tsx
950
1105
  * useConditionalBlocker('low-battery', {
951
- * condition: () => {
952
- * // Check battery level if available
953
- * if ('getBattery' in navigator) {
954
- * return navigator.getBattery().then(b => b.level < 0.05);
955
- * }
956
- * return false;
957
- * },
1106
+ * condition: () => !navigator.onLine,
958
1107
  * checkInterval: 30000, // Check every 30 seconds
959
1108
  * scope: 'heavy-operations',
960
- * reason: 'Battery level too low for this operation',
1109
+ * reason: 'Internet connection is required for this operation',
961
1110
  * priority: 80
962
1111
  * });
963
1112
  * ```
@@ -969,7 +1118,7 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
969
1118
  * @public
970
1119
  * @since 0.6.0
971
1120
  */
972
- declare const useConditionalBlocker: <TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>) => void;
1121
+ declare function useConditionalBlocker<TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>): void;
973
1122
 
974
1123
  /**
975
1124
  * Configuration for the confirmation dialog
@@ -1145,7 +1294,7 @@ interface UseConfirmableBlockerReturn {
1145
1294
  * @public
1146
1295
  * @since 0.6.0
1147
1296
  */
1148
- declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
1297
+ declare function useConfirmableBlocker(blockerId: string, config: ConfirmableBlockerConfig): UseConfirmableBlockerReturn;
1149
1298
 
1150
1299
  /**
1151
1300
  * Checks if one or more scopes are currently blocked.
@@ -1219,7 +1368,7 @@ declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBloc
1219
1368
  * @public
1220
1369
  * @since 0.6.0
1221
1370
  */
1222
- declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
1371
+ declare function useIsBlocked(scope?: string | ReadonlyArray<string>): boolean;
1223
1372
 
1224
1373
  /**
1225
1374
  * Schedule configuration for time-based blocking.
@@ -1371,7 +1520,7 @@ interface ScheduledBlockerConfig extends BlockerConfig {
1371
1520
  * @public
1372
1521
  * @since 0.6.0
1373
1522
  */
1374
- declare const useScheduledBlocker: (blockerId: string, config: ScheduledBlockerConfig) => void;
1523
+ declare function useScheduledBlocker(blockerId: string, config: ScheduledBlockerConfig): void;
1375
1524
 
1376
1525
  /**
1377
1526
  * Default scope type
@@ -1442,130 +1591,16 @@ interface TypedHooks<TScope extends string> {
1442
1591
  declare function createTypedHooks<TScope extends string>(): TypedHooks<TScope>;
1443
1592
 
1444
1593
  /**
1445
- * Props for the {@link UIBlockingProvider} component.
1446
- *
1447
- * Configures an isolated UI blocking store instance for a subtree of your app.
1448
- * Useful for SSR, testing, micro-frontends, or any scenario where you need
1449
- * independent blocking state.
1450
- *
1451
- * @public
1452
- * @since 0.6.0
1594
+ * @deprecated Use `useOptionalUIBlockingContext`.
1453
1595
  */
1454
- interface UIBlockingProviderProps {
1455
- children: ReactNode;
1456
- /** Enable Redux DevTools integration (default: true in development) */
1457
- enableDevtools?: boolean;
1458
- /** Name for Redux DevTools (default: "UIBlocking") */
1459
- devtoolsName?: string;
1460
- /** Initial middlewares to register */
1461
- middlewares?: ReadonlyArray<Middleware>;
1462
- }
1596
+ declare function useOptionalContext(): ReturnType<typeof useOptionalUIBlockingContext>;
1463
1597
  /**
1464
- * Base provider implementation created using toolkit's createStoreProvider.
1465
- * @internal
1598
+ * @deprecated Use `useResolvedStoreApi`.
1466
1599
  */
1467
- declare const useUIBlockingContext: () => StoreApi<UIBlockingStore>;
1468
- declare const useUIBlockingStoreFromContext: {
1469
- (): UIBlockingStore;
1470
- <T>(selector: (state: UIBlockingStore) => T): T;
1471
- };
1472
- declare const useIsInsideUIBlockingProvider: () => boolean;
1600
+ declare function useResolvedStore(): StoreApi<UIBlockingStore>;
1473
1601
  /**
1474
- * Provider component for isolated UI blocking state management.
1475
- *
1476
- * Creates an independent store instance for a subtree of your application.
1477
- * Each provider has its own isolated blocking state, separate from the global
1478
- * store and other provider instances.
1479
- *
1480
- * **Use cases:**
1481
- * - Server-Side Rendering (SSR) - Avoid state sharing between requests
1482
- * - Testing - Isolated state for each test
1483
- * - Micro-frontends - Independent blocking state per micro-app
1484
- * - Modals/Dialogs - Scoped blocking within a dialog
1485
- *
1486
- * Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
1487
- * built-in DevTools support and middleware registration.
1488
- *
1489
- * @param props - Provider configuration
1490
- * @param props.children - React children to wrap with the provider
1491
- * @param props.enableDevtools - Enable Redux DevTools integration. Defaults to true in development, false in production.
1492
- * @param props.devtoolsName - Name displayed in Redux DevTools. Defaults to "UIBlocking".
1493
- * @param props.middlewares - Array of middleware functions to register on store creation
1494
- *
1495
- * @example
1496
- * Basic SSR setup
1497
- * ```tsx
1498
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
1499
- *
1500
- * function App() {
1501
- * return (
1502
- * <UIBlockingProvider>
1503
- * <MyApplication />
1504
- * </UIBlockingProvider>
1505
- * );
1506
- * }
1507
- * ```
1508
- *
1509
- * @example
1510
- * With middleware and custom DevTools name
1511
- * ```tsx
1512
- * import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
1513
- *
1514
- * function App() {
1515
- * return (
1516
- * <UIBlockingProvider
1517
- * enableDevtools={true}
1518
- * devtoolsName="MyApp-UIBlocking"
1519
- * middlewares={[loggerMiddleware]}
1520
- * >
1521
- * <MyApplication />
1522
- * </UIBlockingProvider>
1523
- * );
1524
- * }
1525
- * ```
1526
- *
1527
- * @example
1528
- * Multiple providers for micro-frontends
1529
- * ```tsx
1530
- * function MicroFrontendApp() {
1531
- * return (
1532
- * <div>
1533
- * <UIBlockingProvider devtoolsName="MicroApp1">
1534
- * <MicroApp1 />
1535
- * </UIBlockingProvider>
1536
- *
1537
- * <UIBlockingProvider devtoolsName="MicroApp2">
1538
- * <MicroApp2 />
1539
- * </UIBlockingProvider>
1540
- * </div>
1541
- * );
1542
- * }
1543
- * ```
1544
- *
1545
- * @example
1546
- * Testing with isolated state
1547
- * ```tsx
1548
- * // In your test file
1549
- * import { render } from '@testing-library/react';
1550
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
1551
- *
1552
- * test('blocker behavior', () => {
1553
- * render(
1554
- * <UIBlockingProvider enableDevtools={false}>
1555
- * <ComponentUnderTest />
1556
- * </UIBlockingProvider>
1557
- * );
1558
- * // Each test gets isolated state
1559
- * });
1560
- * ```
1561
- *
1562
- * @see {@link useUIBlockingContext} to access the store from context
1563
- * @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
1564
- * @see {@link UIBlockingProviderProps} for prop details
1565
- *
1566
- * @public
1567
- * @since 0.6.0
1602
+ * @deprecated Use `useResolvedValue`.
1568
1603
  */
1569
- declare function UIBlockingProvider({ children, enableDevtools, devtoolsName, middlewares, }: UIBlockingProviderProps): ReactNode;
1604
+ declare function useResolvedStoreWithSelector<T>(selector: (state: UIBlockingStore) => T): T;
1570
1605
 
1571
- export { type AmplitudeConfig, type AnalyticsConfig, type AnalyticsEventData, type AnalyticsProvider, type BlockerConfig, type BlockerConfigTyped, type BlockerInfo, type BlockingAction, type BlockingSchedule, type ConditionalBlockerConfig, type ConfirmDialogConfig, type ConfirmableBlockerConfig, type CustomAnalyticsConfig, type DefaultScopes, type GoogleAnalyticsConfig, type Middleware, type MiddlewareContext, type MixpanelConfig, type PerformanceConfig, type ScheduledBlockerConfig, type ScopeValue, type StoredBlocker, type TypedHooks, UIBlockingProvider, type UIBlockingProviderProps, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, type UseAsyncActionOptions, type UseConfirmableBlockerReturn, configureMiddleware, createAnalyticsMiddleware, createPerformanceMiddleware, createTypedHooks, loggerMiddleware, uiBlockingStoreApi, useAsyncAction, useBlocker, useBlockingInfo, useConditionalBlocker, useConfirmableBlocker, useIsBlocked, useIsInsideUIBlockingProvider, useScheduledBlocker, useUIBlockingContext, useUIBlockingStore, useUIBlockingStoreFromContext };
1606
+ export { type AmplitudeConfig, type AnalyticsConfig, type AnalyticsEventData, type AnalyticsProvider, type BlockerConfig, type BlockerConfigTyped, type BlockerInfo, type BlockingAction, type BlockingSchedule, type ConditionalBlockerConfig, type ConfirmDialogConfig, type ConfirmableBlockerConfig, type CustomAnalyticsConfig, type DefaultScopes, type GoogleAnalyticsConfig, type Middleware, type MiddlewareContext, type MixpanelConfig, type PerformanceConfig, type ScheduledBlockerConfig, type ScopeValue, type ShallowStoreBindings, type StoredBlocker, type TypedHooks, UIBlockingProvider, type UIBlockingProviderProps, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, type UseAsyncActionOptions, type UseConfirmableBlockerReturn, configureMiddleware, createAnalyticsMiddleware, createPerformanceMiddleware, createTypedHooks, loggerMiddleware, uiBlockingStoreApi, useAsyncAction, useBlocker, useBlockingInfo, useConditionalBlocker, useConfirmableBlocker, useIsBlocked, useIsInsideUIBlockingProvider, useOptionalContext, useOptionalUIBlockingContext, useResolvedStore, useResolvedStoreApi, useResolvedStoreWithSelector, useResolvedValue, useScheduledBlocker, useUIBlockingContext, useUIBlockingStore, useUIBlockingStoreFromContext };