@okyrychenko-dev/react-action-guard 1.0.4 → 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/dist/index.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { ReactNode } from 'react';
2
2
  import * as zustand from 'zustand';
3
3
  import { StoreApi } from 'zustand';
4
+ import { Optional } from '@okyrychenko-dev/type-utils';
4
5
 
5
- type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
6
+ type AnalyticsEventData = Record<string, Optional<string | number | ReadonlyArray<string>>>;
6
7
  type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
7
8
  interface GoogleAnalyticsConfig {
8
9
  provider: "ga";
@@ -18,39 +19,32 @@ interface CustomAnalyticsConfig {
18
19
  }
19
20
  type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
20
21
 
22
+ /** An immutable point-in-time blocker projection. */
23
+ type BlockingLifecycleSnapshot = ReadonlyArray<Readonly<BlockerInfo>>;
24
+ /** Read snapshots and observe lifecycle changes without transition authority. */
25
+ interface BlockingLifecycleObservation {
26
+ getSnapshot: () => BlockingLifecycleSnapshot;
27
+ subscribe: (listener: (snapshot: BlockingLifecycleSnapshot) => void) => VoidFunction;
28
+ observe: (observer: Middleware) => VoidFunction;
29
+ }
30
+ interface BlockingLifecycle extends BlockingLifecycleObservation {
31
+ add: (id: string, config?: BlockerConfig) => void;
32
+ update: (id: string, config?: Partial<BlockerConfig>) => void;
33
+ replace: (id: string, config: BlockerConfig) => void;
34
+ remove: (id: string) => void;
35
+ clear: () => void;
36
+ clearScope: (scope: string) => void;
37
+ isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
38
+ getBlockingInfo: (scope: string) => BlockingLifecycleSnapshot;
39
+ }
40
+ type BlockingEvent = MiddlewareContext;
41
+
42
+ /** Owns one independent store's blockers, timers, snapshots, and transition events. */
43
+ declare function createBlockingLifecycle(): BlockingLifecycle;
44
+
21
45
  /**
22
46
  * Configuration options for creating or updating a blocker.
23
47
  *
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
48
  * @public
55
49
  * @since 0.6.0
56
50
  */
@@ -60,173 +54,65 @@ interface BlockerConfig {
60
54
  /** Priority level (negative values are normalized to 0) */
61
55
  priority?: number;
62
56
  timestamp?: number;
63
- /** Automatically remove the blocker after N milliseconds */
64
- timeout?: number;
65
- /** Callback invoked when the blocker is automatically removed due to timeout */
66
- onTimeout?: (blockerId: string) => void;
67
- }
68
- /**
69
- * Internal representation of a blocker with all fields populated.
70
- *
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.
73
- *
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.
77
- *
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) */
57
+ /**
58
+ * Remove after a positive finite delay in milliseconds, using bounded timer chunks.
59
+ * Omitted, zero and finite negative values disable expiration. Invalid/nonfinite values
60
+ * or a deadline beyond Number.MAX_SAFE_INTEGER throw RangeError before mutation.
61
+ */
86
62
  timeout?: number;
87
- /** Internal timeout ID for cleanup */
88
- timeoutId?: ReturnType<typeof setTimeout>;
89
63
  /** Callback invoked when the blocker is automatically removed due to timeout */
90
64
  onTimeout?: (blockerId: string) => void;
91
65
  }
92
66
  /**
93
67
  * Complete blocker information including its unique identifier.
94
68
  *
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}.
98
- *
99
- * @example
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
- * ```
107
- *
108
69
  * @public
109
70
  * @since 0.6.0
110
71
  */
