@okyrychenko-dev/react-action-guard 1.0.5 → 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.cts 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,153 +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
111
 
207
112
  type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
208
113
  type MiddlewareBlockerConfig = BlockerConfig;
209
114
  interface MiddlewareContext {
115
+ registration?: object;
210
116
  action: BlockingAction;
211
117
  blockerId: string;
212
118
  config?: MiddlewareBlockerConfig;
@@ -220,281 +126,68 @@ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
220
126
  /**
221
127
  * Creates middleware for tracking blocker events to analytics platforms.
222
128
  *
223
- * Automatically tracks all blocker actions (add, remove, update, timeout, clear)
224
- * to your analytics platform. Supports Google Analytics, Mixpanel, Amplitude,
225
- * and custom analytics providers.
226
- *
227
- * Event data includes: blockerId, action, scope, reason, priority, timestamp,
228
- * and duration (for remove events).
229
- *
230
- * @param config - Analytics configuration specifying provider and credentials
231
- *
232
- * @example
233
- * Google Analytics integration
234
- * ```ts
235
- * import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
236
- *
237
- * const analyticsMiddleware = createAnalyticsMiddleware({
238
- * provider: 'ga',
239
- * trackingId: 'GA-XXXXX-Y'
240
- * });
241
- *
242
- * configureMiddleware([analyticsMiddleware]);
243
- * ```
244
- *
245
- * @example
246
- * Mixpanel integration
247
- * ```ts
248
- * const analyticsMiddleware = createAnalyticsMiddleware({
249
- * provider: 'mixpanel',
250
- * token: 'your_mixpanel_token',
251
- * options: {
252
- * debug: true
253
- * }
254
- * });
255
- *
256
- * configureMiddleware([analyticsMiddleware]);
257
- * ```
258
- *
259
- * @example
260
- * Custom analytics provider
261
- * ```ts
262
- * const analyticsMiddleware = createAnalyticsMiddleware({
263
- * track: (eventName, eventData) => {
264
- * // Send to your custom analytics
265
- * myAnalytics.track(eventName, {
266
- * ...eventData,
267
- * userId: getCurrentUserId()
268
- * });
269
- * }
270
- * });
271
- * ```
272
- *
273
- * @example
274
- * Amplitude with user properties
275
- * ```ts
276
- * const analyticsMiddleware = createAnalyticsMiddleware({
277
- * provider: 'amplitude',
278
- * apiKey: 'your_amplitude_key',
279
- * userId: 'user-123',
280
- * userProperties: {
281
- * plan: 'premium',
282
- * role: 'admin'
283
- * }
284
- * });
285
- * ```
286
- *
287
- * @see {@link AnalyticsConfig} for configuration options
288
- * @see {@link configureMiddleware} for registering middleware
289
- *
290
129
  * @public
291
130
  * @since 0.6.0
131
+ * @see {@link AnalyticsConfig} for configuration options
132
+ * @see {@link configureMiddleware} for registering middleware
292
133
  */
293
134
  declare function createAnalyticsMiddleware(config: AnalyticsConfig): Middleware;
294
135
 
295
- /**
296
- * Configures and registers multiple middleware functions for the UI blocking store.
297
- *
298
- * Convenience function that registers an array of middleware at once. Each middleware
299
- * is automatically assigned a unique name based on its array index ("middleware-0",
300
- * "middleware-1", etc.). Middleware are executed in the order they are registered.
301
- *
302
- * All middleware receive {@link MiddlewareContext} containing action type, blocker ID,
303
- * configuration, and timestamp for each blocking event.
304
- *
305
- * @param middlewares - Array of middleware functions to register. Functions are called
306
- * in order for each blocking action (add, remove, update, etc.)
307
- *
308
- * @example
309
- * Basic usage with built-in middleware
310
- * ```ts
311
- * import { configureMiddleware, loggerMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
312
- *
313
- * configureMiddleware([
314
- * loggerMiddleware,
315
- * createPerformanceMiddleware({ threshold: 1000 })
316
- * ]);
317
- * ```
318
- *
319
- * @example
320
- * With custom middleware
321
- * ```ts
322
- * const customMiddleware: Middleware = (context) => {
323
- * if (context.action === 'add') {
324
- * console.log(`Blocker added: ${context.blockerId}`);
325
- * }
326
- * };
327
- *
328
- * configureMiddleware([
329
- * loggerMiddleware,
330
- * customMiddleware
331
- * ]);
332
- * ```
333
- *
334
- * @example
335
- * Analytics integration
336
- * ```ts
337
- * import { configureMiddleware, createAnalyticsMiddleware } from '@okyrychenko-dev/react-action-guard';
338
- *
339
- * configureMiddleware([
340
- * createAnalyticsMiddleware({
341
- * provider: 'google-analytics',
342
- * trackingId: 'GA-XXXXX'
343
- * })
344
- * ]);
345
- * ```
346
- *
347
- * @see {@link Middleware} for middleware function signature
348
- * @see {@link loggerMiddleware} for built-in logging middleware
349
- * @see {@link createAnalyticsMiddleware} for analytics tracking
350
- * @see {@link createPerformanceMiddleware} for performance monitoring
351
- *
352
- * @public
353
- * @since 0.6.0
354
- */
136
+ /** Replace configured global observations without releasing independently owned observations. */
355
137
  declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): void;
