@okyrychenko-dev/react-action-guard 1.0.4 → 1.0.5

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
@@ -232,6 +232,10 @@ Wraps an async function with automatic blocking/unblocking.
232
232
 
233
233
  **Returns:** `(asyncFn: () => Promise<T>) => Promise<T>` - Function wrapper
234
234
 
235
+ Each execution receives a unique blocker ID within the resolved store, including concurrent
236
+ executions from different hook instances. Separate `UIBlockingProvider` stores allocate IDs
237
+ independently.
238
+
235
239
  **Example:**
236
240
 
237
241
  ```jsx
@@ -355,6 +359,9 @@ Use it when blocking is driven by time windows rather than user-triggered async
355
359
  - `onScheduleEnd?: () => void` - Callback when blocking ends
356
360
  - Plus all `BlockerConfig` properties (scope, reason, priority)
357
361
 
362
+ Changing `schedule.start`, `schedule.end`, or `schedule.duration` on rerender cancels the previous
363
+ timers and schedules the blocker using the latest values.
364
+
358
365
  **Example:**
359
366
 
360
367
  ```jsx
@@ -390,10 +397,12 @@ Use this when polling is an acceptable tradeoff and the condition is not natural
390
397
  - `config: ConditionalBlockerConfig<TState>`
391
398
  - `scope: string | string[]` - Required scope(s) to block
392
399
  - `condition: (state?: TState) => boolean` - Function that determines if blocking should be active
393
- - `checkInterval?: number` - How often to check the condition in ms (default: 1000)
400
+ - `checkInterval?: number` - How often to check the condition in ms (default: 1000; non-positive values use the default)
394
401
  - `state?: TState` - Optional state to pass to the condition function
395
402
  - Plus all other `BlockerConfig` properties (reason, priority)
396
403
 
404
+ Changing `checkInterval` on rerender restarts condition checks using the latest interval.
405
+
397
406
  **Example:**
398
407
 
399
408
  ```jsx
@@ -626,6 +635,7 @@ configureMiddleware([
626
635
  #### Logger Middleware
627
636
 
628
637
  Log blocker lifecycle events to the console for debugging.
638
+ The middleware is not disabled automatically in production; register it conditionally when needed.
629
639
 
630
640
  ```jsx
631
641
  import { configureMiddleware, loggerMiddleware } from "@okyrychenko-dev/react-action-guard";
package/dist/index.cjs CHANGED
@@ -36,6 +36,16 @@ function createMiddlewareContext(action, blockerId, config, prevState) {
36
36
  ...prevState && { prevState }
37
37
  };
38
38
  }