111
- interface BlockerInfo extends Omit<StoredBlocker, "timeoutId"> {
112
- id: string;
72
+ interface BlockerInfo {
73
+ readonly id: string;
74
+ readonly scope: string | ReadonlyArray<string>;
75
+ readonly reason: string;
76
+ readonly priority: number;
77
+ readonly timestamp: number;
78
+ readonly timeout?: number;
79
+ readonly onTimeout?: (blockerId: string) => void;
113
80
  }
114
- /**
115
- * Internal state structure of the UI blocking store.
116
- *
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.
120
- *
121
- * @internal
122
- * @since 0.6.0
123
- */
81
+ /** Immutable projection published by the Zustand adapter. */
124
82
  interface UIBlockingStoreState {
125
- activeBlockers: Map<string, StoredBlocker>;
126
- middlewares: Map<string, Middleware>;
83
+ readonly blockingSnapshot: BlockingLifecycleSnapshot;
127
84
  }
128
85
  /**
129
86
  * All available actions for managing UI blocking state.
130
87
  *
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.
134
- *
135
- * All actions are safe to call multiple times and handle edge cases gracefully.
136
- *
137
- * @example
138
- * Using store actions directly
139
- * ```ts
140
- * import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
141
- *
142
- * // Add a blocker
143
- * uiBlockingStoreApi.getState().addBlocker('my-blocker', {
144
- * scope: 'form',
145
- * reason: 'Processing...'
146
- * });
147
- *
148
- * // Check if blocked
149
- * const isBlocked = uiBlockingStoreApi.getState().isBlocked('form');
150
- *
151
- * // Remove blocker
152
- * uiBlockingStoreApi.getState().removeBlocker('my-blocker');
153
- * ```
154
- *
155
88
  * @public
156
89
  * @since 0.6.0
157
90
  */
158
91
  interface UIBlockingStoreActions {
159
92
  addBlocker: (id: string, config?: BlockerConfig) => void;
160
93
  updateBlocker: (id: string, config?: Partial<BlockerConfig>) => void;
94
+ /** @internal Apply current reactive configuration, clearing omitted optional fields. */
95
+ replaceBlocker: (id: string, config: BlockerConfig) => void;
161
96
  removeBlocker: (id: string) => void;
162
97
  isBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
163
- getBlockingInfo: (scope: string) => ReadonlyArray<BlockerInfo>;
98
+ getBlockingInfo: (scope: string) => ReadonlyArray<Readonly<BlockerInfo>>;
164
99
  clearAllBlockers: VoidFunction;
165
100
  clearBlockersForScope: (scope: string) => void;
166
- registerMiddleware: (name: string, middleware: Middleware) => void;
167
- unregisterMiddleware: (name: string) => void;
168
- runMiddlewares: (context: MiddlewareContext) => Promise<void>;
101
+ /** Observe lifecycle transitions through an anonymous, ownership-safe lease. */
102
+ observeBlockingEvents: (observer: Middleware) => VoidFunction;
169
103
  }
170
104
  /**
171
105
  * Complete UI blocking store type combining state and actions.
172
106
  *
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
- *
177
- * @example
178
- * Using the complete store
179
- * ```ts
180
- * import { useUIBlockingStore } from '@okyrychenko-dev/react-action-guard';
181
- *
182
- * function MyComponent() {
183
- * const store = useUIBlockingStore();
184
- *
185
- * // Access state
186
- * console.log(store.activeBlockers.size);
187
- *
188
- * // Call actions
189
- * store.addBlocker('my-blocker', { scope: 'form' });
190
- *
191
- * return <div>...</div>;
192
- * }
193
- * ```
194
- *
195
107
  * @public
196
108
  * @since 0.6.0
197
109
  */
198
110
  type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
199
- interface ShallowStoreBindings<StoreState> {
200
- useStore: {
201
- (): StoreState;
202
- <T>(selector: (state: StoreState) => T): T;
203
- };
204
- useStoreApi: StoreApi<StoreState>;
205
- }
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;
226
111
 
227
112
  type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
228
113
  type MiddlewareBlockerConfig = BlockerConfig;
229
114
  interface MiddlewareContext {
115
+ registration?: object;
230
116
  action: BlockingAction;
231
117
  blockerId: string;
232
118
  config?: MiddlewareBlockerConfig;
@@ -240,281 +126,68 @@ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
240
126
  /**
241
127
  * Creates middleware for tracking blocker events to analytics platforms.
242
128
  *
243
- * Automatically tracks all blocker actions (add, remove, update, timeout, clear)
244
- * to your analytics platform. Supports Google Analytics, Mixpanel, Amplitude,
245
- * and custom analytics providers.
246
- *
247
- * Event data includes: blockerId, action, scope, reason, priority, timestamp,
248
- * and duration (for remove events).
249
- *
250
- * @param config - Analytics configuration specifying provider and credentials
251
- *
252
- * @example
253
- * Google Analytics integration
254
- * ```ts
255
- * import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
256
- *
257
- * const analyticsMiddleware = createAnalyticsMiddleware({
258
- * provider: 'ga',
259
- * trackingId: 'GA-XXXXX-Y'
260
- * });
261
- *
262
- * configureMiddleware([analyticsMiddleware]);
263
- * ```
264
- *
265
- * @example
266
- * Mixpanel integration
267
- * ```ts
268
- * const analyticsMiddleware = createAnalyticsMiddleware({
269
- * provider: 'mixpanel',
270
- * token: 'your_mixpanel_token',
271
- * options: {
272
- * debug: true
273
- * }
274
- * });
275
- *
276
- * configureMiddleware([analyticsMiddleware]);
277
- * ```
278
- *
279
- * @example
280
- * Custom analytics provider
281
- * ```ts
282
- * const analyticsMiddleware = createAnalyticsMiddleware({
283
- * track: (eventName, eventData) => {
284
- * // Send to your custom analytics
285
- * myAnalytics.track(eventName, {
286
- * ...eventData,
287
- * userId: getCurrentUserId()
288
- * });
289
- * }
290
- * });
291
- * ```
292
- *
293
- * @example
294
- * Amplitude with user properties
295
- * ```ts
296
- * const analyticsMiddleware = createAnalyticsMiddleware({
297
- * provider: 'amplitude',
298
- * apiKey: 'your_amplitude_key',
299
- * userId: 'user-123',
300
- * userProperties: {
301
- * plan: 'premium',
302
- * role: 'admin'
303
- * }
304
- * });
305
- * ```
306
- *
307
- * @see {@link AnalyticsConfig} for configuration options
308
- * @see {@link configureMiddleware} for registering middleware
309
- *
310
129
  * @public
311
130
  * @since 0.6.0
131
+ * @see {@link AnalyticsConfig} for configuration options
132
+ * @see {@link configureMiddleware} for registering middleware
312
133
  */
313
134
  declare function createAnalyticsMiddleware(config: AnalyticsConfig): Middleware;
314
135
 
315
- /**
316
- * Configures and registers multiple middleware functions for the UI blocking store.
317
- *
318
- * Convenience function that registers an array of middleware at once. Each middleware
319
- * is automatically assigned a unique name based on its array index ("middleware-0",
320
- * "middleware-1", etc.). Middleware are executed in the order they are registered.
321
- *
322
- * All middleware receive {@link MiddlewareContext} containing action type, blocker ID,
323
- * configuration, and timestamp for each blocking event.
324
- *
325
- * @param middlewares - Array of middleware functions to register. Functions are called
326
- * in order for each blocking action (add, remove, update, etc.)
327
- *
328
- * @example
329
- * Basic usage with built-in middleware
330
- * ```ts
331
- * import { configureMiddleware, loggerMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
332
- *
333
- * configureMiddleware([
334
- * loggerMiddleware,
335
- * createPerformanceMiddleware({ threshold: 1000 })
336
- * ]);
337
- * ```
338
- *
339
- * @example
340
- * With custom middleware
341
- * ```ts
342
- * const customMiddleware: Middleware = (context) => {
343
- * if (context.action === 'add') {
344
- * console.log(`Blocker added: ${context.blockerId}`);
345
- * }
346
- * };
347
- *
348
- * configureMiddleware([
349
- * loggerMiddleware,
350
- * customMiddleware
351
- * ]);
352
- * ```
353
- *
354
- * @example
355
- * Analytics integration
356
- * ```ts
357
- * import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
358
- *
359
- * configureMiddleware([
360
- * createAnalyticsMiddleware({
361
- * provider: 'google-analytics',
362
- * trackingId: 'GA-XXXXX'
363
- * })
364
- * ]);
365
- * ```
366
- *
367
- * @see {@link Middleware} for middleware function signature
368
- * @see {@link loggerMiddleware} for built-in logging middleware
369
- * @see {@link createAnalyticsMiddleware} for analytics tracking
370
- * @see {@link createPerformanceMiddleware} for performance monitoring
371
- *
372
- * @public
373
- * @since 0.6.0
374
- */
136
+ /** Replace configured global observations without releasing independently owned observations. */
375
137
  declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): void;
376
138
 
377
139
  /**
378
140
  * Built-in middleware for logging all blocking actions to the console.
379
141
  *
380
- * Logs every blocker action (add, remove, update, timeout, clear) with emoji
381
- * indicators and formatted data. Useful for debugging blocking behavior during
382
- * development. Automatically disabled in production builds for performance.
383
- *
384
- * Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
385
- *
386
- * Emoji indicators:
387
- * - 🔒 Add
388
- * - 🔓 Remove
389
- * - 🔄 Update
390
- * - ⏰ Timeout
391
- * - 🧹 Clear/ClearScope
392
- *
393
- * @example
394
- * Register logger middleware
395
- * ```ts
396
- * import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
397
- *
398
- * // Enable logging for all blocking actions
399
- * configureMiddleware([loggerMiddleware]);
400
- * ```
401
- *
402
- * @example
403
- * Console output examples
404
- * ```
405
- * [UIBlocking] 🔒 save-form {scope: "form", reason: "Saving...", priority: 50}
406
- * [UIBlocking] 🔄 save-form {scope: "form", reason: "Almost done..."}
407
- * [UIBlocking] 🔓 save-form {reason: "Saving..."}
408
- * [UIBlocking] ⏰ api-timeout {timeout: 30000}
409
- * ```
410
- *
411
- * @example
412
- * Production check pattern
413
- * ```ts
414
- * import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
415
- * const middlewares = [];
416
- * if (process.env.NODE_ENV !== 'production') {
417
- * middlewares.push(loggerMiddleware);
418
- * }
419
- * configureMiddleware(middlewares);
420
- * ```
421
- *
142
+ * @public
143
+ * @since 0.6.0
422
144
  * @see {@link configureMiddleware} for registering middleware
423
145
  * @see {@link Middleware} for middleware function signature
424
146
  * @see {@link MiddlewareContext} for available context data
425
- *
426
- * @public
427
- * @since 0.6.0
428
147
  */
429
148
  declare function loggerMiddleware(context: MiddlewareContext): void;
430
149
 
150
+ declare const DEFAULT_SCOPE: "global";
151
+
152
+ /**
153
+ * A blocking scope is either one name or a list of names.
154
+ *
155
+ * Lists are interpreted as a set: order and duplicate entries have no semantic meaning.
156
+ * An empty list observes no scope. Guarded controls additionally treat an empty explicit list as
157
+ * an instruction to inherit their provider scope through {@link resolveScope}.
158
+ */
159
+ type Scope = string | ReadonlyArray<string>;
160
+
161
+ /**
162
+ * Returns a frozen, stable, deduplicated, sorted scope list.
163
+ * An omitted scope means global; an empty list remains empty.
164
+ */
165
+ declare function normalizeScope(scope?: Scope): ReadonlyArray<string>;
166
+ /**
167
+ * Resolves guarded-control inheritance.
168
+ * An omitted or empty explicit scope inherits; any non-empty explicit scope wins.
169
+ */
170
+ declare function resolveScope(explicitScope?: Scope, inheritedScope?: Scope): Optional<Scope>;
171
+ /**
172
+ * Checks ordinary observation semantics.
173
+ * A global blocker affects every observed scope and otherwise any shared scope is a match.
174
+ */
175
+ declare function scopeAffectsObservation(blockerScope: Scope, observedScope: Scope): boolean;
176
+ /**
177
+ * Checks targeted-clear semantics.
178
+ * Global blockers never match a target, including the global target; otherwise any shared scope
179
+ * is a match.
180
+ */
181
+ declare function scopeMatchesTarget(blockerScope: Scope, targetScope: Scope): boolean;
182
+
431
183
  interface PerformanceConfig {
432
184
  slowBlockThreshold?: number;
433
185
  onSlowBlock?: (blockerId: string, duration: number) => void;
434
186
  }
435
187
 
436
188
  /**
437
- * Creates middleware for monitoring blocker performance and detecting slow blocks.
438
- *
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.
442
- *
443
- * The middleware is side-effect free by default; use `onSlowBlock` for custom
444
- * reporting, logging, or analytics integration.
445
- *
446
- * @param config - Performance monitoring configuration
447
- * @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
448
- * Defaults to 3000ms (3 seconds).
449
- * @param config.onSlowBlock - Optional callback invoked when a slow block is detected.
450
- * Receives blockerId and duration in milliseconds.
451
- *
452
- * @returns Middleware function for performance monitoring
453
- *
454
- * @example
455
- * Basic usage with default threshold
456
- * ```ts
457
- * import { configureMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
458
- *
459
- * const performanceMiddleware = createPerformanceMiddleware();
460
- *
461
- * configureMiddleware([performanceMiddleware]);
462
- * // Track slow blocks via onSlowBlock if any blocker lasts > 3 seconds
463
- * ```
464
- *
465
- * @example
466
- * Custom threshold and callback
467
- * ```ts
468
- * const performanceMiddleware = createPerformanceMiddleware({
469
- * slowBlockThreshold: 5000, // 5 seconds
470
- * onSlowBlock: (blockerId, duration) => {
471
- * console.error(`Blocker ${blockerId} took ${duration}ms`);
472
- *
473
- * // Report to error tracking
474
- * Sentry.captureMessage('Slow UI Blocker', {
475
- * extra: { blockerId, duration }
476
- * });
477
- * }
478
- * });
479
- * ```
480
- *
481
- * @example
482
- * With analytics integration
483
- * ```ts
484
- * const performanceMiddleware = createPerformanceMiddleware({
485
- * slowBlockThreshold: 2000,
486
- * onSlowBlock: (blockerId, duration) => {
487
- * // Track slow blocks in analytics
488
- * analytics.track('slow_blocker_detected', {
489
- * blocker_id: blockerId,
490
- * duration_ms: duration,
491
- * threshold_ms: 2000
492
- * });
493
- * }
494
- * });
495
- * ```
496
- *
497
- * @example
498
- * Development vs Production
499
- * ```ts
500
- * const performanceMiddleware = createPerformanceMiddleware({
501
- * // Strict threshold in development
502
- * slowBlockThreshold: process.env.NODE_ENV === 'production' ? 5000 : 1000,
503
- * onSlowBlock: (blockerId, duration) => {
504
- * if (process.env.NODE_ENV === 'production') {
505
- * // Report to error service in production
506
- * errorService.log({ blockerId, duration });
507
- * } else {
508
- * // Detailed logging in development
509
- * console.table({ blockerId, duration });
510
- * }
511
- * }
512
- * });
513
- * ```
514
- *
515
- * @see {@link PerformanceConfig} for configuration options
516
- * @see {@link configureMiddleware} for registering middleware
517
- *
189
+ * Measures registration lifetimes from add until removal, timeout or bulk clear.
190
+ * Updates preserve the start; replacement adds restart it. Callback failures are isolated.
518
191
  * @public
519
192
  * @since 0.6.0
520
193
  */
@@ -547,94 +220,11 @@ declare function useUIBlockingStoreFromContext<T>(selector: (state: UIBlockingSt
547
220
  /**
548
221
  * Provider component for isolated UI blocking state management.
549
222
  *
550
- * Creates an independent store instance for a subtree of your application.
551
- * Each provider has its own isolated blocking state, separate from the global
552
- * store and other provider instances.
553
- *
554
- * **Use cases:**
555
- * - Server-Side Rendering (SSR) - Avoid state sharing between requests
556
- * - Testing - Isolated state for each test
557
- * - Micro-frontends - Independent blocking state per micro-app
558
- * - Modals/Dialogs - Scoped blocking within a dialog
559
- *
560
- * Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
561
- * provider-scoped store creation and middleware registration.
562
- *
563
- * @param props - Provider configuration
564
- * @param props.children - React children to wrap with the provider
565
- * @param props.middlewares - Array of middleware functions to register on store creation
566
- *
567
- * @example
568
- * Basic SSR setup
569
- * ```tsx
570
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
571
- *
572
- * function App() {
573
- * return (
574
- * <UIBlockingProvider>
575
- * <MyApplication />
576
- * </UIBlockingProvider>
577
- * );
578
- * }
579
- * ```
580
- *
581
- * @example
582
- * With middleware
583
- * ```tsx
584
- * import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
585
- *
586
- * function App() {
587
- * return (
588
- * <UIBlockingProvider
589
- * middlewares={[loggerMiddleware]}
590
- * >
591
- * <MyApplication />
592
- * </UIBlockingProvider>
593
- * );
594
- * }
595
- * ```
596
- *
597
- * @example
598
- * Multiple providers for micro-frontends
599
- * ```tsx
600
- * function MicroFrontendApp() {
601
- * return (
602
- * <div>
603
- * <UIBlockingProvider>
604
- * <MicroApp1 />
605
- * </UIBlockingProvider>
606
- *
607
- * <UIBlockingProvider>
608
- * <MicroApp2 />
609
- * </UIBlockingProvider>
610
- * </div>
611
- * );
612
- * }
613
- * ```
614
- *
615
- * @example
616
- * Testing with isolated state
617
- * ```tsx
618
- * // In your test file
619
- * import { render } from '@testing-library/react';
620
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
621
- *
622
- * test('blocker behavior', () => {
623
- * render(
624
- * <UIBlockingProvider>
625
- * <ComponentUnderTest />
626
- * </UIBlockingProvider>
627
- * );
628
- * // Each test gets isolated state
629
- * });
630
- * ```
631
- *
223
+ * @public
224
+ * @since 0.6.0
632
225
  * @see {@link useUIBlockingContext} to access the store from context
633
226
  * @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
634
227
  * @see {@link UIBlockingProviderProps} for prop details
635
- *
636
- * @public
637
- * @since 0.6.0
638
228
  */
639
229
  declare function UIBlockingProvider({ children, middlewares, }: UIBlockingProviderProps): ReactNode;
640
230
 
@@ -653,305 +243,96 @@ declare function useResolvedStoreApi(): StoreApi<UIBlockingStore>;
653
243
  /**
654
244
  * Hook to use the resolved store with a selector
655
245
  *
656
- * Automatically applies shallow comparison to prevent unnecessary re-renders.
246
+ * Uses toolkit shallow comparison by default, or the supplied domain equality.
657
247
  *
658
248
  * @param selector - Selector function to pick state from the store
249
+ * @param equalityFn - Optional comparison retaining equivalent selected values
659
250
  * @returns Selected state value
660
251
  */
661
- declare function useResolvedValue<T>(selector: (state: UIBlockingStore) => T): T;
252
+ declare function useResolvedValue<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (left: T, right: T) => boolean): T;
253
+
254
+ /**
255
+ * @deprecated Use `useOptionalUIBlockingContext`.
256
+ */
257
+ declare function useOptionalContext(): ReturnType<typeof useOptionalUIBlockingContext>;
258
+ /**
259
+ * @deprecated Use `useResolvedStoreApi`.
260
+ */
261
+ declare function useResolvedStore(): StoreApi<UIBlockingStore>;
262
+ /**
263
+ * @deprecated Use `useResolvedValue`.
264
+ */
265
+ declare function useResolvedStoreWithSelector<T>(selector: (state: UIBlockingStore) => T): T;
662
266
 
663
267
  /**
664
268
  * Options for useAsyncAction hook
665
269
  */
666
270
  interface UseAsyncActionOptions {
667
- /** Timeout in milliseconds after which the blocker will be automatically removed */
271
+ /** Blocker lifetime in milliseconds; does not cancel or settle the operation */
668
272
  timeout?: number;
669
273
  /** Callback invoked when the blocker is automatically removed due to timeout */
670
274
  onTimeout?: (blockerId: string) => void;
671
275
  }
672
276
  /**
673
- * Wraps async functions with automatic UI blocking during execution.
674
- *
675
- * Creates a wrapper function that automatically:
676
- * 1. Adds a blocker before executing the async function
677
- * 2. Executes the async function
678
- * 3. Removes the blocker after completion (success or failure)
679
- *
680
- * Each invocation generates a unique blocker ID (actionId-counter), allowing
681
- * multiple concurrent executions of the same action without ID conflicts.
682
- * The blocker is removed even if the async function throws an error.
683
- *
684
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
685
- *
686
- * @template T - The return type of the async function being wrapped
687
- * @param actionId - Base identifier for the action. Used in blocker ID and default reason.
688
- * Example: "saveForm", "fetchData", "submitOrder"
689
- * @param scope - Scope(s) to block during execution. Can be a single scope or array.
690
- * Defaults to "global" if not specified.
691
- * @param options - Optional configuration
692
- * @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
693
- * @param options.onTimeout - Callback invoked when blocker times out before async completes
694
- *
695
- * @returns Wrapper function that accepts an async function and returns a Promise with the same type
696
- *
697
- * @example
698
- * Basic form submission
699
- * ```tsx
700
- * function MyForm() {
701
- * const executeWithBlocking = useAsyncAction('saveForm', 'form');
702
- *
703
- * const handleSubmit = async (e) => {
704
- * e.preventDefault();
705
- *
706
- * await executeWithBlocking(async () => {
707
- * const response = await fetch('/api/save', {
708
- * method: 'POST',
709
- * body: JSON.stringify(formData)
710
- * });
711
- * return response.json();
712
- * });
713
- *
714
- * alert('Form saved!');
715
- * };
716
- *
717
- * return <form onSubmit={handleSubmit}>...</form>;
718
- * }
719
- * ```
720
- *
721
- * @example
722
- * With timeout and error handling
723
- * ```tsx
724
- * const executeWithBlocking = useAsyncAction(
725
- * 'fetchData',
726
- * ['global', 'data'],
727
- * {
728
- * timeout: 30000, // 30 seconds
729
- * onTimeout: (blockerId) => {
730
- * console.warn(`Operation ${blockerId} timed out`);
731
- * }
732
- * }
733
- * );
734
- *
735
- * const loadData = async () => {
736
- * try {
737
- * const data = await executeWithBlocking(async () => {
738
- * return await fetchDataFromAPI();
739
- * });
740
- * setData(data);
741
- * } catch (error) {
742
- * console.error('Failed to load data:', error);
743
- * // Blocker is automatically removed even on error
744
- * }
745
- * };
746
- * ```
747
- *
748
- * @example
749
- * Multiple concurrent actions
750
- * ```tsx
751
- * function FileUploader() {
752
- * const uploadWithBlocking = useAsyncAction('upload', 'uploads');
753
- *
754
- * const uploadFiles = async (files) => {
755
- * // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
756
- * const promises = files.map(file =>
757
- * uploadWithBlocking(() => uploadFile(file))
758
- * );
759
- *
760
- * await Promise.all(promises);
761
- * };
762
- *
763
- * return <...>;
764
- * }
765
- * ```
766
- *
767
- * @example
768
- * Type-safe return value
769
- * ```tsx
770
- * interface UserData {
771
- * id: string;
772
- * name: string;
773
- * }
774
- *
775
- * function UserProfile() {
776
- * const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
777
- *
778
- * const loadUser = async (userId: string) => {
779
- * const userData = await executeWithBlocking(async () => {
780
- * const response = await fetch(`/api/users/${userId}`);
781
- * return response.json(); // TypeScript knows this is UserData
782
- * });
783
- *
784
- * // userData is typed as UserData
785
- * console.log(userData.name);
786
- * };
787
- *
788
- * return <...>;
789
- * }
790
- * ```
791
- *
792
- * @see {@link useBlocker} for manual blocker management
793
- * @see {@link useIsBlocked} to check blocking state
794
- * @see {@link UseAsyncActionOptions} for available options
277
+ * Wraps async functions with UI blocking. Concurrent calls get unique blocker IDs;
278
+ * each blocker is removed in finally on success or failure, even after caller unmount.
279
+ * Tracking does not exclude concurrent execution. Timeout releases only the blocker.
795
280
  *
796
281
  * @public
797
282
  * @since 0.6.0
283
+ * @see {@link useActionBlocker} for manual blocker management
284
+ * @see {@link useIsBlocked} to check blocking state
285
+ * @see {@link UseAsyncActionOptions} for available options
798
286
  */
799
287
  declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
800
288
 
289
+ declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
290
+ setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
291
+ [x: string]: unknown;
292
+ [x: number]: unknown;
293
+ [x: symbol]: unknown;
294
+ type: string;
295
+ }) | undefined): void;
296
+ setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
297
+ [x: string]: unknown;
298
+ [x: number]: unknown;
299
+ [x: symbol]: unknown;
300
+ type: string;
301
+ }) | undefined): void;
302
+ devtools: {
303
+ cleanup: () => void;
304
+ };
305
+ };
306
+ declare function useUIBlockingStore(): UIBlockingStore;
307
+ declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
308
+
801
309
  /**
802
310
  * Automatically manages a UI blocker based on component lifecycle.
803
311
  *
804
- * Creates a blocker that is automatically added when the condition is true
805
- * and removed when the component unmounts or condition becomes false.
806
- * Supports scoped blocking, priorities, timeouts, and works with both
807
- * global store and isolated provider instances.
808
- *
809
- * @param blockerId - Unique identifier for this blocker. Must be unique within the scope.
810
- * @param config - Configuration object for the blocker
811
- * @param config.scope - Scope(s) to block. Can be a single scope string (e.g., "form", "navigation")
812
- * or an array of scopes (e.g., ["form", "navigation"]). Defaults to "global".
813
- * @param config.reason - Human-readable reason for blocking, displayed in DevTools and UI
814
- * @param config.priority - Priority level (0-100). Higher priority blockers take precedence.
815
- * Use 0-30 for low priority, 31-70 for medium, 71-100 for high priority.
816
- * @param config.timeout - Auto-removal timeout in milliseconds. Blocker will be automatically
817
- * removed after this duration. Use 0 or undefined for no timeout.
818
- * @param config.onTimeout - Callback function invoked when the blocker times out
819
- * @param isActive - When true, blocker is active; when false, blocker is removed.
820
- * Defaults to true. Use this for conditional blocking.
821
- *
822
- * @example
823
- * Basic form submission blocking
824
- * ```tsx
825
- * function MyForm() {
826
- * const [isSubmitting, setIsSubmitting] = useState(false);
827
- *
828
- * useBlocker('form-submit', {
829
- * scope: 'form',
830
- * reason: 'Submitting form...',
831
- * priority: 50,
832
- * timeout: 30000, // Auto-remove after 30 seconds
833
- * onTimeout: () => {
834
- * console.warn('Form submission timed out');
835
- * setIsSubmitting(false);
836
- * }
837
- * }, isSubmitting);
838
- *
839
- * return <form onSubmit={handleSubmit}>...</form>;
840
- * }
841
- * ```
842
- *
843
- * @example
844
- * Multiple scopes with high priority
845
- * ```tsx
846
- * useBlocker('critical-save', {
847
- * scope: ['form', 'navigation', 'actions'],
848
- * reason: 'Saving critical data...',
849
- * priority: 90,
850
- * }, isSaving);
851
- * ```
852
- *
853
- * @example
854
- * Conditional blocking based on unsaved changes
855
- * ```tsx
856
- * function Editor() {
857
- * const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
858
- *
859
- * useBlocker('unsaved-changes', {
860
- * scope: 'navigation',
861
- * reason: 'You have unsaved changes',
862
- * priority: 80,
863
- * }, hasUnsavedChanges);
864
- *
865
- * return <div>...</div>;
866
- * }
867
- * ```
868
- *
312
+ * @public
313
+ * @since 0.6.0
869
314
  * @see {@link useIsBlocked} to check if a scope is currently blocked
870
315
  * @see {@link useBlockingInfo} to get detailed blocker information
871
316
  * @see {@link useAsyncAction} for async operation wrapping with automatic blocking
872
317
  * @see {@link useConfirmableBlocker} for blockers that require user confirmation
873
- *
874
- * @public
875
- * @since 0.6.0
876
318
  */