356
138
 
357
139
  /**
358
140
  * Built-in middleware for logging all blocking actions to the console.
359
141
  *
360
- * Logs every blocker action (add, remove, update, timeout, clear) with emoji
361
- * indicators and formatted data. Useful for debugging blocking behavior during
362
- * development. Register it conditionally when production logging is not desired.
363
- *
364
- * Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
365
- *
366
- * Emoji indicators:
367
- * - 🔒 Add
368
- * - 🔓 Remove
369
- * - 🔄 Update
370
- * - ⏰ Timeout
371
- * - 🧹 Clear/ClearScope
372
- *
373
- * @example
374
- * Register logger middleware
375
- * ```ts
376
- * import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
377
- *
378
- * // Enable logging for all blocking actions
379
- * configureMiddleware([loggerMiddleware]);
380
- * ```
381
- *
382
- * @example
383
- * Console output examples
384
- * ```
385
- * [UIBlocking] 🔒 save-form {scope: "form", reason: "Saving...", priority: 50}
386
- * [UIBlocking] 🔄 save-form {scope: "form", reason: "Almost done..."}
387
- * [UIBlocking] 🔓 save-form {reason: "Saving..."}
388
- * [UIBlocking] ⏰ api-timeout {timeout: 30000}
389
- * ```
390
- *
391
- * @example
392
- * Production check pattern
393
- * ```ts
394
- * import { configureMiddleware, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
395
- * const middlewares = [];
396
- * if (process.env.NODE_ENV !== 'production') {
397
- * middlewares.push(loggerMiddleware);
398
- * }
399
- * configureMiddleware(middlewares);
400
- * ```
401
- *
142
+ * @public
143
+ * @since 0.6.0
402
144
  * @see {@link configureMiddleware} for registering middleware
403
145
  * @see {@link Middleware} for middleware function signature
404
146
  * @see {@link MiddlewareContext} for available context data
405
- *
406
- * @public
407
- * @since 0.6.0
408
147
  */
409
148
  declare function loggerMiddleware(context: MiddlewareContext): void;
410
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
+
411
183
  interface PerformanceConfig {
412
184
  slowBlockThreshold?: number;
413
185
  onSlowBlock?: (blockerId: string, duration: number) => void;
414
186
  }
415
187
 