39
+ function toPublicBlockerConfig(blocker) {
40
+ return {
41
+ scope: blocker.scope,
42
+ reason: blocker.reason,
43
+ priority: blocker.priority,
44
+ timestamp: blocker.timestamp,
45
+ timeout: blocker.timeout,
46
+ onTimeout: blocker.onTimeout
47
+ };
48
+ }
39
49
  var createUIBlockingActions = (set, get) => ({
40
50
  // State
41
51
  activeBlockers: /* @__PURE__ */ new Map(),
@@ -106,14 +116,15 @@ var createUIBlockingActions = (set, get) => ({
106
116
  let timeoutId;
107
117
  if (config.timeout && config.timeout > 0) {
108
118
  timeoutId = setTimeout(() => {
109
- if (get().activeBlockers.has(id)) {
110
- config.onTimeout?.(id);
119
+ const blocker = get().activeBlockers.get(id);
120
+ if (blocker) {
121
+ blocker.onTimeout?.(id);
111
122
  void get().runMiddlewares(
112
123
  createMiddlewareContext("timeout", id, {
113
- scope: config.scope,
114
- reason: config.reason,
115
- priority: config.priority,
116
- timeout: config.timeout
124
+ scope: blocker.scope,
125
+ reason: blocker.reason,
126
+ priority: blocker.priority,
127
+ timeout: blocker.timeout
117
128
  })
118
129
  );
119
130
  get().removeBlocker(id);
@@ -184,16 +195,16 @@ var createUIBlockingActions = (set, get) => ({
184
195
  }
185
196
  if (config.timeout && config.timeout > 0) {
186
197
  const newTimeout = config.timeout;
187
- const newOnTimeout = config.onTimeout ?? existingBlocker.onTimeout;
188
198
  timeoutId = setTimeout(() => {
189
- if (get().activeBlockers.has(id)) {
190
- newOnTimeout?.(id);
199
+ const blocker = get().activeBlockers.get(id);
200
+ if (blocker) {
201
+ blocker.onTimeout?.(id);
191
202
  void get().runMiddlewares(
192
203
  createMiddlewareContext("timeout", id, {
193
- scope: config.scope ?? existingBlocker.scope,
194
- reason: config.reason ?? existingBlocker.reason,
195
- priority: config.priority ?? existingBlocker.priority,
196
- timeout: newTimeout
204
+ scope: blocker.scope,
205
+ reason: blocker.reason,
206
+ priority: blocker.priority,
207
+ timeout: blocker.timeout
197
208
  })
198
209
  );
199
210
  get().removeBlocker(id);
@@ -271,7 +282,8 @@ var createUIBlockingActions = (set, get) => ({
271
282
  return { activeBlockers: newBlockers };
272
283
  });
273
284
  if (prevBlocker) {
274
- void get().runMiddlewares(createMiddlewareContext("remove", id, prevBlocker, prevBlocker));
285
+ const publicConfig = toPublicBlockerConfig(prevBlocker);
286
+ void get().runMiddlewares(createMiddlewareContext("remove", id, publicConfig, publicConfig));
275
287
  }
276
288
  },
277
289
  /**
@@ -503,16 +515,36 @@ function useResolvedValue(selector) {
503
515
  const store = useResolvedStoreApi();
504
516
  return zustand.useStore(store, shallow.useShallow(selector));
505
517
  }
518
+
519
+ // src/store/blockerId.utils.ts
520
+ var storeSequences = /* @__PURE__ */ new WeakMap();
521
+ function allocateBlockerId(store, prefix) {
522
+ let sequence = storeSequences.get(store) ?? 0;
523
+ let blockerId;
524
+ do {
525
+ sequence += 1;
526
+ blockerId = `${prefix}-${String(sequence)}`;
527
+ } while (store.getState().activeBlockers.has(blockerId));
528
+ storeSequences.set(store, sequence);
529
+ return blockerId;
530
+ }
531
+
532
+ // src/hooks/useBlockerIdAllocator/useBlockerIdAllocator.ts
533
+ function useBlockerIdAllocator() {
534
+ const store = useResolvedStoreApi();
535
+ return react.useCallback((prefix) => allocateBlockerId(store, prefix), [store]);
536
+ }
537
+
538
+ // src/hooks/useAsyncAction/useAsyncAction.ts
506
539
  function useAsyncAction(actionId, scope, options) {
540
+ const allocateBlockerId2 = useBlockerIdAllocator();
507
541
  const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
508
542
  addBlocker: state.addBlocker,
509
543
  removeBlocker: state.removeBlocker
510
544
  }));
511
- const counterRef = react.useRef(0);
512
545
  const executeWithBlocking = react.useCallback(
513
546
  async (asyncFn) => {
514
- counterRef.current += 1;
515
- const blockerId = `${actionId}-${String(counterRef.current)}`;
547
+ const blockerId = allocateBlockerId2(actionId);
516
548
  try {
517
549
  addBlocker(blockerId, {
518
550
  scope,
@@ -526,7 +558,15 @@ function useAsyncAction(actionId, scope, options) {
526
558
  removeBlocker(blockerId);
527
559
  }
528
560
  },
529
- [actionId, scope, options?.timeout, options?.onTimeout, addBlocker, removeBlocker]
561
+ [
562
+ actionId,
563
+ scope,
564
+ options?.timeout,
565
+ options?.onTimeout,
566
+ allocateBlockerId2,
567
+ addBlocker,
568
+ removeBlocker
569
+ ]
530
570
  );
531
571
  return executeWithBlocking;
532
572
  }
@@ -571,12 +611,24 @@ function useBlocker(blockerId, config, isActive = true) {
571
611
  updateBlocker: state.updateBlocker
572
612
  }));
573
613
  const lastConfigRef = react.useRef(null);
614
+ const onTimeoutRef = react.useRef(config.onTimeout);
615
+ onTimeoutRef.current = config.onTimeout;
616
+ const handleTimeout = react.useCallback((id) => {
617
+ onTimeoutRef.current?.(id);
618
+ }, []);
619
+ const storeConfig = react.useMemo(
620
+ () => ({
621
+ ...config,
622
+ onTimeout: config.onTimeout ? handleTimeout : void 0
623
+ }),
624
+ [config, handleTimeout]
625
+ );
574
626
  react.useEffect(() => {
575
627
  if (!isActive || !blockerId) {
576
628
  return;
577
629
  }
578
- addBlocker(blockerId, config);
579
- lastConfigRef.current = config;
630
+ addBlocker(blockerId, storeConfig);
631
+ lastConfigRef.current = storeConfig;
580
632
  return () => {
581
633
  lastConfigRef.current = null;
582
634
  removeBlocker(blockerId);
@@ -586,12 +638,12 @@ function useBlocker(blockerId, config, isActive = true) {
586
638
  if (!isActive || !blockerId) {
587
639
  return;
588
640
  }
589
- if (lastConfigRef.current !== null && areBlockerConfigsEqual(lastConfigRef.current, config)) {
641
+ if (lastConfigRef.current !== null && areBlockerConfigsEqual(lastConfigRef.current, storeConfig)) {
590
642
  return;
591
643
  }
592
- updateBlocker(blockerId, config);
593
- lastConfigRef.current = config;
594
- }, [blockerId, config, isActive, updateBlocker]);
644
+ updateBlocker(blockerId, storeConfig);
645
+ lastConfigRef.current = storeConfig;
646
+ }, [blockerId, storeConfig, isActive, updateBlocker]);
595
647
  }
596
648
  function useBlockingInfo(scope = DEFAULT_SCOPE) {
597
649
  const store = useResolvedStoreApi();
@@ -609,6 +661,7 @@ function useConfigRef(config) {
609
661
  }
610
662
 
611
663
  // src/hooks/useConditionalBlocker/useConditionalBlocker.ts
664
+ var DEFAULT_CHECK_INTERVAL = 1e3;
612
665
  function useConditionalBlocker(blockerId, config) {
613
666
  const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
614
667
  addBlocker: state.addBlocker,
@@ -617,6 +670,7 @@ function useConditionalBlocker(blockerId, config) {
617
670
  const intervalRef = react.useRef(null);
618
671
  const isBlockedRef = react.useRef(false);
619
672
  const configRef = useConfigRef(config);
673
+ const checkInterval = config.checkInterval !== void 0 && config.checkInterval > 0 ? config.checkInterval : DEFAULT_CHECK_INTERVAL;
620
674
  const checkCondition = react.useCallback(() => {
621
675
  const currentConfig = configRef.current;
622
676
  const shouldBlock = currentConfig.condition(currentConfig.state);
@@ -632,8 +686,7 @@ function useConditionalBlocker(blockerId, config) {
632
686
  }, [blockerId, addBlocker, removeBlocker, configRef]);
633
687
  react.useEffect(() => {
634
688
  checkCondition();
635
- const interval = configRef.current.checkInterval ?? 1e3;
636
- intervalRef.current = setInterval(checkCondition, interval);
689
+ intervalRef.current = setInterval(checkCondition, checkInterval);
637
690
  return () => {
638
691
  if (intervalRef.current) {
639
692
  clearInterval(intervalRef.current);
@@ -644,7 +697,7 @@ function useConditionalBlocker(blockerId, config) {
644
697
  isBlockedRef.current = false;
645
698
  }
646
699
  };
647
- }, [checkCondition, blockerId, removeBlocker]);
700
+ }, [checkCondition, blockerId, removeBlocker, checkInterval]);
648
701
  }
649
702
  function useConfirmableBlocker(blockerId, config) {
650
703
  const [isDialogOpen, setIsDialogOpen] = react.useState(false);
@@ -738,6 +791,9 @@ function useScheduledBlocker(blockerId, config) {
738
791
  }));
739
792
  const timeoutsRef = react.useRef({});
740
793
  const configRef = useConfigRef(config);
794
+ const scheduleStart = parseDate(config.schedule.start);
795
+ const scheduleEnd = config.schedule.end === void 0 ? void 0 : parseDate(config.schedule.end);
796
+ const scheduleDuration = config.schedule.duration;
741
797
  const cleanup = react.useCallback(() => {
742
798
  if (timeoutsRef.current.start) {
743
799
  clearTimeout(timeoutsRef.current.start);
@@ -755,7 +811,7 @@ function useScheduledBlocker(blockerId, config) {
755
811
  if (!isValidTimestamp(startTime)) {
756
812
  return;
757
813
  }
758
- const scheduleEnd = () => {
814
+ const scheduleEnd2 = () => {
759
815
  const endTime = calculateEndTime(currentConfig.schedule, startTime);
760
816
  if (!endTime) {
761
817
  return;
@@ -772,7 +828,7 @@ function useScheduledBlocker(blockerId, config) {
772
828
  const cfg = configRef.current;
773
829
  addBlocker(blockerId, createBlockerConfig(cfg));
774
830
  cfg.onScheduleStart?.();
775
- scheduleEnd();
831
+ scheduleEnd2();
776
832
  };
777
833
  if (startTime > now) {
778
834
  const delay = startTime - now;
@@ -792,7 +848,16 @@ function useScheduledBlocker(blockerId, config) {
792
848
  cleanup();
793
849
  removeBlocker(blockerId);
794
850
  };
795
- }, [blockerId, addBlocker, removeBlocker, cleanup]);
851
+ }, [
852
+ blockerId,
853
+ addBlocker,
854
+ removeBlocker,
855
+ cleanup,
856
+ scheduleStart,
857
+ scheduleEnd,
858
+ scheduleDuration,
859
+ configRef
860
+ ]);
796
861
  }
797
862
 
798
863
  // src/middleware/analyticsMiddleware.utils.ts