877
- declare function useBlocker(blockerId: string, config: BlockerConfig, isActive?: boolean): void;
319
+ declare function useActionBlocker(blockerId: string, config: BlockerConfig, isActive?: boolean): void;
320
+ /**
321
+ * @deprecated Use {@link useActionBlocker} to avoid confusion with React Router's `useBlocker`.
322
+ */
323
+ declare const useBlocker: typeof useActionBlocker;
878
324
 
879
325
  /**
880
- * Gets detailed information about all active blockers for a specific scope.
881
- *
882
- * Returns an array of blocker information objects, sorted by priority (highest first).
883
- * Each blocker object contains id, reason, priority, and scope information.
884
- * The hook automatically re-renders when blockers are added, removed, or updated.
885
- *
886
- * Uses memoization to provide a stable reference - only re-renders when the actual
887
- * blocker list changes, not on every store update. This makes it efficient for
888
- * rendering lists of blockers.
889
- *
890
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
891
- *
892
- * @param scope - Scope to get blocking information for. Defaults to "global" if not specified.
893
- * @returns Array of {@link BlockerInfo} objects, sorted by priority (highest to lowest)
894
- *
895
- * @example
896
- * Display list of active blockers
897
- * ```tsx
898
- * function BlockerList() {
899
- * const blockers = useBlockingInfo('form');
900
- *
901
- * if (blockers.length === 0) {
902
- * return <div>No active blockers</div>;
903
- * }
904
- *
905
- * return (
906
- * <ul>
907
- * {blockers.map((blocker) => (
908
- * <li key={blocker.id}>
909
- * {blocker.reason} (Priority: {blocker.priority})
910
- * </li>
911
- * ))}
912
- * </ul>
913
- * );
914
- * }
915
- * ```
916
- *
917
- * @example
918
- * Show highest priority blocker reason
919
- * ```tsx
920
- * function BlockingBanner() {
921
- * const blockers = useBlockingInfo('global');
922
- *
923
- * // blockers are already sorted by priority (highest first)
924
- * const topBlocker = blockers[0];
925
- *
926
- * if (!topBlocker) {
927
- * return null;
928
- * }
929
- *
930
- * return <div className="banner">{topBlocker.reason}</div>;
931
- * }
932
- * ```
933
- *
934
- * @example
935
- * Count active blockers
936
- * ```tsx
937
- * function BlockerCount() {
938
- * const formBlockers = useBlockingInfo('form');
939
- * const navBlockers = useBlockingInfo('navigation');
940
- *
941
- * const total = formBlockers.length + navBlockers.length;
942
- *
943
- * return <div>{total} active blockers</div>;
944
- * }
945
- * ```
946
- *
947
- * @see {@link useIsBlocked} for a simple boolean check
948
- * @see {@link useBlocker} to create blockers
949
- * @see {@link BlockerInfo} for the structure of blocker information objects
326
+ * Gets immutable, priority-ordered information about blockers affecting the observed scopes.
327
+ * Unrelated lifecycle changes preserve the result and do not trigger a render.
950
328
  *
951
329
  * @public
952
330
  * @since 0.6.0
331
+ * @see {@link useIsBlocked} for a simple boolean check
332
+ * @see {@link useActionBlocker} to create blockers
333
+ * @see {@link BlockerInfo} for the structure of blocker information objects
953
334
  */