416
188
  /**
417
- * Creates middleware for monitoring blocker performance and detecting slow blocks.
418
- *
419
- * Tracks the duration of each blocker (from add to remove) and invokes an optional
420
- * callback when a blocker exceeds the configured threshold. Useful for identifying
421
- * performance issues, stuck blockers, or operations that take too long.
422
- *
423
- * The middleware is side-effect free by default; use `onSlowBlock` for custom
424
- * reporting, logging, or analytics integration.
425
- *
426
- * @param config - Performance monitoring configuration
427
- * @param config.slowBlockThreshold - Duration in ms before considering a block "slow".
428
- * Defaults to 3000ms (3 seconds).
429
- * @param config.onSlowBlock - Optional callback invoked when a slow block is detected.
430
- * Receives blockerId and duration in milliseconds.
431
- *
432
- * @returns Middleware function for performance monitoring
433
- *
434
- * @example
435
- * Basic usage with default threshold
436
- * ```ts
437
- * import { configureMiddleware, createPerformanceMiddleware } from '@okyrychenko-dev/react-action-guard';
438
- *
439
- * const performanceMiddleware = createPerformanceMiddleware();
440
- *
441
- * configureMiddleware([performanceMiddleware]);
442
- * // Track slow blocks via onSlowBlock if any blocker lasts > 3 seconds
443
- * ```
444
- *
445
- * @example
446
- * Custom threshold and callback
447
- * ```ts
448
- * const performanceMiddleware = createPerformanceMiddleware({
449
- * slowBlockThreshold: 5000, // 5 seconds
450
- * onSlowBlock: (blockerId, duration) => {
451
- * console.error(`Blocker ${blockerId} took ${duration}ms`);
452
- *
453
- * // Report to error tracking
454
- * Sentry.captureMessage('Slow UI Blocker', {
455
- * extra: { blockerId, duration }
456
- * });
457
- * }
458
- * });
459
- * ```
460
- *
461
- * @example
462
- * With analytics integration
463
- * ```ts
464
- * const performanceMiddleware = createPerformanceMiddleware({
465
- * slowBlockThreshold: 2000,
466
- * onSlowBlock: (blockerId, duration) => {
467
- * // Track slow blocks in analytics
468
- * analytics.track('slow_blocker_detected', {
469
- * blocker_id: blockerId,
470
- * duration_ms: duration,
471
- * threshold_ms: 2000
472
- * });
473
- * }
474
- * });
475
- * ```
476
- *
477
- * @example
478
- * Development vs Production
479
- * ```ts
480
- * const performanceMiddleware = createPerformanceMiddleware({
481
- * // Strict threshold in development
482
- * slowBlockThreshold: process.env.NODE_ENV === 'production' ? 5000 : 1000,
483
- * onSlowBlock: (blockerId, duration) => {
484
- * if (process.env.NODE_ENV === 'production') {
485
- * // Report to error service in production
486
- * errorService.log({ blockerId, duration });
487
- * } else {
488
- * // Detailed logging in development
489
- * console.table({ blockerId, duration });
490
- * }
491
- * }
492
- * });
493
- * ```
494
- *
495
- * @see {@link PerformanceConfig} for configuration options
496
- * @see {@link configureMiddleware} for registering middleware
497
- *
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.
498
191
  * @public
499
192
  * @since 0.6.0
500
193
  */
