@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/dist/index.d.cts
CHANGED
|
@@ -204,26 +204,6 @@ interface ShallowStoreBindings<StoreState> {
|
|
|
204
204
|
useStoreApi: StoreApi<StoreState>;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
208
|
-
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
209
|
-
[x: string]: unknown;
|
|
210
|
-
[x: number]: unknown;
|
|
211
|
-
[x: symbol]: unknown;
|
|
212
|
-
type: string;
|
|
213
|
-
}) | undefined): void;
|
|
214
|
-
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
215
|
-
[x: string]: unknown;
|
|
216
|
-
[x: number]: unknown;
|
|
217
|
-
[x: symbol]: unknown;
|
|
218
|
-
type: string;
|
|
219
|
-
}) | undefined): void;
|
|
220
|
-
devtools: {
|
|
221
|
-
cleanup: () => void;
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
declare function useUIBlockingStore(): UIBlockingStore;
|
|
225
|
-
declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
|
|
226
|
-
|
|
227
207
|
type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
|
|
228
208
|
type MiddlewareBlockerConfig = BlockerConfig;
|
|
229
209
|
interface MiddlewareContext {
|
|
@@ -379,7 +359,7 @@ declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): vo
|
|
|
379
359
|
*
|
|
380
360
|
* Logs every blocker action (add, remove, update, timeout, clear) with emoji
|
|
381
361
|
* indicators and formatted data. Useful for debugging blocking behavior during
|
|
382
|
-
* development.
|
|
362
|
+
* development. Register it conditionally when production logging is not desired.
|
|
383
363
|
*
|
|
384
364
|
* Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
|
|
385
365
|
*
|
|
@@ -798,6 +778,26 @@ interface UseAsyncActionOptions {
|
|
|
798
778
|
*/
|
|
799
779
|
declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
|
|
800
780
|
|
|
781
|
+
declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
782
|
+
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
783
|
+
[x: string]: unknown;
|
|
784
|
+
[x: number]: unknown;
|
|
785
|
+
[x: symbol]: unknown;
|
|
786
|
+
type: string;
|
|
787
|
+
}) | undefined): void;
|
|
788
|
+
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
789
|
+
[x: string]: unknown;
|
|
790
|
+
[x: number]: unknown;
|
|
791
|
+
[x: symbol]: unknown;
|
|
792
|
+
type: string;
|
|
793
|
+
}) | undefined): void;
|
|
794
|
+
devtools: {
|
|
795
|
+
cleanup: () => void;
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
declare function useUIBlockingStore(): UIBlockingStore;
|
|
799
|
+
declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
|
|
800
|
+
|
|
801
801
|
/**
|
|
802
802
|
* Automatically manages a UI blocker based on component lifecycle.
|
|
803
803
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -204,26 +204,6 @@ interface ShallowStoreBindings<StoreState> {
|
|
|
204
204
|
useStoreApi: StoreApi<StoreState>;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
208
|
-
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
209
|
-
[x: string]: unknown;
|
|
210
|
-
[x: number]: unknown;
|
|
211
|
-
[x: symbol]: unknown;
|
|
212
|
-
type: string;
|
|
213
|
-
}) | undefined): void;
|
|
214
|
-
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
215
|
-
[x: string]: unknown;
|
|
216
|
-
[x: number]: unknown;
|
|
217
|
-
[x: symbol]: unknown;
|
|
218
|
-
type: string;
|
|
219
|
-
}) | undefined): void;
|
|
220
|
-
devtools: {
|
|
221
|
-
cleanup: () => void;
|
|
222
|
-
};
|
|
223
|
-
};
|
|
224
|
-
declare function useUIBlockingStore(): UIBlockingStore;
|
|
225
|
-
declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
|
|
226
|
-
|
|
227
207
|
type BlockingAction = "add" | "update" | "remove" | "timeout" | "clear" | "clear_scope";
|
|
228
208
|
type MiddlewareBlockerConfig = BlockerConfig;
|
|
229
209
|
interface MiddlewareContext {
|
|
@@ -379,7 +359,7 @@ declare function configureMiddleware(middlewares: ReadonlyArray<Middleware>): vo
|
|
|
379
359
|
*
|
|
380
360
|
* Logs every blocker action (add, remove, update, timeout, clear) with emoji
|
|
381
361
|
* indicators and formatted data. Useful for debugging blocking behavior during
|
|
382
|
-
* development.
|
|
362
|
+
* development. Register it conditionally when production logging is not desired.
|
|
383
363
|
*
|
|
384
364
|
* Output format: `[UIBlocking] 🔒 blocker-id {scope: "form", reason: "..."}`
|
|
385
365
|
*
|
|
@@ -798,6 +778,26 @@ interface UseAsyncActionOptions {
|
|
|
798
778
|
*/
|
|
799
779
|
declare function useAsyncAction<T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>, options?: UseAsyncActionOptions): (asyncFn: () => Promise<T>) => Promise<T>;
|
|
800
780
|
|
|
781
|
+
declare const uiBlockingStoreApi: Omit<zustand.StoreApi<UIBlockingStore>, "setState" | "devtools"> & {
|
|
782
|
+
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
783
|
+
[x: string]: unknown;
|
|
784
|
+
[x: number]: unknown;
|
|
785
|
+
[x: symbol]: unknown;
|
|
786
|
+
type: string;
|
|
787
|
+
}) | undefined): void;
|
|
788
|
+
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
789
|
+
[x: string]: unknown;
|
|
790
|
+
[x: number]: unknown;
|
|
791
|
+
[x: symbol]: unknown;
|
|
792
|
+
type: string;
|
|
793
|
+
}) | undefined): void;
|
|
794
|
+
devtools: {
|
|
795
|
+
cleanup: () => void;
|
|
796
|
+
};
|
|
797
|
+
};
|
|
798
|
+
declare function useUIBlockingStore(): UIBlockingStore;
|
|
799
|
+
declare function useUIBlockingStore<T>(selector: (state: UIBlockingStore) => T, equalityFn?: (a: T, b: T) => boolean): T;
|
|
800
|
+
|
|
801
801
|
/**
|
|
802
802
|
* Automatically manages a UI blocker based on component lifecycle.
|
|
803
803
|
*
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import 'zustand/react/shallow';
|
|
|
4
4
|
import { jsx } from 'react/jsx-runtime';
|
|
5
5
|
import { useShallow, shallow } from 'zustand/shallow';
|
|
6
6
|
import { devtools } from 'zustand/middleware';
|
|
7
|
-
import { useRef,
|
|
7
|
+
import { useCallback, useRef, useMemo, useEffect, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
// src/context/UIBlockingContext.tsx
|
|
10
10
|
|
|
@@ -34,6 +34,16 @@ function createMiddlewareContext(action, blockerId, config, prevState) {
|
|
|
34
34
|
...prevState && { prevState }
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
+
function toPublicBlockerConfig(blocker) {
|
|
38
|
+
return {
|
|
39
|
+
scope: blocker.scope,
|
|
40
|
+
reason: blocker.reason,
|
|
41
|
+
priority: blocker.priority,
|
|
42
|
+
timestamp: blocker.timestamp,
|
|
43
|
+
timeout: blocker.timeout,
|
|
44
|
+
onTimeout: blocker.onTimeout
|
|
45
|
+
};
|
|
46
|
+
}
|
|
37
47
|
var createUIBlockingActions = (set, get) => ({
|
|
38
48
|
// State
|
|
39
49
|
activeBlockers: /* @__PURE__ */ new Map(),
|
|
@@ -104,14 +114,15 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
104
114
|
let timeoutId;
|
|
105
115
|
if (config.timeout && config.timeout > 0) {
|
|
106
116
|
timeoutId = setTimeout(() => {
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
const blocker = get().activeBlockers.get(id);
|
|
118
|
+
if (blocker) {
|
|
119
|
+
blocker.onTimeout?.(id);
|
|
109
120
|
void get().runMiddlewares(
|
|
110
121
|
createMiddlewareContext("timeout", id, {
|
|
111
|
-
scope:
|
|
112
|
-
reason:
|
|
113
|
-
priority:
|
|
114
|
-
timeout:
|
|
122
|
+
scope: blocker.scope,
|
|
123
|
+
reason: blocker.reason,
|
|
124
|
+
priority: blocker.priority,
|
|
125
|
+
timeout: blocker.timeout
|
|
115
126
|
})
|
|
116
127
|
);
|
|
117
128
|
get().removeBlocker(id);
|
|
@@ -182,16 +193,16 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
182
193
|
}
|
|
183
194
|
if (config.timeout && config.timeout > 0) {
|
|
184
195
|
const newTimeout = config.timeout;
|
|
185
|
-
const newOnTimeout = config.onTimeout ?? existingBlocker.onTimeout;
|
|
186
196
|
timeoutId = setTimeout(() => {
|
|
187
|
-
|
|
188
|
-
|
|
197
|
+
const blocker = get().activeBlockers.get(id);
|
|
198
|
+
if (blocker) {
|
|
199
|
+
blocker.onTimeout?.(id);
|
|
189
200
|
void get().runMiddlewares(
|
|
190
201
|
createMiddlewareContext("timeout", id, {
|
|
191
|
-
scope:
|
|
192
|
-
reason:
|
|
193
|
-
priority:
|
|
194
|
-
timeout:
|
|
202
|
+
scope: blocker.scope,
|
|
203
|
+
reason: blocker.reason,
|
|
204
|
+
priority: blocker.priority,
|
|
205
|
+
timeout: blocker.timeout
|
|
195
206
|
})
|
|
196
207
|
);
|
|
197
208
|
get().removeBlocker(id);
|
|
@@ -269,7 +280,8 @@ var createUIBlockingActions = (set, get) => ({
|
|
|
269
280
|
return { activeBlockers: newBlockers };
|
|
270
281
|
});
|
|
271
282
|
if (prevBlocker) {
|
|
272
|
-
|
|
283
|
+
const publicConfig = toPublicBlockerConfig(prevBlocker);
|
|
284
|
+
void get().runMiddlewares(createMiddlewareContext("remove", id, publicConfig, publicConfig));
|
|
273
285
|
}
|
|
274
286
|
},
|
|
275
287
|
/**
|
|
@@ -501,16 +513,36 @@ function useResolvedValue(selector) {
|
|
|
501
513
|
const store = useResolvedStoreApi();
|
|
502
514
|
return useStore(store, useShallow(selector));
|
|
503
515
|
}
|
|
516
|
+
|
|
517
|
+
// src/store/blockerId.utils.ts
|
|
518
|
+
var storeSequences = /* @__PURE__ */ new WeakMap();
|
|
519
|
+
function allocateBlockerId(store, prefix) {
|
|
520
|
+
let sequence = storeSequences.get(store) ?? 0;
|
|
521
|
+
let blockerId;
|
|
522
|
+
do {
|
|
523
|
+
sequence += 1;
|
|
524
|
+
blockerId = `${prefix}-${String(sequence)}`;
|
|
525
|
+
} while (store.getState().activeBlockers.has(blockerId));
|
|
526
|
+
storeSequences.set(store, sequence);
|
|
527
|
+
return blockerId;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/hooks/useBlockerIdAllocator/useBlockerIdAllocator.ts
|
|
531
|
+
function useBlockerIdAllocator() {
|
|
532
|
+
const store = useResolvedStoreApi();
|
|
533
|
+
return useCallback((prefix) => allocateBlockerId(store, prefix), [store]);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
504
537
|
function useAsyncAction(actionId, scope, options) {
|
|
538
|
+
const allocateBlockerId2 = useBlockerIdAllocator();
|
|
505
539
|
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
506
540
|
addBlocker: state.addBlocker,
|
|
507
541
|
removeBlocker: state.removeBlocker
|
|
508
542
|
}));
|
|
509
|
-
const counterRef = useRef(0);
|
|
510
543
|
const executeWithBlocking = useCallback(
|
|
511
544
|
async (asyncFn) => {
|
|
512
|
-
|
|
513
|
-
const blockerId = `${actionId}-${String(counterRef.current)}`;
|
|
545
|
+
const blockerId = allocateBlockerId2(actionId);
|
|
514
546
|
try {
|
|
515
547
|
addBlocker(blockerId, {
|
|
516
548
|
scope,
|
|
@@ -524,7 +556,15 @@ function useAsyncAction(actionId, scope, options) {
|
|
|
524
556
|
removeBlocker(blockerId);
|
|
525
557
|
}
|
|
526
558
|
},
|
|
527
|
-
[
|
|
559
|
+
[
|
|
560
|
+
actionId,
|
|
561
|
+
scope,
|
|
562
|
+
options?.timeout,
|
|
563
|
+
options?.onTimeout,
|
|
564
|
+
allocateBlockerId2,
|
|
565
|
+
addBlocker,
|
|
566
|
+
removeBlocker
|
|
567
|
+
]
|
|
528
568
|
);
|
|
529
569
|
return executeWithBlocking;
|
|
530
570
|
}
|
|
@@ -569,12 +609,24 @@ function useBlocker(blockerId, config, isActive = true) {
|
|
|
569
609
|
updateBlocker: state.updateBlocker
|
|
570
610
|
}));
|
|
571
611
|
const lastConfigRef = useRef(null);
|
|
612
|
+
const onTimeoutRef = useRef(config.onTimeout);
|
|
613
|
+
onTimeoutRef.current = config.onTimeout;
|
|
614
|
+
const handleTimeout = useCallback((id) => {
|
|
615
|
+
onTimeoutRef.current?.(id);
|
|
616
|
+
}, []);
|
|
617
|
+
const storeConfig = useMemo(
|
|
618
|
+
() => ({
|
|
619
|
+
...config,
|
|
620
|
+
onTimeout: config.onTimeout ? handleTimeout : void 0
|
|
621
|
+
}),
|
|
622
|
+
[config, handleTimeout]
|
|
623
|
+
);
|
|
572
624
|
useEffect(() => {
|
|
573
625
|
if (!isActive || !blockerId) {
|
|
574
626
|
return;
|
|
575
627
|
}
|
|
576
|
-
addBlocker(blockerId,
|
|
577
|
-
lastConfigRef.current =
|
|
628
|
+
addBlocker(blockerId, storeConfig);
|
|
629
|
+
lastConfigRef.current = storeConfig;
|
|
578
630
|
return () => {
|
|
579
631
|
lastConfigRef.current = null;
|
|
580
632
|
removeBlocker(blockerId);
|
|
@@ -584,12 +636,12 @@ function useBlocker(blockerId, config, isActive = true) {
|
|
|
584
636
|
if (!isActive || !blockerId) {
|
|
585
637
|
return;
|
|
586
638
|
}
|
|
587
|
-
if (lastConfigRef.current !== null && areBlockerConfigsEqual(lastConfigRef.current,
|
|
639
|
+
if (lastConfigRef.current !== null && areBlockerConfigsEqual(lastConfigRef.current, storeConfig)) {
|
|
588
640
|
return;
|
|
589
641
|
}
|
|
590
|
-
updateBlocker(blockerId,
|
|
591
|
-
lastConfigRef.current =
|
|
592
|
-
}, [blockerId,
|
|
642
|
+
updateBlocker(blockerId, storeConfig);
|
|
643
|
+
lastConfigRef.current = storeConfig;
|
|
644
|
+
}, [blockerId, storeConfig, isActive, updateBlocker]);
|
|
593
645
|
}
|
|
594
646
|
function useBlockingInfo(scope = DEFAULT_SCOPE) {
|
|
595
647
|
const store = useResolvedStoreApi();
|
|
@@ -607,6 +659,7 @@ function useConfigRef(config) {
|
|
|
607
659
|
}
|
|
608
660
|
|
|
609
661
|
// src/hooks/useConditionalBlocker/useConditionalBlocker.ts
|
|
662
|
+
var DEFAULT_CHECK_INTERVAL = 1e3;
|
|
610
663
|
function useConditionalBlocker(blockerId, config) {
|
|
611
664
|
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
612
665
|
addBlocker: state.addBlocker,
|
|
@@ -615,6 +668,7 @@ function useConditionalBlocker(blockerId, config) {
|
|
|
615
668
|
const intervalRef = useRef(null);
|
|
616
669
|
const isBlockedRef = useRef(false);
|
|
617
670
|
const configRef = useConfigRef(config);
|
|
671
|
+
const checkInterval = config.checkInterval !== void 0 && config.checkInterval > 0 ? config.checkInterval : DEFAULT_CHECK_INTERVAL;
|
|
618
672
|
const checkCondition = useCallback(() => {
|
|
619
673
|
const currentConfig = configRef.current;
|
|
620
674
|
const shouldBlock = currentConfig.condition(currentConfig.state);
|
|
@@ -630,8 +684,7 @@ function useConditionalBlocker(blockerId, config) {
|
|
|
630
684
|
}, [blockerId, addBlocker, removeBlocker, configRef]);
|
|
631
685
|
useEffect(() => {
|
|
632
686
|
checkCondition();
|
|
633
|
-
|
|
634
|
-
intervalRef.current = setInterval(checkCondition, interval);
|
|
687
|
+
intervalRef.current = setInterval(checkCondition, checkInterval);
|
|
635
688
|
return () => {
|
|
636
689
|
if (intervalRef.current) {
|
|
637
690
|
clearInterval(intervalRef.current);
|
|
@@ -642,7 +695,7 @@ function useConditionalBlocker(blockerId, config) {
|
|
|
642
695
|
isBlockedRef.current = false;
|
|
643
696
|
}
|
|
644
697
|
};
|
|
645
|
-
}, [checkCondition, blockerId, removeBlocker]);
|
|
698
|
+
}, [checkCondition, blockerId, removeBlocker, checkInterval]);
|
|
646
699
|
}
|
|
647
700
|
function useConfirmableBlocker(blockerId, config) {
|
|
648
701
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
|
@@ -736,6 +789,9 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
736
789
|
}));
|
|
737
790
|
const timeoutsRef = useRef({});
|
|
738
791
|
const configRef = useConfigRef(config);
|
|
792
|
+
const scheduleStart = parseDate(config.schedule.start);
|
|
793
|
+
const scheduleEnd = config.schedule.end === void 0 ? void 0 : parseDate(config.schedule.end);
|
|
794
|
+
const scheduleDuration = config.schedule.duration;
|
|
739
795
|
const cleanup = useCallback(() => {
|
|
740
796
|
if (timeoutsRef.current.start) {
|
|
741
797
|
clearTimeout(timeoutsRef.current.start);
|
|
@@ -753,7 +809,7 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
753
809
|
if (!isValidTimestamp(startTime)) {
|
|
754
810
|
return;
|
|
755
811
|
}
|
|
756
|
-
const
|
|
812
|
+
const scheduleEnd2 = () => {
|
|
757
813
|
const endTime = calculateEndTime(currentConfig.schedule, startTime);
|
|
758
814
|
if (!endTime) {
|
|
759
815
|
return;
|
|
@@ -770,7 +826,7 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
770
826
|
const cfg = configRef.current;
|
|
771
827
|
addBlocker(blockerId, createBlockerConfig(cfg));
|
|
772
828
|
cfg.onScheduleStart?.();
|
|
773
|
-
|
|
829
|
+
scheduleEnd2();
|
|
774
830
|
};
|
|
775
831
|
if (startTime > now) {
|
|
776
832
|
const delay = startTime - now;
|
|
@@ -790,7 +846,16 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
790
846
|
cleanup();
|
|
791
847
|
removeBlocker(blockerId);
|
|
792
848
|
};
|
|
793
|
-
}, [
|
|
849
|
+
}, [
|
|
850
|
+
blockerId,
|
|
851
|
+
addBlocker,
|
|
852
|
+
removeBlocker,
|
|
853
|
+
cleanup,
|
|
854
|
+
scheduleStart,
|
|
855
|
+
scheduleEnd,
|
|
856
|
+
scheduleDuration,
|
|
857
|
+
configRef
|
|
858
|
+
]);
|
|
794
859
|
}
|
|
795
860
|
|
|
796
861
|
// src/middleware/analyticsMiddleware.utils.ts
|