954
- declare function useBlockingInfo(scope?: string): ReadonlyArray<BlockerInfo>;
335
+ declare function useBlockingInfo(scope?: Scope): ReadonlyArray<Readonly<BlockerInfo>>;
955
336
 
956
337
  /**
957
338
  * Configuration for conditional blocker hook.
@@ -973,142 +354,11 @@ interface ConditionalBlockerConfig<TState = unknown> extends Omit<BlockerConfig,
973
354
  /**
974
355
  * Blocks UI based on a dynamic condition that is periodically evaluated.
975
356
  *
976
- * Periodically checks a condition function and automatically adds or removes
977
- * a blocker based on the result. Useful for blocking based on application state,
978
- * network status, feature flags, or any other dynamic condition.
979
- *
980
- * The condition function receives optional state and returns a boolean.
981
- * It's evaluated immediately on mount and then at the specified interval.
982
- * The blocker is added/removed automatically as the condition changes.
983
- *
984
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
985
- *
986
- * @template TState - Type of the state object passed to the condition function
987
- * @param blockerId - Unique identifier for this conditional blocker
988
- * @param config - Configuration for conditional blocking
989
- * @param config.condition - Function that returns true to block, false to unblock.
990
- * Receives the `state` object as parameter.
991
- * @param config.state - Optional state object passed to the condition function
992
- * @param config.checkInterval - How often to check the condition in milliseconds.
993
- * Defaults to 1000ms (1 second). Lower values mean
994
- * more responsive blocking but higher CPU usage.
995
- * @param config.scope - Scope(s) to block when condition is true
996
- * @param config.reason - Reason for blocking (displayed to users)
997
- * @param config.priority - Optional blocker priority (0-100)
998
- *
999
- * @example
1000
- * Block based on network status
1001
- * ```tsx
1002
- * function OfflineBlocker() {
1003
- * const [isOnline, setIsOnline] = useState(navigator.onLine);
1004
- *
1005
- * useEffect(() => {
1006
- * const handleOnline = () => setIsOnline(true);
1007
- * const handleOffline = () => setIsOnline(false);
1008
- *
1009
- * window.addEventListener('online', handleOnline);
1010
- * window.addEventListener('offline', handleOffline);
1011
- *
1012
- * return () => {
1013
- * window.removeEventListener('online', handleOnline);
1014
- * window.removeEventListener('offline', handleOffline);
1015
- * };
1016
- * }, []);
1017
- *
1018
- * useConditionalBlocker('offline', {
1019
- * condition: () => !isOnline,
1020
- * state: { isOnline },
1021
- * checkInterval: 500, // Check every 500ms
1022
- * scope: 'global',
1023
- * reason: 'No internet connection. Please check your network.',
1024
- * priority: 95
1025
- * });
1026
- *
1027
- * return <App />;
1028
- * }
1029
- * ```
1030
- *
1031
- * @example
1032
- * Block based on feature flag
1033
- * ```tsx
1034
- * function FeatureFlagBlocker() {
1035
- * const featureFlags = useFeatureFlags();
1036
- *
1037
- * useConditionalBlocker('feature-disabled', {
1038
- * condition: (state) => !state.featureFlags.newFeatureEnabled,
1039
- * state: { featureFlags },
1040
- * checkInterval: 5000, // Check every 5 seconds
1041
- * scope: 'new-feature',
1042
- * reason: 'This feature is currently disabled',
1043
- * priority: 70
1044
- * });
1045
- *
1046
- * return <NewFeature />;
1047
- * }
1048
- * ```
1049
- *
1050
- * @example
1051
- * Block during API rate limit
1052
- * ```tsx
1053
- * function RateLimitBlocker() {
1054
- * const [requestCount, setRequestCount] = useState(0);
1055
- * const [resetTime, setResetTime] = useState<number | null>(null);
1056
- *
1057
- * useConditionalBlocker('rate-limit', {
1058
- * condition: (state) => {
1059
- * if (!state.resetTime) {
1060
- * return false;
1061
- * }
1062
- * return Date.now() < state.resetTime && state.requestCount >= 100;
1063
- * },
1064
- * state: { requestCount, resetTime },
1065
- * checkInterval: 1000,
1066
- * scope: 'api',
1067
- * reason: 'API rate limit reached. Please wait.',
1068
- * priority: 90
1069
- * });
1070
- *
1071
- * return <Dashboard />;
1072
- * }
1073
- * ```
1074
- *
1075
- * @example
1076
- * Block based on form validation
1077
- * ```tsx
1078
- * function FormValidationBlocker() {
1079
- * const { formState } = useFormContext();
1080
- *
1081
- * useConditionalBlocker('invalid-form', {
1082
- * condition: (state) => !state.isValid && state.isDirty,
1083
- * state: formState,
1084
- * checkInterval: 500,
1085
- * scope: 'form-submit',
1086
- * reason: 'Please fix form errors before submitting',
1087
- * priority: 60
1088
- * });
1089
- *
1090
- * return <Form />;
1091
- * }
1092
- * ```
1093
- *
1094
- * @example
1095
- * Without state parameter (simple condition)
1096
- * ```tsx
1097
- * useConditionalBlocker('low-battery', {
1098
- * condition: () => !navigator.onLine,
1099
- * checkInterval: 30000, // Check every 30 seconds
1100
- * scope: 'heavy-operations',
1101
- * reason: 'Internet connection is required for this operation',
1102
- * priority: 80
1103
- * });
1104
- * ```
1105
- *
1106
- * @see {@link useBlocker} for simple conditional blocking with boolean
1107
- * @see {@link useScheduledBlocker} for time-based blocking
1108
- * @see {@link ConditionalBlockerConfig} for configuration options
1109
- *
1110
357
  * @public
1111
358
  * @since 0.6.0
359
+ * @see {@link useActionBlocker} for simple conditional blocking with boolean
360
+ * @see {@link useScheduledBlocker} for time-based blocking
361
+ * @see {@link ConditionalBlockerConfig} for configuration options
1112
362
  */