@@ -527,94 +220,11 @@ declare function useUIBlockingStoreFromContext<T>(selector: (state: UIBlockingSt
527
220
  /**
528
221
  * Provider component for isolated UI blocking state management.
529
222
  *
530
- * Creates an independent store instance for a subtree of your application.
531
- * Each provider has its own isolated blocking state, separate from the global
532
- * store and other provider instances.
533
- *
534
- * **Use cases:**
535
- * - Server-Side Rendering (SSR) - Avoid state sharing between requests
536
- * - Testing - Isolated state for each test
537
- * - Micro-frontends - Independent blocking state per micro-app
538
- * - Modals/Dialogs - Scoped blocking within a dialog
539
- *
540
- * Uses `createStoreProvider` from `@okyrychenko-dev/react-zustand-toolkit` with
541
- * provider-scoped store creation and middleware registration.
542
- *
543
- * @param props - Provider configuration
544
- * @param props.children - React children to wrap with the provider
545
- * @param props.middlewares - Array of middleware functions to register on store creation
546
- *
547
- * @example
548
- * Basic SSR setup
549
- * ```tsx
550
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
551
- *
552
- * function App() {
553
- * return (
554
- * <UIBlockingProvider>
555
- * <MyApplication />
556
- * </UIBlockingProvider>
557
- * );
558
- * }
559
- * ```
560
- *
561
- * @example
562
- * With middleware
563
- * ```tsx
564
- * import { UIBlockingProvider, loggerMiddleware } from '@okyrychenko-dev/react-action-guard';
565
- *
566
- * function App() {
567
- * return (
568
- * <UIBlockingProvider
569
- * middlewares={[loggerMiddleware]}
570
- * >
571
- * <MyApplication />
572
- * </UIBlockingProvider>
573
- * );
574
- * }
575
- * ```
576
- *
577
- * @example
578
- * Multiple providers for micro-frontends
579
- * ```tsx
580
- * function MicroFrontendApp() {
581
- * return (
582
- * <div>
583
- * <UIBlockingProvider>
584
- * <MicroApp1 />
585
- * </UIBlockingProvider>
586
- *
587
- * <UIBlockingProvider>
588
- * <MicroApp2 />
589
- * </UIBlockingProvider>
590
- * </div>
591
- * );
592
- * }
593
- * ```
594
- *
595
- * @example
596
- * Testing with isolated state
597
- * ```tsx
598
- * // In your test file
599
- * import { render } from '@testing-library/react';
600
- * import { UIBlockingProvider } from '@okyrychenko-dev/react-action-guard';
601
- *
602
- * test('blocker behavior', () => {
603
- * render(
604
- * <UIBlockingProvider>
605
- * <ComponentUnderTest />
606
- * </UIBlockingProvider>
607
- * );
608
- * // Each test gets isolated state
609
- * });
610
- * ```
611
- *
223
+ * @public
224
+ * @since 0.6.0
612
225
  * @see {@link useUIBlockingContext} to access the store from context
613
226
  * @see {@link useIsInsideUIBlockingProvider} to check if inside a provider
614
227
  * @see {@link UIBlockingProviderProps} for prop details
615
- *
616
- * @public
617
- * @since 0.6.0
618
228
  */
619
229
  declare function UIBlockingProvider({ children, middlewares, }: UIBlockingProviderProps): ReactNode;
620
230
 
@@ -633,148 +243,46 @@ declare function useResolvedStoreApi(): StoreApi<UIBlockingStore>;
633
243
  /**
634
244
  * Hook to use the resolved store with a selector
635
245
  *
636
- * Automatically applies shallow comparison to prevent unnecessary re-renders.
246
+ * Uses toolkit shallow comparison by default, or the supplied domain equality.
637
247
  *
638
248
  * @param selector - Selector function to pick state from the store
249
+ * @param equalityFn - Optional comparison retaining equivalent selected values
639
250
  * @returns Selected state value
640
251
  */
641
- 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;
642
266
 
643
267
  /**
644
268
  * Options for useAsyncAction hook
645
269
  */
646
270
  interface UseAsyncActionOptions {
647
- /** Timeout in milliseconds after which the blocker will be automatically removed */
271
+ /** Blocker lifetime in milliseconds; does not cancel or settle the operation */
648
272
  timeout?: number;
649
273
  /** Callback invoked when the blocker is automatically removed due to timeout */
650
274
  onTimeout?: (blockerId: string) => void;
651
275
  }
652
276
  /**
653
- * Wraps async functions with automatic UI blocking during execution.
654
- *
655
- * Creates a wrapper function that automatically:
656
- * 1. Adds a blocker before executing the async function
657
- * 2. Executes the async function
658
- * 3. Removes the blocker after completion (success or failure)
659
- *
660
- * Each invocation generates a unique blocker ID (actionId-counter), allowing
661
- * multiple concurrent executions of the same action without ID conflicts.
662
- * The blocker is removed even if the async function throws an error.
663
- *
664
- * Works with both the global store and isolated provider instances (via UIBlockingProvider).
665
- *
666
- * @template T - The return type of the async function being wrapped
667
- * @param actionId - Base identifier for the action. Used in blocker ID and default reason.
668
- * Example: "saveForm", "fetchData", "submitOrder"
669
- * @param scope - Scope(s) to block during execution. Can be a single scope or array.
670
- * Defaults to "global" if not specified.
671
- * @param options - Optional configuration
672
- * @param options.timeout - Timeout in milliseconds. Blocker auto-removes after this duration.
673
- * @param options.onTimeout - Callback invoked when blocker times out before async completes
674
- *
675
- * @returns Wrapper function that accepts an async function and returns a Promise with the same type
676
- *
677
- * @example
678
- * Basic form submission
679
- * ```tsx
680
- * function MyForm() {
681
- * const executeWithBlocking = useAsyncAction('saveForm', 'form');
682
- *
683
- * const handleSubmit = async (e) => {
684
- * e.preventDefault();
685
- *
686
- * await executeWithBlocking(async () => {
687
- * const response = await fetch('/api/save', {
688
- * method: 'POST',
689
- * body: JSON.stringify(formData)
690
- * });
691
- * return response.json();
692
- * });
693
- *
694
- * alert('Form saved!');
695
- * };
696
- *
697
- * return <form onSubmit={handleSubmit}>...</form>;
698
- * }
699
- * ```
700
- *
701
- * @example
702
- * With timeout and error handling
703
- * ```tsx
704
- * const executeWithBlocking = useAsyncAction(
705
- * 'fetchData',
706
- * ['global', 'data'],
707
- * {
708
- * timeout: 30000, // 30 seconds
709
- * onTimeout: (blockerId) => {
710
- * console.warn(`Operation ${blockerId} timed out`);
711
- * }
712
- * }
713
- * );
714
- *
715
- * const loadData = async () => {
716
- * try {
717
- * const data = await executeWithBlocking(async () => {
718
- * return await fetchDataFromAPI();
719
- * });
720
- * setData(data);
721
- * } catch (error) {
722
- * console.error('Failed to load data:', error);
723
- * // Blocker is automatically removed even on error
724
- * }
725
- * };
726
- * ```
727
- *
728
- * @example
729
- * Multiple concurrent actions
730
- * ```tsx
731
- * function FileUploader() {
732
- * const uploadWithBlocking = useAsyncAction('upload', 'uploads');
733
- *
734
- * const uploadFiles = async (files) => {
735
- * // Each upload gets unique blocker ID (upload-1, upload-2, etc.)
736
- * const promises = files.map(file =>
737
- * uploadWithBlocking(() => uploadFile(file))
738
- * );
739
- *
740
- * await Promise.all(promises);
741
- * };
742
- *
743
- * return <...>;
744
- * }
745
- * ```
746
- *
747
- * @example
748
- * Type-safe return value
749
- * ```tsx
750
- * interface UserData {
751
- * id: string;
752
- * name: string;
753
- * }
754
- *
755
- * function UserProfile() {
756
- * const executeWithBlocking = useAsyncAction<UserData>('fetchUser', 'user');
757
- *
758
- * const loadUser = async (userId: string) => {
759
- * const userData = await executeWithBlocking(async () => {
760
- * const response = await fetch(`/api/users/${userId}`);
761
- * return response.json(); // TypeScript knows this is UserData
762
- * });
763
- *
764
- * // userData is typed as UserData
765
- * console.log(userData.name);
766
- * };
767
- *
768
- * return <...>;
769
- * }
770
- * ```
771
- *
772
- * @see {@link useBlocker} for manual blocker management
773
- * @see {@link useIsBlocked} to check blocking state
774
- * @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.
775
280
  *
776
281
  * @public
777
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
778
286
  */
779
287
  declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
780
288
 
@@ -801,157 +309,30 @@ declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T,
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 };