@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 +11 -1
- package/dist/index.cjs +95 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -21
- package/dist/index.d.ts +21 -21
- package/dist/index.js +96 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
110
|
-
|
|
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:
|
|
114
|
-
reason:
|
|
115
|
-
priority:
|
|
116
|
-
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
|
-
|
|
190
|
-
|
|
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:
|
|
194
|
-
reason:
|
|
195
|
-
priority:
|
|
196
|
-
timeout:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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,
|
|
579
|
-
lastConfigRef.current =
|
|
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,
|
|
641
|
+
if (lastConfigRef.current !== null && areBlockerConfigsEqual(lastConfigRef.current, storeConfig)) {
|
|
590
642
|
return;
|
|
591
643
|
}
|
|
592
|
-
updateBlocker(blockerId,
|
|
593
|
-
lastConfigRef.current =
|
|
594
|
-
}, [blockerId,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|