1113
363
  declare function useConditionalBlocker<TState = unknown>(blockerId: string, config: ConditionalBlockerConfig<TState>): void;
1114
364
 
@@ -1156,209 +406,21 @@ interface UseConfirmableBlockerReturn {
1156
406
  /**
1157
407
  * Creates a blocker that requires user confirmation before executing an action.
1158
408
  *
1159
- * Manages a confirmation dialog and blocking state for actions that need user approval.
1160
- * The blocker is active during both the confirmation dialog display and action execution.
1161
- *
1162
- * Workflow:
1163
- * 1. Call `execute()` to show confirmation dialog (blocker activated)
1164
- * 2. User confirms → `onConfirm` callback runs (blocker remains active)
1165
- * 3. Action completes → blocker removed
1166
- * 4. User cancels → blocker removed, optional `onCancel` callback runs
1167
- *
1168
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
1169
- *
1170
- * @param blockerId - Unique identifier for this confirmable blocker
1171
- * @param config - Configuration for the confirmable action
1172
- * @param config.confirmMessage - Message to display in the confirmation dialog
1173
- * @param config.confirmTitle - Optional dialog title (defaults to "Confirm Action")
1174
- * @param config.confirmButtonText - Optional confirm button text (defaults to "Confirm")
1175
- * @param config.cancelButtonText - Optional cancel button text (defaults to "Cancel")
1176
- * @param config.onConfirm - Async function to execute after user confirms
1177
- * @param config.onCancel - Optional callback when user cancels
1178
- * @param config.scope - Scope(s) to block during confirmation and execution
1179
- * @param config.reason - Optional custom blocking reason (defaults to confirmMessage)
1180
- * @param config.priority - Optional blocker priority (0-100)
1181
- *
1182
- * @returns Object containing:
1183
- * - `execute`: Function to trigger the confirmation dialog
1184
- * - `isDialogOpen`: Boolean indicating if confirmation dialog is visible
1185
- * - `isExecuting`: Boolean indicating if the confirmed action is executing
1186
- * - `confirmConfig`: Dialog configuration (title, message, button texts)
1187
- * - `onConfirm`: Function to call when user confirms
1188
- * - `onCancel`: Function to call when user cancels
1189
- *
1190
- * @example
1191
- * Delete confirmation with custom dialog
1192
- * ```tsx
1193
- * function DeleteButton({ itemId, itemName }) {
1194
- * const confirmableDelete = useConfirmableBlocker('delete-item', {
1195
- * confirmMessage: `Are you sure you want to delete "${itemName}"?`,
1196
- * confirmTitle: 'Delete Item',
1197
- * confirmButtonText: 'Delete',
1198
- * cancelButtonText: 'Keep',
1199
- * scope: ['form', 'navigation'],
1200
- * priority: 80,
1201
- * onConfirm: async () => {
1202
- * await deleteItem(itemId);
1203
- * showToast('Item deleted successfully');
1204
- * },
1205
- * onCancel: () => {
1206
- * console.log('Delete cancelled');
1207
- * }
1208
- * });
1209
- *
1210
- * return (
1211
- * <>
1212
- * <button onClick={confirmableDelete.execute}>
1213
- * Delete
1214
- * </button>
1215
- *
1216
- * {confirmableDelete.isDialogOpen && (
1217
- * <ConfirmDialog
1218
- * {...confirmableDelete.confirmConfig}
1219
- * onConfirm={confirmableDelete.onConfirm}
1220
- * onCancel={confirmableDelete.onCancel}
1221
- * isLoading={confirmableDelete.isExecuting}
1222
- * />
1223
- * )}
1224
- * </>
1225
- * );
1226
- * }
1227
- * ```
1228
- *
1229
- * @example
1230
- * Simple confirmation with default dialog
1231
- * ```tsx
1232
- * function LogoutButton() {
1233
- * const confirmLogout = useConfirmableBlocker('logout', {
1234
- * confirmMessage: 'Are you sure you want to log out?',
1235
- * scope: 'global',
1236
- * onConfirm: async () => {
1237
- * await api.logout();
1238
- * navigate('/login');
1239
- * }
1240
- * });
1241
- *
1242
- * return (
1243
- * <>
1244
- * <button onClick={confirmLogout.execute}>Logout</button>
1245
- *
1246
- * {confirmLogout.isDialogOpen && (
1247
- * <Dialog
1248
- * title={confirmLogout.confirmConfig.title}
1249
- * message={confirmLogout.confirmConfig.message}
1250
- * onConfirm={confirmLogout.onConfirm}
1251
- * onCancel={confirmLogout.onCancel}
1252
- * />
1253
- * )}
1254
- * </>
1255
- * );
1256
- * }
1257
- * ```
1258
- *
1259
- * @example
1260
- * With loading state during execution
1261
- * ```tsx
1262
- * function SaveButton() {
1263
- * const confirmSave = useConfirmableBlocker('save-changes', {
1264
- * confirmMessage: 'Save all changes?',
1265
- * scope: 'form',
1266
- * onConfirm: async () => {
1267
- * await saveAllChanges();
1268
- * }
1269
- * });
1270
- *
1271
- * return (
1272
- * <button
1273
- * onClick={confirmSave.execute}
1274
- * disabled={confirmSave.isExecuting}
1275
- * >
1276
- * {confirmSave.isExecuting ? 'Saving...' : 'Save All'}
1277
- * </button>
1278
- * );
1279
- * }
1280
- * ```
1281
- *
1282
- * @see {@link useBlocker} for simple blocking without confirmation
1283
- * @see {@link ConfirmableBlockerConfig} for configuration options
1284
- * @see {@link UseConfirmableBlockerReturn} for return value structure
1285
- *
1286
409
  * @public
1287
410
  * @since 0.6.0
411
+ * @see {@link useActionBlocker} for simple blocking without confirmation
412
+ * @see {@link ConfirmableBlockerConfig} for configuration options
413
+ * @see {@link UseConfirmableBlockerReturn} for return value structure
1288
414
  */
1289
415
  declare function useConfirmableBlocker(blockerId: string, config: ConfirmableBlockerConfig): UseConfirmableBlockerReturn;
1290
416
 
1291
417
  /**
1292
418
  * Checks if one or more scopes are currently blocked.
1293
419
  *
1294
- * Returns a boolean indicating whether any of the specified scopes have active blockers.
1295
- * The hook automatically re-renders the component when the blocking state changes,
1296
- * ensuring your UI always reflects the current blocking state.
1297
- *
1298
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
1299
- *
1300
- * @param scope - Scope(s) to check for blocking. Can be:
1301
- * - A single scope string (e.g., "form", "navigation", "global")
1302
- * - An array of scopes (e.g., ["form", "navigation"])
1303
- * - undefined to check the "global" scope
1304
- * @returns true if ANY of the specified scopes are blocked, false otherwise
1305
- *
1306
- * @example
1307
- * Basic single scope check
1308
- * ```tsx
1309
- * function SubmitButton() {
1310
- * const isFormBlocked = useIsBlocked('form');
1311
- *
1312
- * return (
1313
- * <button disabled={isFormBlocked}>
1314
- * {isFormBlocked ? 'Processing...' : 'Submit'}
1315
- * </button>
1316
- * );
1317
- * }
1318
- * ```
1319
- *
1320
- * @example
1321
- * Multiple scopes check
1322
- * ```tsx
1323
- * function NavigationLink({ href, children }) {
1324
- * const isBlocked = useIsBlocked(['navigation', 'global']);
1325
- *
1326
- * return (
1327
- * <a
1328
- * href={href}
1329
- * onClick={(e) => {
1330
- * if (isBlocked) {
1331
- * e.preventDefault();
1332
- * alert('Navigation is currently blocked');
1333
- * }
1334
- * }}
1335
- * style={{ opacity: isBlocked ? 0.5 : 1 }}
1336
- * >
1337
- * {children}
1338
- * </a>
1339
- * );
1340
- * }
1341
- * ```
1342
- *
1343
- * @example
1344
- * Conditional rendering based on blocking state
1345
- * ```tsx
1346
- * function SaveIndicator() {
1347
- * const isSaving = useIsBlocked('save');
1348
- *
1349
- * if (!isSaving) {
1350
- * return null;
1351
- * }
1352
- *
1353
- * return <div className="spinner">Saving...</div>;
1354
- * }
1355
- * ```
1356
- *
1357
- * @see {@link useBlocker} to create a blocker
1358
- * @see {@link useBlockingInfo} to get detailed information about active blockers
1359
- *
1360
420
  * @public
1361
421
  * @since 0.6.0
422
+ * @see {@link useActionBlocker} to create a blocker
423
+ * @see {@link useBlockingInfo} to get detailed information about active blockers
1362
424
  */
1363
425
  declare function useIsBlocked(scope?: string | ReadonlyArray<string>): boolean;
1364
426
 
@@ -1369,7 +431,11 @@ declare function useIsBlocked(scope?: string | ReadonlyArray<string>): boolean;
1369
431
  * 1. start + duration: Block for a specific duration after start time
1370
432
  * 2. start + end: Block between start and end times
1371
433
  *
1372
- * Note: If both duration and end are provided, duration takes precedence.
434
+ * Windows are half-open [start, end). Omit both duration and end for indefinite blocking.
435
+ * Zero duration or equal bounds are empty and emit no schedule notifications.
436
+ * Every supplied bound must be valid, even when duration takes precedence over end.
437
+ * Negative/nonfinite duration, invalid dates, end before start and calculated end
438
+ * outside the JavaScript Date range cancel the schedule with a development warning.
1373
439
  */
1374
440
  interface BlockingSchedule {
1375
441
  /** Start time as ISO string, Date object, or timestamp */
@@ -1394,123 +460,12 @@ interface ScheduledBlockerConfig extends BlockerConfig {
1394
460
  /**
1395
461
  * Schedules UI blocking for a specific time period (e.g., maintenance windows).
1396
462
  *
1397
- * Automatically blocks UI between specified start and end times. Useful for
1398
- * scheduled maintenance, planned downtime, or time-based feature restrictions.
1399
- * Supports callbacks when the blocking period starts and ends.
1400
- *
1401
- * Time specifications support:
1402
- * - Date objects
1403
- * - ISO 8601 strings (e.g., "2024-01-15T10:00:00Z")
1404
- * - Unix timestamps (milliseconds since epoch)
1405
- *
1406
- * You can specify the end time either as an absolute time or as a duration
1407
- * from the start time. If both are provided, duration takes precedence.
1408
- *
1409
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
1410
- *
1411
- * @param blockerId - Unique identifier for this scheduled blocker
1412
- * @param config - Schedule configuration
1413
- * @param config.schedule - Schedule timing configuration
1414
- * @param config.schedule.start - When to start blocking (Date, ISO string, or timestamp)
1415
- * @param config.schedule.end - Optional absolute end time (Date, ISO string, or timestamp)
1416
- * @param config.schedule.duration - Optional duration in milliseconds from start time
1417
- * @param config.scope - Scope(s) to block during the scheduled period
1418
- * @param config.reason - Reason for blocking (displayed to users)
1419
- * @param config.priority - Optional blocker priority (0-100)
1420
- * @param config.onScheduleStart - Optional callback when blocking period starts
1421
- * @param config.onScheduleEnd - Optional callback when blocking period ends
1422
- *
1423
- * @example
1424
- * Maintenance window with absolute times
1425
- * ```tsx
1426
- * function MaintenanceWarning() {
1427
- * useScheduledBlocker('maintenance', {
1428
- * schedule: {
1429
- * start: new Date('2024-01-15T02:00:00Z'),
1430
- * end: new Date('2024-01-15T04:00:00Z'),
1431
- * },
1432
- * scope: 'global',
1433
- * reason: 'System maintenance in progress',
1434
- * priority: 95,
1435
- * onScheduleStart: () => {
1436
- * console.log('Maintenance started');
1437
- * showNotification('System is now in maintenance mode');
1438
- * },
1439
- * onScheduleEnd: () => {
1440
- * console.log('Maintenance completed');
1441
- * showNotification('System is back online');
1442
- * }
1443
- * });
1444
- *
1445
- * return <div>...</div>;
1446
- * }
1447
- * ```
1448
- *
1449
- * @example
1450
- * Short maintenance with duration
1451
- * ```tsx
1452
- * useScheduledBlocker('quick-maintenance', {
1453
- * schedule: {
1454
- * start: new Date('2024-01-15T03:00:00'),
1455
- * duration: 15 * 60 * 1000, // 15 minutes
1456
- * },
1457
- * scope: ['api', 'forms'],
1458
- * reason: 'Brief system update',
1459
- * priority: 90
1460
- * });
1461
- * ```
1462
- *
1463
- * @example
1464
- * Business hours enforcement
1465
- * ```tsx
1466
- * function BusinessHoursBlocker() {
1467
- * const now = new Date();
1468
- * const tomorrow9AM = new Date(now);
1469
- * tomorrow9AM.setDate(now.getDate() + 1);
1470
- * tomorrow9AM.setHours(9, 0, 0, 0);
1471
- *
1472
- * useScheduledBlocker('after-hours', {
1473
- * schedule: {
1474
- * start: new Date(now.getTime() + 1000), // Start immediately
1475
- * end: tomorrow9AM,
1476
- * },
1477
- * scope: 'trading',
1478
- * reason: 'Trading is only available during business hours (9 AM - 5 PM)',
1479
- * priority: 85
1480
- * });
1481
- *
1482
- * return <TradingDashboard />;
1483
- * }
1484
- * ```
1485
- *
1486
- * @example
1487
- * Using ISO strings for scheduling
1488
- * ```tsx
1489
- * useScheduledBlocker('deployment', {
1490
- * schedule: {
1491
- * start: '2024-01-20T00:00:00Z',
1492
- * duration: 2 * 60 * 60 * 1000, // 2 hours
1493
- * },
1494
- * scope: 'global',
1495
- * reason: 'Deploying new version',
1496
- * priority: 100,
1497
- * onScheduleStart: () => {
1498
- * analytics.track('deployment_started');
1499
- * },
1500
- * onScheduleEnd: () => {
1501
- * analytics.track('deployment_completed');
1502
- * window.location.reload(); // Reload to get new version
1503
- * }
1504
- * });
1505
- * ```
1506
- *
1507
- * @see {@link useBlocker} for immediate blocking without scheduling
463
+ * @public
464
+ * @since 0.6.0
465
+ * @see {@link useActionBlocker} for immediate blocking without scheduling
1508
466
  * @see {@link useConditionalBlocker} for condition-based blocking
1509
467
  * @see {@link ScheduledBlockerConfig} for configuration options
1510
468
  * @see {@link BlockingSchedule} for schedule specification details
1511
- *
1512
- * @public
1513
- * @since 0.6.0
1514
469
  */
1515
470
  declare function useScheduledBlocker(blockerId: string, config: ScheduledBlockerConfig): void;
1516
471
 
@@ -1530,7 +485,7 @@ interface BlockerConfigTyped<TScope extends string> {
1530
485
  scope?: ScopeValue<TScope>;
1531
486
  /** Human-readable reason for blocking */
1532
487
  reason?: string;
1533
- /** Priority level (higher priority blockers take precedence, minimum value is 0) */
488
+ /** Reason ordering priority (minimum 0). Every matching active blocker retains protection, including lower-priority ones. */
1534
489
  priority?: number;
1535
490
  /** Timestamp when the blocker was created */
1536
491
  timestamp?: number;
@@ -1545,7 +500,12 @@ interface BlockerConfigTyped<TScope extends string> {
1545
500
  */
1546
501
  interface TypedHooks<TScope extends string> {
1547
502
  /**
1548
- * Type-safe version of useBlocker hook
503
+ * Type-safe action blocker hook
504
+ */
505
+ useActionBlocker: (blockerId: string, config: BlockerConfigTyped<TScope>, isActive?: boolean) => void;
506
+ /**
507
+ * Backward-compatible name for useActionBlocker
508
+ * @deprecated Use `useActionBlocker`.
1549
509
  */
1550
510
  useBlocker: (blockerId: string, config: BlockerConfigTyped<TScope>, isActive?: boolean) => void;
1551
511
  /**
@@ -1559,7 +519,7 @@ interface TypedHooks<TScope extends string> {
1559
519
  /**
1560
520
  * Type-safe version of useBlockingInfo hook
1561
521
  */
1562
- useBlockingInfo: (scope: TScope) => ReturnType<typeof useBlockingInfo>;
522
+ useBlockingInfo: (scope: ScopeValue<TScope>) => ReturnType<typeof useBlockingInfo>;
1563
523
  }
1564
524
  /**
1565
525
  * Creates type-safe versions of all blocking hooks with your custom scope types
@@ -1573,26 +533,13 @@ interface TypedHooks<TScope extends string> {
1573
533
  * type AppScopes = "global" | "form" | "navigation" | "checkout";
1574
534
  *
1575
535
  * // Create typed hooks
1576
- * const { useBlocker, useIsBlocked, useAsyncAction } = createTypedHooks<AppScopes>();
536
+ * const { useActionBlocker, useIsBlocked, useAsyncAction } = createTypedHooks<AppScopes>();
1577
537
  *
1578
538
  * // Now TypeScript will catch typos
1579
- * useBlocker("id", { scope: "form" }); // ✓ OK
1580
- * useBlocker("id", { scope: "typo" }); // ✗ Type error!
539
+ * useActionBlocker("id", { scope: "form" }); // ✓ OK
540
+ * useActionBlocker("id", { scope: "typo" }); // ✗ Type error!
1581
541
  * ```
1582
542
  */
1583
543
  declare function createTypedHooks<TScope extends string>(): TypedHooks<TScope>;
1584
544
 
1585
- /**
1586
- * @deprecated Use `useOptionalUIBlockingContext`.
1587
- */
1588
- declare function useOptionalContext(): ReturnType<typeof useOptionalUIBlockingContext>;
1589
- /**
1590
- * @deprecated Use `useResolvedStoreApi`.
1591
- */
1592
- declare function useResolvedStore(): StoreApi<UIBlockingStore>;
1593
- /**
1594
- * @deprecated Use `useResolvedValue`.
1595
- */
1596
- declare function useResolvedStoreWithSelector<T>(selector: (state: UIBlockingStore) => T): T;
1597
-
1598
- 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 };
545
+ export { type AmplitudeConfig, type AnalyticsConfig, type AnalyticsEventData, type AnalyticsProvider, type BlockerConfig, type BlockerConfigTyped, type BlockerInfo, type BlockingAction, type BlockingEvent, type BlockingLifecycle, type BlockingLifecycleObservation, type BlockingLifecycleSnapshot, type BlockingSchedule, type ConditionalBlockerConfig, type ConfirmDialogConfig, type ConfirmableBlockerConfig, type CustomAnalyticsConfig, DEFAULT_SCOPE, type DefaultScopes, type GoogleAnalyticsConfig, type Middleware, type MiddlewareContext, type MixpanelConfig, type PerformanceConfig, type ScheduledBlockerConfig, type Scope, type ScopeValue, type TypedHooks, UIBlockingProvider, type UIBlockingProviderProps, type UIBlockingStore, type UIBlockingStoreActions, type UIBlockingStoreState, type UseAsyncActionOptions, type UseConfirmableBlockerReturn, configureMiddleware, createAnalyticsMiddleware, createBlockingLifecycle, createPerformanceMiddleware, createTypedHooks, loggerMiddleware, normalizeScope, resolveScope, scopeAffectsObservation, scopeMatchesTarget, uiBlockingStoreApi, useActionBlocker, useAsyncAction, useBlocker, useBlockingInfo, useConditionalBlocker, useConfirmableBlocker, useIsBlocked, useIsInsideUIBlockingProvider, useOptionalContext, useOptionalUIBlockingContext, useResolvedStore, useResolvedStoreApi, useResolvedStoreWithSelector, useResolvedValue, useScheduledBlocker, useUIBlockingContext, useUIBlockingStore, useUIBlockingStoreFromContext };