@okyrychenko-dev/react-action-guard 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,8 +36,8 @@ pnpm add @okyrychenko-dev/react-action-guard zustand
36
36
 
37
37
  This package requires the following peer dependencies:
38
38
 
39
- - [React](https://react.dev/) ^17.0.0 || ^18.0.0 || ^19.0.0
40
- - [Zustand](https://zustand-demo.pmnd.rs/) ^4.5.7 || ^5.0.0 - State management library
39
+ - [React](https://react.dev/) ^18.0.0 || ^19.0.0
40
+ - [Zustand](https://zustand-demo.pmnd.rs/) ^5.0.0 - State management library
41
41
 
42
42
  ## Quick Start
43
43
 
package/dist/index.cjs CHANGED
@@ -24,16 +24,18 @@ function normalizeScopeToArray(scope) {
24
24
  }
25
25
 
26
26
  // src/store/uiBlockingStore.actions.ts
27
- var normalizePriority = (priority, fallback = DEFAULT_PRIORITY) => {
27
+ function normalizePriority(priority, fallback = DEFAULT_PRIORITY) {
28
28
  return priority !== void 0 ? Math.max(0, priority) : fallback;
29
- };
30
- var createMiddlewareContext = (action, blockerId, config, prevState) => ({
31
- action,
32
- blockerId,
33
- config,
34
- timestamp: Date.now(),
35
- ...prevState && { prevState }
36
- });
29
+ }
30
+ function createMiddlewareContext(action, blockerId, config, prevState) {
31
+ return {
32
+ action,
33
+ blockerId,
34
+ config,
35
+ timestamp: Date.now(),
36
+ ...prevState && { prevState }
37
+ };
38
+ }
37
39
  var createUIBlockingActions = (set, get) => ({
38
40
  // State
39
41
  activeBlockers: /* @__PURE__ */ new Map(),
@@ -64,11 +66,38 @@ var createUIBlockingActions = (set, get) => ({
64
66
  },
65
67
  // Actions
66
68
  /**
67
- * Add a blocker to the store
69
+ * Adds a new blocker or overwrites an existing one.
70
+ *
71
+ * If a blocker with the same ID already exists, it will be replaced and any
72
+ * existing timeout will be cleared. The blocker is active immediately after
73
+ * being added. Triggers middleware with 'add' action.
74
+ *
75
+ * @param id - Unique identifier for the blocker. Must be unique within the store.
76
+ * @param config - Blocker configuration. All fields are optional with defaults applied.
68
77
  *
69
- * @param id - Unique blocker identifier
70
- * @param config - Optional blocker configuration
78
+ * @example
79
+ * Basic usage
80
+ * ```ts
81
+ * store.getState().addBlocker('save-operation', {
82
+ * scope: 'form',
83
+ * reason: 'Saving data...',
84
+ * priority: 70
85
+ * });
86
+ * ```
71
87
  *
88
+ * @example
89
+ * With timeout
90
+ * ```ts
91
+ * store.getState().addBlocker('api-call', {
92
+ * scope: 'global',
93
+ * reason: 'Loading...',
94
+ * timeout: 30000, // Auto-remove after 30 seconds
95
+ * onTimeout: (id) => console.warn(`${id} timed out`)
96
+ * });
97
+ * ```
98
+ *
99
+ * @see {@link removeBlocker} to remove a blocker
100
+ * @see {@link updateBlocker} to update an existing blocker
72
101
  */
73
102
  addBlocker: (id, config = {}) => {
74
103
  const existingBlocker = get().activeBlockers.get(id);
@@ -116,11 +145,31 @@ var createUIBlockingActions = (set, get) => ({
116
145
  );
117
146
  },
118
147
  /**
119
- * Update blocker metadata. If timeout is updated, the timer will be restarted.
148
+ * Updates an existing blocker or creates it if it doesn't exist.
149
+ *
150
+ * Merges the provided configuration with existing blocker data. If the timeout
151
+ * is changed, the timeout timer is restarted. Triggers middleware with 'update' action.
152
+ *
153
+ * @param id - Identifier of the blocker to update
154
+ * @param config - Partial configuration to merge with existing blocker data
120
155
  *
121
- * @param id - Unique blocker identifier
122
- * @param config - Partial blocker configuration to merge
156
+ * @example
157
+ * Update blocker reason
158
+ * ```ts
159
+ * store.getState().updateBlocker('save', {
160
+ * reason: 'Saving additional data...'
161
+ * });
162
+ * ```
123
163
  *
164
+ * @example
165
+ * Extend timeout
166
+ * ```ts
167
+ * store.getState().updateBlocker('api-call', {
168
+ * timeout: 60000 // Extend to 60 seconds
169
+ * });
170
+ * ```
171
+ *
172
+ * @see {@link addBlocker} to create a new blocker
124
173
  */
125
174
  updateBlocker: (id, config = {}) => {
126
175
  const existingBlocker = get().activeBlockers.get(id);
@@ -194,10 +243,23 @@ var createUIBlockingActions = (set, get) => ({
194
243
  );
195
244
  },
196
245
  /**
197
- * Remove a blocker from the store
246
+ * Removes a blocker from the store.
247
+ *
248
+ * Clears any associated timeout and triggers middleware with 'remove' action.
249
+ * Safe to call even if the blocker doesn't exist (no-op in that case).
198
250
  *
199
- * @param id - Blocker identifier to remove
251
+ * @param id - Identifier of the blocker to remove
200
252
  *
253
+ * @example
254
+ * ```ts
255
+ * // Add and remove a blocker
256
+ * store.getState().addBlocker('temp', { scope: 'form' });
257
+ * // ... later
258
+ * store.getState().removeBlocker('temp');
259
+ * ```
260
+ *
261
+ * @see {@link addBlocker} to add a blocker
262
+ * @see {@link clearAllBlockers} to remove all blockers at once
201
263
  */
202
264
  removeBlocker: (id) => {
203
265
  const prevBlocker = get().activeBlockers.get(id);
@@ -214,11 +276,30 @@ var createUIBlockingActions = (set, get) => ({
214
276
  }
215
277
  },
216
278
  /**
217
- * Check if a scope is currently blocked
279
+ * Checks if one or more scopes are currently blocked.
280
+ *
281
+ * Returns true if ANY blocker affects the specified scope(s). Global scope
282
+ * blockers will cause this to return true for any scope check.
283
+ *
284
+ * @param scope - Scope(s) to check. Defaults to "global" if not specified.
285
+ * Can be a single scope string or array of scopes.
286
+ * @returns true if any of the specified scopes are currently blocked
287
+ *
288
+ * @example
289
+ * Single scope check
290
+ * ```ts
291
+ * const isFormBlocked = store.getState().isBlocked('form');
292
+ * console.log(isFormBlocked); // true or false
293
+ * ```
218
294
  *
219
- * @param scope - Scope or array of scopes to check (default: "global")
220
- * @returns True if any of the scopes are blocked
295
+ * @example
296
+ * Multiple scopes check
297
+ * ```ts
298
+ * const isBlocked = store.getState().isBlocked(['form', 'navigation']);
299
+ * // Returns true if either 'form' OR 'navigation' has active blockers
300
+ * ```
221
301
  *
302
+ * @see {@link getBlockingInfo} for detailed blocker information
222
303
  */
223
304
  isBlocked: (scope = DEFAULT_SCOPE) => {
224
305
  const { activeBlockers } = get();
@@ -235,11 +316,37 @@ var createUIBlockingActions = (set, get) => ({
235
316
  return false;
236
317
  },
237
318
  /**
238
- * Get detailed blocking information for a specific scope
319
+ * Gets detailed information about all blockers affecting a specific scope.
320
+ *
321
+ * Returns an array of blocker objects sorted by priority (highest first).
322
+ * Includes global blockers and scope-specific blockers. Each blocker object
323
+ * contains id, reason, priority, scope, timestamp, and timeout information.
239
324
  *
240
325
  * @param scope - Scope to get blocking information for
241
- * @returns Array of blockers affecting the scope, sorted by priority (highest first)
326
+ * @returns Array of {@link BlockerInfo} objects, sorted by priority descending
327
+ *
328
+ * @example
329
+ * Display blocker details
330
+ * ```ts
331
+ * const blockers = store.getState().getBlockingInfo('form');
332
+ * blockers.forEach(blocker => {
333
+ * console.log(`${blocker.id}: ${blocker.reason}`);
334
+ * console.log(`Priority: ${blocker.priority}`);
335
+ * });
336
+ * ```
337
+ *
338
+ * @example
339
+ * Get highest priority blocker
340
+ * ```ts
341
+ * const blockers = store.getState().getBlockingInfo('navigation');
342
+ * const topBlocker = blockers[0]; // Highest priority
343
+ * if (topBlocker) {
344
+ * console.log(`Top blocker: ${topBlocker.reason}`);
345
+ * }
346
+ * ```
242
347
  *
348
+ * @see {@link isBlocked} for a simple boolean check
349
+ * @see {@link BlockerInfo} for the structure of returned objects
243
350
  */
244
351
  getBlockingInfo: (scope) => {
245
352
  const { activeBlockers } = get();
@@ -253,7 +360,18 @@ var createUIBlockingActions = (set, get) => ({
253
360
  return blockers.sort((a, b) => b.priority - a.priority);
254
361
  },
255
362
  /**
256
- * Clear all blockers from the store
363
+ * Removes all active blockers from the store.
364
+ *
365
+ * Clears all timeouts and triggers middleware with 'clear' action.
366
+ * Useful for resetting the blocking state.
367
+ *
368
+ * @example
369
+ * ```ts
370
+ * // Clear all blockers on logout
371
+ * store.getState().clearAllBlockers();
372
+ * ```
373
+ *
374
+ * @see {@link clearBlockersForScope} to clear blockers for a specific scope
257
375
  */
258
376
  clearAllBlockers: () => {
259
377
  const { activeBlockers } = get();
@@ -274,10 +392,28 @@ var createUIBlockingActions = (set, get) => ({
274
392
  }
275
393
  },
276
394
  /**
277
- * Clear all blockers for a specific scope
395
+ * Removes all blockers that affect a specific scope.
396
+ *
397
+ * Clears timeouts for removed blockers and triggers middleware with
398
+ * 'clear_scope' action. Global scope blockers are NOT cleared by this method.
278
399
  *
279
400
  * @param scope - Scope to clear blockers for
280
401
  *
402
+ * @example
403
+ * Clear form blockers
404
+ * ```ts
405
+ * // Clear all blockers affecting 'form' scope
406
+ * store.getState().clearBlockersForScope('form');
407
+ * ```
408
+ *
409
+ * @example
410
+ * Clear on navigation
411
+ * ```ts
412
+ * // Clear navigation blockers when user navigates away
413
+ * store.getState().clearBlockersForScope('navigation');
414
+ * ```
415
+ *
416
+ * @see {@link clearAllBlockers} to clear all blockers
281
417
  */
282
418
  clearBlockersForScope: (scope) => {
283
419
  const { activeBlockers } = get();
@@ -364,7 +500,7 @@ function useResolvedStoreWithSelector(selector) {
364
500
  }
365
501
 
366
502
  // src/hooks/useAsyncAction/useAsyncAction.ts
367
- var useAsyncAction = (actionId, scope, options) => {
503
+ function useAsyncAction(actionId, scope, options) {
368
504
  const { addBlocker, removeBlocker } = useResolvedStoreWithSelector((state) => ({
369
505
  addBlocker: state.addBlocker,
370
506
  removeBlocker: state.removeBlocker
@@ -390,8 +526,8 @@ var useAsyncAction = (actionId, scope, options) => {
390
526
  [actionId, scope, options?.timeout, options?.onTimeout, addBlocker, removeBlocker]
391
527
  );
392
528
  return executeWithBlocking;
393
- };
394
- var useBlocker = (blockerId, config, isActive = true) => {
529
+ }
530
+ function useBlocker(blockerId, config, isActive = true) {
395
531
  const { addBlocker, removeBlocker } = useResolvedStoreWithSelector((state) => ({
396
532
  addBlocker: state.addBlocker,
397
533
  removeBlocker: state.removeBlocker
@@ -405,7 +541,7 @@ var useBlocker = (blockerId, config, isActive = true) => {
405
541
  removeBlocker(blockerId);
406
542
  };
407
543
  }, [blockerId, isActive, addBlocker, removeBlocker]);
408
- };
544
+ }
409
545
 
410
546
  // src/hooks/useBlocker/useBlocker.utils.ts
411
547
  function createBlockerConfig(config) {
@@ -415,13 +551,13 @@ function createBlockerConfig(config) {
415
551
  priority: config.priority
416
552
  };
417
553
  }
418
- var useBlockingInfo = (scope = DEFAULT_SCOPE) => {
554
+ function useBlockingInfo(scope = DEFAULT_SCOPE) {
419
555
  const store = useResolvedStore();
420
556
  const activeBlockers = zustand.useStore(store, (state) => state.activeBlockers);
421
557
  return react.useMemo(() => {
422
558
  return store.getState().getBlockingInfo(scope);
423
559
  }, [activeBlockers, scope, store]);
424
- };
560
+ }
425
561
  function useConfigRef(config) {
426
562
  const configRef = react.useRef(config);
427
563
  react.useEffect(() => {
@@ -431,7 +567,7 @@ function useConfigRef(config) {
431
567
  }
432
568
 
433
569
  // src/hooks/useConditionalBlocker/useConditionalBlocker.ts
434
- var useConditionalBlocker = (blockerId, config) => {
570
+ function useConditionalBlocker(blockerId, config) {
435
571
  const { addBlocker, removeBlocker } = useResolvedStoreWithSelector((state) => ({
436
572
  addBlocker: state.addBlocker,
437
573
  removeBlocker: state.removeBlocker
@@ -467,8 +603,8 @@ var useConditionalBlocker = (blockerId, config) => {
467
603
  }
468
604
  };
469
605
  }, [checkCondition, blockerId, removeBlocker]);
470
- };
471
- var useConfirmableBlocker = (blockerId, config) => {
606
+ }
607
+ function useConfirmableBlocker(blockerId, config) {
472
608
  const [isDialogOpen, setIsDialogOpen] = react.useState(false);
473
609
  const [isExecuting, setIsExecuting] = react.useState(false);
474
610
  useBlocker(
@@ -511,12 +647,12 @@ var useConfirmableBlocker = (blockerId, config) => {
511
647
  onConfirm,
512
648
  onCancel
513
649
  };
514
- };
650
+ }
515
651
 
516
652
  // src/hooks/useIsBlocked/useIsBlocked.ts
517
- var useIsBlocked = (scope) => {
653
+ function useIsBlocked(scope) {
518
654
  return useResolvedStoreWithSelector((state) => state.isBlocked(scope));
519
- };
655
+ }
520
656
 
521
657
  // src/hooks/useScheduledBlocker/useScheduledBlocker.utils.ts
522
658
  var MAX_TIMEOUT_MS = 2147483647;
@@ -556,7 +692,7 @@ function isScheduleInPast(startTime, endTime, now) {
556
692
  }
557
693
 
558
694
  // src/hooks/useScheduledBlocker/useScheduledBlocker.ts
559
- var useScheduledBlocker = (blockerId, config) => {
695
+ function useScheduledBlocker(blockerId, config) {
560
696
  const { addBlocker, removeBlocker } = useResolvedStoreWithSelector((state) => ({
561
697
  addBlocker: state.addBlocker,
562
698
  removeBlocker: state.removeBlocker
@@ -622,13 +758,13 @@ var useScheduledBlocker = (blockerId, config) => {
622
758
  removeBlocker(blockerId);
623
759
  };
624
760
  }, [blockerId, addBlocker, removeBlocker, cleanup]);
625
- };
761
+ }
626
762
 
627
763
  // src/middleware/analyticsMiddleware.utils.ts
628
- var capitalizeFirst = (str) => {
764
+ function capitalizeFirst(str) {
629
765
  return str.charAt(0).toUpperCase() + str.slice(1);
630
- };
631
- var buildEventData = (context) => {
766
+ }
767
+ function buildEventData(context) {
632
768
  const eventData = {
633
769
  blocker_id: context.blockerId
634
770
  };
@@ -643,33 +779,33 @@ var buildEventData = (context) => {
643
779
  eventData.priority = priority;
644
780
  }
645
781
  return eventData;
646
- };
647
- var trackGoogleAnalytics = (action, eventData) => {
782
+ }
783
+ function trackGoogleAnalytics(action, eventData) {
648
784
  if (window.gtag) {
649
785
  window.gtag("event", `ui_blocking_${action}`, eventData);
650
786
  }
651
- };
652
- var trackMixpanel = (action, eventData) => {
787
+ }
788
+ function trackMixpanel(action, eventData) {
653
789
  if (window.mixpanel) {
654
790
  const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
655
791
  window.mixpanel.track(eventName, eventData);
656
792
  }
657
- };
658
- var trackAmplitude = (action, eventData) => {
793
+ }
794
+ function trackAmplitude(action, eventData) {
659
795
  if (window.amplitude) {
660
796
  const eventName = `UI Blocking ${action.split("_").map(capitalizeFirst).join(" ")}`;
661
797
  window.amplitude.track(eventName, eventData);
662
798
  }
663
- };
664
- var trackCustom = (trackFn, action, eventData) => {
799
+ }
800
+ function trackCustom(trackFn, action, eventData) {
665
801
  try {
666
802
  trackFn(`ui_blocking_${action}`, eventData);
667
803
  } catch {
668
804
  }
669
- };
805
+ }
670
806
 
671
807
  // src/middleware/analyticsMiddleware.ts
672
- var trackEvent = (config, context, eventData) => {
808
+ function trackEvent(config, context, eventData) {
673
809
  if ("provider" in config) {
674
810
  switch (config.provider) {
675
811
  case "ga":
@@ -686,13 +822,13 @@ var trackEvent = (config, context, eventData) => {
686
822
  if ("track" in config) {
687
823
  trackCustom(config.track, context.action, eventData);
688
824
  }
689
- };
690
- var createAnalyticsMiddleware = (config) => {
825
+ }
826
+ function createAnalyticsMiddleware(config) {
691
827
  return (context) => {
692
828
  const eventData = buildEventData(context);
693
829
  trackEvent(config, context, eventData);
694
830
  };
695
- };
831
+ }
696
832
 
697
833
  // src/middleware/configureMiddleware.ts
698
834
  function configureMiddleware(middlewares) {
@@ -704,7 +840,7 @@ function configureMiddleware(middlewares) {
704
840
  }
705
841
 
706
842
  // src/middleware/loggerMiddleware.utils.ts
707
- var getActionEmoji = (action) => {
843
+ function getActionEmoji(action) {
708
844
  switch (action) {
709
845
  case "add":
710
846
  return "\u2795";
@@ -721,8 +857,8 @@ var getActionEmoji = (action) => {
721
857
  default:
722
858
  return "\u2753";
723
859
  }
724
- };
725
- var extractConfigDetails = (config) => {
860
+ }
861
+ function extractConfigDetails(config) {
726
862
  if (!config) {
727
863
  return {};
728
864
  }
@@ -735,8 +871,8 @@ var extractConfigDetails = (config) => {
735
871
  details.priority = priority;
736
872
  }
737
873
  return details;
738
- };
739
- var formatLogData = (context) => {
874
+ }
875
+ function formatLogData(context) {
740
876
  const configDetails = extractConfigDetails(context.config);
741
877
  const logData = {};
742
878
  if (context.scope !== void 0) {
@@ -755,26 +891,26 @@ var formatLogData = (context) => {
755
891
  return configDetails;
756
892
  }
757
893
  return Object.keys(logData).length === 1 && logData.config ? configDetails : logData;
758
- };
894
+ }
759
895
 
760
896
  // src/middleware/loggerMiddleware.ts
761
897
  var LOG_PREFIX = "[UIBlocking]";
762
- var loggerMiddleware = (context) => {
898
+ function loggerMiddleware(context) {
763
899
  const emoji = getActionEmoji(context.action);
764
900
  const logData = formatLogData(context);
765
901
  console.log(LOG_PREFIX, emoji, context.blockerId, logData);
766
- };
902
+ }
767
903
 
768
904
  // src/middleware/performanceMiddleware.utils.ts
769
- var handleAddAction = (blockerId, timestamp, blockStartTimes) => {
905
+ function handleAddAction(blockerId, timestamp, blockStartTimes) {
770
906
  blockStartTimes.set(blockerId, timestamp);
771
- };
907
+ }
772
908
 
773
909
  // src/middleware/performanceMiddleware.ts
774
910
  var DEFAULT_SLOW_BLOCK_THRESHOLD = 3e3;
775
911
  var ACTION_ADD = "add";
776
912
  var ACTION_REMOVE = "remove";
777
- var handleRemoveAction = (context, blockStartTimes, slowBlockThreshold, onSlowBlock) => {
913
+ function handleRemoveAction(context, blockStartTimes, slowBlockThreshold, onSlowBlock) {
778
914
  const startTime = blockStartTimes.get(context.blockerId);
779
915
  if (startTime === void 0) {
780
916
  return;
@@ -787,8 +923,8 @@ var handleRemoveAction = (context, blockStartTimes, slowBlockThreshold, onSlowBl
787
923
  );
788
924
  onSlowBlock?.(context.blockerId, duration);
789
925
  }
790
- };
791
- var createPerformanceMiddleware = (config = {}) => {
926
+ }
927
+ function createPerformanceMiddleware(config = {}) {
792
928
  const { slowBlockThreshold = DEFAULT_SLOW_BLOCK_THRESHOLD, onSlowBlock } = config;
793
929
  const blockStartTimes = /* @__PURE__ */ new Map();
794
930
  return (context) => {
@@ -798,7 +934,7 @@ var createPerformanceMiddleware = (config = {}) => {
798
934
  handleRemoveAction(context, blockStartTimes, slowBlockThreshold, onSlowBlock);
799
935
  }
800
936
  };
801
- };
937
+ }
802
938
 
803
939
  // src/createTypedHooks.ts
804
940
  function createTypedHooks() {