@okyrychenko-dev/react-action-guard 0.2.2 → 0.3.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 +15 -19
- package/dist/index.cjs +176 -114
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -57
- package/dist/index.d.ts +40 -57
- package/dist/index.js +177 -115
- package/dist/index.js.map +1 -1
- package/package.json +14 -2
package/dist/index.d.cts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
declare const loggerMiddleware: Middleware;
|
|
1
|
+
/**
|
|
2
|
+
* Hook to wrap async functions with automatic blocking/unblocking
|
|
3
|
+
*
|
|
4
|
+
* Creates a wrapper function that automatically adds a blocker before executing
|
|
5
|
+
* the async function and removes it after completion (success or failure).
|
|
6
|
+
*
|
|
7
|
+
* @template T - The return type of the async function
|
|
8
|
+
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
9
|
+
* @param scope - Scope(s) to block during execution (default: "global")
|
|
10
|
+
* @returns Function that wraps async functions with blocking behavior
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
15
14
|
|
|
16
15
|
type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
|
|
17
16
|
type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
|
|
@@ -29,8 +28,21 @@ interface CustomAnalyticsConfig {
|
|
|
29
28
|
}
|
|
30
29
|
type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
|
|
31
30
|
|
|
31
|
+
type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
|
|
32
|
+
type MiddlewareBlockerConfig = BlockerConfig;
|
|
33
|
+
interface MiddlewareContext {
|
|
34
|
+
action: BlockingAction;
|
|
35
|
+
blockerId: string;
|
|
36
|
+
config?: MiddlewareBlockerConfig;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
prevState?: MiddlewareBlockerConfig;
|
|
39
|
+
}
|
|
40
|
+
type Middleware = (context: MiddlewareContext) => void | Promise<void>;
|
|
41
|
+
|
|
32
42
|
declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
|
|
33
43
|
|
|
44
|
+
declare const loggerMiddleware: Middleware;
|
|
45
|
+
|
|
34
46
|
interface PerformanceConfig {
|
|
35
47
|
slowBlockThreshold?: number;
|
|
36
48
|
onSlowBlock?: (blockerId: string, duration: number) => void;
|
|
@@ -98,25 +110,10 @@ type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
|
98
110
|
* - Scope-based blocking (global, specific areas, or multiple scopes)
|
|
99
111
|
* - Automatic cleanup on unmount (when using hooks)
|
|
100
112
|
* - DevTools integration for debugging
|
|
113
|
+
* - Automatic shallow comparison for selectors
|
|
101
114
|
*
|
|
102
115
|
*/
|
|
103
|
-
declare const useUIBlockingStore:
|
|
104
|
-
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
105
|
-
[x: string]: unknown;
|
|
106
|
-
[x: number]: unknown;
|
|
107
|
-
[x: symbol]: unknown;
|
|
108
|
-
type: string;
|
|
109
|
-
}) | undefined): void;
|
|
110
|
-
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
111
|
-
[x: string]: unknown;
|
|
112
|
-
[x: number]: unknown;
|
|
113
|
-
[x: symbol]: unknown;
|
|
114
|
-
type: string;
|
|
115
|
-
}) | undefined): void;
|
|
116
|
-
devtools: {
|
|
117
|
-
cleanup: () => void;
|
|
118
|
-
};
|
|
119
|
-
}>;
|
|
116
|
+
declare const useUIBlockingStore: <T>(selector: (state: UIBlockingStore) => T) => T;
|
|
120
117
|
|
|
121
118
|
/**
|
|
122
119
|
* Hook to automatically add and remove a blocker based on component lifecycle
|
|
@@ -131,32 +128,6 @@ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UI
|
|
|
131
128
|
*/
|
|
132
129
|
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
133
130
|
|
|
134
|
-
/**
|
|
135
|
-
* Hook to check if a scope is currently blocked
|
|
136
|
-
*
|
|
137
|
-
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
138
|
-
* Automatically re-renders when the blocking state changes.
|
|
139
|
-
*
|
|
140
|
-
* @param scope - Scope or array of scopes to check (default: "global")
|
|
141
|
-
* @returns True if any of the specified scopes are blocked
|
|
142
|
-
*
|
|
143
|
-
*/
|
|
144
|
-
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Hook to wrap async functions with automatic blocking/unblocking
|
|
148
|
-
*
|
|
149
|
-
* Creates a wrapper function that automatically adds a blocker before executing
|
|
150
|
-
* the async function and removes it after completion (success or failure).
|
|
151
|
-
*
|
|
152
|
-
* @template T - The return type of the async function
|
|
153
|
-
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
154
|
-
* @param scope - Scope(s) to block during execution (default: "global")
|
|
155
|
-
* @returns Function that wraps async functions with blocking behavior
|
|
156
|
-
*
|
|
157
|
-
*/
|
|
158
|
-
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
159
|
-
|
|
160
131
|
/**
|
|
161
132
|
* Configuration for conditional blocker hook.
|
|
162
133
|
*
|
|
@@ -233,6 +204,18 @@ interface UseConfirmableBlockerReturn {
|
|
|
233
204
|
*/
|
|
234
205
|
declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
|
|
235
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Hook to check if a scope is currently blocked
|
|
209
|
+
*
|
|
210
|
+
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
211
|
+
* Automatically re-renders when the blocking state changes.
|
|
212
|
+
*
|
|
213
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
214
|
+
* @returns True if any of the specified scopes are blocked
|
|
215
|
+
*
|
|
216
|
+
*/
|
|
217
|
+
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
218
|
+
|
|
236
219
|
/**
|
|
237
220
|
* Schedule configuration for time-based blocking.
|
|
238
221
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
declare const loggerMiddleware: Middleware;
|
|
1
|
+
/**
|
|
2
|
+
* Hook to wrap async functions with automatic blocking/unblocking
|
|
3
|
+
*
|
|
4
|
+
* Creates a wrapper function that automatically adds a blocker before executing
|
|
5
|
+
* the async function and removes it after completion (success or failure).
|
|
6
|
+
*
|
|
7
|
+
* @template T - The return type of the async function
|
|
8
|
+
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
9
|
+
* @param scope - Scope(s) to block during execution (default: "global")
|
|
10
|
+
* @returns Function that wraps async functions with blocking behavior
|
|
11
|
+
*
|
|
12
|
+
*/
|
|
13
|
+
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
15
14
|
|
|
16
15
|
type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
|
|
17
16
|
type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
|
|
@@ -29,8 +28,21 @@ interface CustomAnalyticsConfig {
|
|
|
29
28
|
}
|
|
30
29
|
type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
|
|
31
30
|
|
|
31
|
+
type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
|
|
32
|
+
type MiddlewareBlockerConfig = BlockerConfig;
|
|
33
|
+
interface MiddlewareContext {
|
|
34
|
+
action: BlockingAction;
|
|
35
|
+
blockerId: string;
|
|
36
|
+
config?: MiddlewareBlockerConfig;
|
|
37
|
+
timestamp: number;
|
|
38
|
+
prevState?: MiddlewareBlockerConfig;
|
|
39
|
+
}
|
|
40
|
+
type Middleware = (context: MiddlewareContext) => void | Promise<void>;
|
|
41
|
+
|
|
32
42
|
declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
|
|
33
43
|
|
|
44
|
+
declare const loggerMiddleware: Middleware;
|
|
45
|
+
|
|
34
46
|
interface PerformanceConfig {
|
|
35
47
|
slowBlockThreshold?: number;
|
|
36
48
|
onSlowBlock?: (blockerId: string, duration: number) => void;
|
|
@@ -98,25 +110,10 @@ type UIBlockingStore = UIBlockingStoreState & UIBlockingStoreActions;
|
|
|
98
110
|
* - Scope-based blocking (global, specific areas, or multiple scopes)
|
|
99
111
|
* - Automatic cleanup on unmount (when using hooks)
|
|
100
112
|
* - DevTools integration for debugging
|
|
113
|
+
* - Automatic shallow comparison for selectors
|
|
101
114
|
*
|
|
102
115
|
*/
|
|
103
|
-
declare const useUIBlockingStore:
|
|
104
|
-
setState(partial: UIBlockingStore | Partial<UIBlockingStore> | ((state: UIBlockingStore) => UIBlockingStore | Partial<UIBlockingStore>), replace?: false | undefined, action?: (string | {
|
|
105
|
-
[x: string]: unknown;
|
|
106
|
-
[x: number]: unknown;
|
|
107
|
-
[x: symbol]: unknown;
|
|
108
|
-
type: string;
|
|
109
|
-
}) | undefined): void;
|
|
110
|
-
setState(state: UIBlockingStore | ((state: UIBlockingStore) => UIBlockingStore), replace: true, action?: (string | {
|
|
111
|
-
[x: string]: unknown;
|
|
112
|
-
[x: number]: unknown;
|
|
113
|
-
[x: symbol]: unknown;
|
|
114
|
-
type: string;
|
|
115
|
-
}) | undefined): void;
|
|
116
|
-
devtools: {
|
|
117
|
-
cleanup: () => void;
|
|
118
|
-
};
|
|
119
|
-
}>;
|
|
116
|
+
declare const useUIBlockingStore: <T>(selector: (state: UIBlockingStore) => T) => T;
|
|
120
117
|
|
|
121
118
|
/**
|
|
122
119
|
* Hook to automatically add and remove a blocker based on component lifecycle
|
|
@@ -131,32 +128,6 @@ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UI
|
|
|
131
128
|
*/
|
|
132
129
|
declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
|
|
133
130
|
|
|
134
|
-
/**
|
|
135
|
-
* Hook to check if a scope is currently blocked
|
|
136
|
-
*
|
|
137
|
-
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
138
|
-
* Automatically re-renders when the blocking state changes.
|
|
139
|
-
*
|
|
140
|
-
* @param scope - Scope or array of scopes to check (default: "global")
|
|
141
|
-
* @returns True if any of the specified scopes are blocked
|
|
142
|
-
*
|
|
143
|
-
*/
|
|
144
|
-
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Hook to wrap async functions with automatic blocking/unblocking
|
|
148
|
-
*
|
|
149
|
-
* Creates a wrapper function that automatically adds a blocker before executing
|
|
150
|
-
* the async function and removes it after completion (success or failure).
|
|
151
|
-
*
|
|
152
|
-
* @template T - The return type of the async function
|
|
153
|
-
* @param actionId - Unique identifier for the action (used in blocker ID and reason)
|
|
154
|
-
* @param scope - Scope(s) to block during execution (default: "global")
|
|
155
|
-
* @returns Function that wraps async functions with blocking behavior
|
|
156
|
-
*
|
|
157
|
-
*/
|
|
158
|
-
declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
|
|
159
|
-
|
|
160
131
|
/**
|
|
161
132
|
* Configuration for conditional blocker hook.
|
|
162
133
|
*
|
|
@@ -233,6 +204,18 @@ interface UseConfirmableBlockerReturn {
|
|
|
233
204
|
*/
|
|
234
205
|
declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
|
|
235
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Hook to check if a scope is currently blocked
|
|
209
|
+
*
|
|
210
|
+
* Returns a boolean indicating whether the specified scope(s) are blocked.
|
|
211
|
+
* Automatically re-renders when the blocking state changes.
|
|
212
|
+
*
|
|
213
|
+
* @param scope - Scope or array of scopes to check (default: "global")
|
|
214
|
+
* @returns True if any of the specified scopes are blocked
|
|
215
|
+
*
|
|
216
|
+
*/
|
|
217
|
+
declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
|
|
218
|
+
|
|
236
219
|
/**
|
|
237
220
|
* Schedule configuration for time-based blocking.
|
|
238
221
|
*
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { create } from 'zustand';
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
2
|
import { devtools } from 'zustand/middleware';
|
|
3
|
+
import { createStore, useStore } from 'zustand';
|
|
4
|
+
import { useShallow } from 'zustand/shallow';
|
|
4
5
|
|
|
5
|
-
// src/hooks/
|
|
6
|
+
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
6
7
|
|
|
7
8
|
// src/store/uiBlockingStore.constants.ts
|
|
8
9
|
var DEFAULT_SCOPE = "global";
|
|
@@ -10,8 +11,13 @@ var DEFAULT_REASON = "Unknown";
|
|
|
10
11
|
var DEFAULT_PRIORITY = 0;
|
|
11
12
|
var ASYNC_ACTION_PRIORITY = 20;
|
|
12
13
|
var DEVTOOLS_NAME = "UIBlocking";
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
function createShallowStore(storeCreator) {
|
|
15
|
+
const storeApi = createStore(storeCreator);
|
|
16
|
+
const useScopedStore = (selector) => {
|
|
17
|
+
return useStore(storeApi, useShallow(selector));
|
|
18
|
+
};
|
|
19
|
+
return { useStore: useScopedStore, useStoreApi: storeApi };
|
|
20
|
+
}
|
|
15
21
|
function normalizeScopeToArray(scope) {
|
|
16
22
|
if (typeof scope === "string") {
|
|
17
23
|
return [scope];
|
|
@@ -176,30 +182,14 @@ var devtoolsConfig = {
|
|
|
176
182
|
};
|
|
177
183
|
|
|
178
184
|
// src/store/uiBlockingStore.store.ts
|
|
179
|
-
var useUIBlockingStore =
|
|
180
|
-
devtools(createUIBlockingActions, devtoolsConfig)
|
|
181
|
-
);
|
|
182
|
-
|
|
183
|
-
// src/hooks/useBlocker.ts
|
|
184
|
-
var useBlocker = (blockerId, config, isActive = true) => {
|
|
185
|
-
const { addBlocker, removeBlocker } = useUIBlockingStore();
|
|
186
|
-
useEffect(() => {
|
|
187
|
-
if (!isActive || !blockerId) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
addBlocker(blockerId, config);
|
|
191
|
-
return () => {
|
|
192
|
-
removeBlocker(blockerId);
|
|
193
|
-
};
|
|
194
|
-
}, [blockerId, isActive, addBlocker, removeBlocker]);
|
|
195
|
-
};
|
|
185
|
+
var { useStore: useUIBlockingStore} = createShallowStore(devtools(createUIBlockingActions, devtoolsConfig));
|
|
196
186
|
|
|
197
|
-
// src/hooks/
|
|
198
|
-
var useIsBlocked = (scope) => {
|
|
199
|
-
return useUIBlockingStore((state) => state.isBlocked(scope));
|
|
200
|
-
};
|
|
187
|
+
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
201
188
|
var useAsyncAction = (actionId, scope) => {
|
|
202
|
-
const { addBlocker, removeBlocker } = useUIBlockingStore()
|
|
189
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
190
|
+
addBlocker: state.addBlocker,
|
|
191
|
+
removeBlocker: state.removeBlocker
|
|
192
|
+
}));
|
|
203
193
|
const executeWithBlocking = useCallback(
|
|
204
194
|
async (asyncFn) => {
|
|
205
195
|
const blockerId = `${actionId}-${Date.now().toString()}`;
|
|
@@ -218,15 +208,23 @@ var useAsyncAction = (actionId, scope) => {
|
|
|
218
208
|
);
|
|
219
209
|
return executeWithBlocking;
|
|
220
210
|
};
|
|
221
|
-
|
|
222
|
-
const
|
|
211
|
+
var useBlocker = (blockerId, config, isActive = true) => {
|
|
212
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
213
|
+
addBlocker: state.addBlocker,
|
|
214
|
+
removeBlocker: state.removeBlocker
|
|
215
|
+
}));
|
|
223
216
|
useEffect(() => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
217
|
+
if (!isActive || !blockerId) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
addBlocker(blockerId, config);
|
|
221
|
+
return () => {
|
|
222
|
+
removeBlocker(blockerId);
|
|
223
|
+
};
|
|
224
|
+
}, [blockerId, isActive, addBlocker, removeBlocker]);
|
|
225
|
+
};
|
|
228
226
|
|
|
229
|
-
// src/hooks/useBlocker.utils.ts
|
|
227
|
+
// src/hooks/useBlocker/useBlocker.utils.ts
|
|
230
228
|
function createBlockerConfig(config) {
|
|
231
229
|
return {
|
|
232
230
|
scope: config.scope,
|
|
@@ -234,10 +232,20 @@ function createBlockerConfig(config) {
|
|
|
234
232
|
priority: config.priority
|
|
235
233
|
};
|
|
236
234
|
}
|
|
235
|
+
function useConfigRef(config) {
|
|
236
|
+
const configRef = useRef(config);
|
|
237
|
+
useEffect(() => {
|
|
238
|
+
configRef.current = config;
|
|
239
|
+
}, [config]);
|
|
240
|
+
return configRef;
|
|
241
|
+
}
|
|
237
242
|
|
|
238
|
-
// src/hooks/useConditionalBlocker.ts
|
|
243
|
+
// src/hooks/useConditionalBlocker/useConditionalBlocker.ts
|
|
239
244
|
var useConditionalBlocker = (blockerId, config) => {
|
|
240
|
-
const { addBlocker, removeBlocker } = useUIBlockingStore()
|
|
245
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
246
|
+
addBlocker: state.addBlocker,
|
|
247
|
+
removeBlocker: state.removeBlocker
|
|
248
|
+
}));
|
|
241
249
|
const intervalRef = useRef(null);
|
|
242
250
|
const isBlockedRef = useRef(false);
|
|
243
251
|
const configRef = useConfigRef(config);
|
|
@@ -249,11 +257,10 @@ var useConditionalBlocker = (blockerId, config) => {
|
|
|
249
257
|
}
|
|
250
258
|
if (shouldBlock) {
|
|
251
259
|
addBlocker(blockerId, createBlockerConfig(currentConfig));
|
|
252
|
-
isBlockedRef.current = true;
|
|
253
260
|
} else {
|
|
254
261
|
removeBlocker(blockerId);
|
|
255
|
-
isBlockedRef.current = false;
|
|
256
262
|
}
|
|
263
|
+
isBlockedRef.current = shouldBlock;
|
|
257
264
|
}, [blockerId, addBlocker, removeBlocker, configRef]);
|
|
258
265
|
useEffect(() => {
|
|
259
266
|
checkCondition();
|
|
@@ -315,8 +322,55 @@ var useConfirmableBlocker = (blockerId, config) => {
|
|
|
315
322
|
onCancel
|
|
316
323
|
};
|
|
317
324
|
};
|
|
325
|
+
|
|
326
|
+
// src/hooks/useIsBlocked/useIsBlocked.ts
|
|
327
|
+
var useIsBlocked = (scope) => {
|
|
328
|
+
return useUIBlockingStore((state) => state.isBlocked(scope));
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/hooks/useScheduledBlocker/useScheduledBlocker.utils.ts
|
|
332
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
333
|
+
function parseDate(date) {
|
|
334
|
+
return new Date(date).getTime();
|
|
335
|
+
}
|
|
336
|
+
function isValidTimestamp(timestamp) {
|
|
337
|
+
return !Number.isNaN(timestamp);
|
|
338
|
+
}
|
|
339
|
+
function calculateEndTime(schedule, startTime) {
|
|
340
|
+
if (schedule.duration) {
|
|
341
|
+
return startTime + schedule.duration;
|
|
342
|
+
}
|
|
343
|
+
if (schedule.end) {
|
|
344
|
+
const endTime = parseDate(schedule.end);
|
|
345
|
+
return isValidTimestamp(endTime) ? endTime : void 0;
|
|
346
|
+
}
|
|
347
|
+
return void 0;
|
|
348
|
+
}
|
|
349
|
+
function isSafeTimeout(delay) {
|
|
350
|
+
return delay > 0 && delay <= MAX_TIMEOUT_MS;
|
|
351
|
+
}
|
|
352
|
+
function isInBlockingPeriod(startTime, endTime, now) {
|
|
353
|
+
if (startTime > now) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
if (!endTime) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
return endTime > now;
|
|
360
|
+
}
|
|
361
|
+
function isScheduleInPast(startTime, endTime, now) {
|
|
362
|
+
if (!endTime) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
return endTime <= now && startTime <= now;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/hooks/useScheduledBlocker/useScheduledBlocker.ts
|
|
318
369
|
var useScheduledBlocker = (blockerId, config) => {
|
|
319
|
-
const { addBlocker, removeBlocker } = useUIBlockingStore()
|
|
370
|
+
const { addBlocker, removeBlocker } = useUIBlockingStore((state) => ({
|
|
371
|
+
addBlocker: state.addBlocker,
|
|
372
|
+
removeBlocker: state.removeBlocker
|
|
373
|
+
}));
|
|
320
374
|
const timeoutsRef = useRef({});
|
|
321
375
|
const configRef = useConfigRef(config);
|
|
322
376
|
const cleanup = useCallback(() => {
|
|
@@ -331,41 +385,47 @@ var useScheduledBlocker = (blockerId, config) => {
|
|
|
331
385
|
}, []);
|
|
332
386
|
useEffect(() => {
|
|
333
387
|
const currentConfig = configRef.current;
|
|
334
|
-
const startTime =
|
|
388
|
+
const startTime = parseDate(currentConfig.schedule.start);
|
|
335
389
|
const now = Date.now();
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} else if (currentConfig.schedule.end) {
|
|
340
|
-
endTime = new Date(currentConfig.schedule.end).getTime();
|
|
390
|
+
if (!isValidTimestamp(startTime)) {
|
|
391
|
+
console.error(`[UIBlocking] Invalid start time for blocker "${blockerId}"`);
|
|
392
|
+
return;
|
|
341
393
|
}
|
|
342
|
-
const scheduleEnd = (
|
|
343
|
-
const
|
|
394
|
+
const scheduleEnd = () => {
|
|
395
|
+
const endTime = calculateEndTime(currentConfig.schedule, startTime);
|
|
396
|
+
if (!endTime) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const remainingTime = endTime - Date.now();
|
|
344
400
|
if (remainingTime > 0) {
|
|
345
401
|
timeoutsRef.current.end = setTimeout(() => {
|
|
346
|
-
removeBlocker(blockerId);
|
|
347
402
|
configRef.current.onScheduleEnd?.();
|
|
403
|
+
removeBlocker(blockerId);
|
|
348
404
|
}, remainingTime);
|
|
349
405
|
}
|
|
350
406
|
};
|
|
407
|
+
const startBlocking = () => {
|
|
408
|
+
const cfg = configRef.current;
|
|
409
|
+
addBlocker(blockerId, createBlockerConfig(cfg));
|
|
410
|
+
cfg.onScheduleStart?.();
|
|
411
|
+
scheduleEnd();
|
|
412
|
+
};
|
|
351
413
|
if (startTime > now) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
} else if (endTime && endTime > now) {
|
|
361
|
-
addBlocker(blockerId, createBlockerConfig(currentConfig));
|
|
362
|
-
currentConfig.onScheduleStart?.();
|
|
363
|
-
scheduleEnd(endTime);
|
|
364
|
-
} else if (!endTime && startTime <= now) {
|
|
365
|
-
addBlocker(blockerId, createBlockerConfig(currentConfig));
|
|
366
|
-
currentConfig.onScheduleStart?.();
|
|
414
|
+
const delay = startTime - now;
|
|
415
|
+
if (!isSafeTimeout(delay)) {
|
|
416
|
+
console.warn(
|
|
417
|
+
`[UIBlocking] Schedule delay exceeds maximum timeout (${MAX_TIMEOUT_MS.toString()}ms) for blocker "${blockerId}"`
|
|
418
|
+
);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
timeoutsRef.current.start = setTimeout(startBlocking, delay);
|
|
367
422
|
} else {
|
|
368
|
-
|
|
423
|
+
const endTime = calculateEndTime(currentConfig.schedule, startTime);
|
|
424
|
+
if (isInBlockingPeriod(startTime, endTime, now)) {
|
|
425
|
+
startBlocking();
|
|
426
|
+
} else if (isScheduleInPast(startTime, endTime, now)) {
|
|
427
|
+
console.warn(`[UIBlocking] Schedule is in the past for blocker "${blockerId}"`);
|
|
428
|
+
}
|
|
369
429
|
}
|
|
370
430
|
return () => {
|
|
371
431
|
cleanup();
|
|
@@ -374,56 +434,6 @@ var useScheduledBlocker = (blockerId, config) => {
|
|
|
374
434
|
}, [blockerId, addBlocker, removeBlocker, cleanup]);
|
|
375
435
|
};
|
|
376
436
|
|
|
377
|
-
// src/middleware/loggerMiddleware.utils.ts
|
|
378
|
-
var getActionEmoji = (action) => {
|
|
379
|
-
switch (action) {
|
|
380
|
-
case "add":
|
|
381
|
-
return "\u2795";
|
|
382
|
-
case "remove":
|
|
383
|
-
return "\u2796";
|
|
384
|
-
case "update":
|
|
385
|
-
return "\u{1F504}";
|
|
386
|
-
case "cancel":
|
|
387
|
-
return "\u274C";
|
|
388
|
-
case "timeout":
|
|
389
|
-
return "\u23F1\uFE0F";
|
|
390
|
-
default:
|
|
391
|
-
return "\u2753";
|
|
392
|
-
}
|
|
393
|
-
};
|
|
394
|
-
var extractConfigDetails = (config) => {
|
|
395
|
-
if (!config) {
|
|
396
|
-
return {};
|
|
397
|
-
}
|
|
398
|
-
const { scope, reason, priority } = config;
|
|
399
|
-
const details = {
|
|
400
|
-
scope,
|
|
401
|
-
reason
|
|
402
|
-
};
|
|
403
|
-
if (priority !== void 0) {
|
|
404
|
-
details.priority = priority;
|
|
405
|
-
}
|
|
406
|
-
return details;
|
|
407
|
-
};
|
|
408
|
-
var formatLogData = (context) => {
|
|
409
|
-
const configDetails = extractConfigDetails(context.config);
|
|
410
|
-
if (context.prevState) {
|
|
411
|
-
return {
|
|
412
|
-
config: configDetails,
|
|
413
|
-
prevState: extractConfigDetails(context.prevState)
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
return configDetails;
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// src/middleware/loggerMiddleware.ts
|
|
420
|
-
var LOG_PREFIX = "[UIBlocking]";
|
|
421
|
-
var loggerMiddleware = (context) => {
|
|
422
|
-
const emoji = getActionEmoji(context.action);
|
|
423
|
-
const logData = formatLogData(context);
|
|
424
|
-
console.log(LOG_PREFIX, emoji, context.blockerId, logData);
|
|
425
|
-
};
|
|
426
|
-
|
|
427
437
|
// src/middleware/analyticsMiddleware.utils.ts
|
|
428
438
|
var capitalizeFirst = (str) => {
|
|
429
439
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
@@ -494,6 +504,56 @@ var createAnalyticsMiddleware = (config) => {
|
|
|
494
504
|
};
|
|
495
505
|
};
|
|
496
506
|
|
|
507
|
+
// src/middleware/loggerMiddleware.utils.ts
|
|
508
|
+
var getActionEmoji = (action) => {
|
|
509
|
+
switch (action) {
|
|
510
|
+
case "add":
|
|
511
|
+
return "\u2795";
|
|
512
|
+
case "remove":
|
|
513
|
+
return "\u2796";
|
|
514
|
+
case "update":
|
|
515
|
+
return "\u{1F504}";
|
|
516
|
+
case "cancel":
|
|
517
|
+
return "\u274C";
|
|
518
|
+
case "timeout":
|
|
519
|
+
return "\u23F1\uFE0F";
|
|
520
|
+
default:
|
|
521
|
+
return "\u2753";
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var extractConfigDetails = (config) => {
|
|
525
|
+
if (!config) {
|
|
526
|
+
return {};
|
|
527
|
+
}
|
|
528
|
+
const { scope, reason, priority } = config;
|
|
529
|
+
const details = {
|
|
530
|
+
scope,
|
|
531
|
+
reason
|
|
532
|
+
};
|
|
533
|
+
if (priority !== void 0) {
|
|
534
|
+
details.priority = priority;
|
|
535
|
+
}
|
|
536
|
+
return details;
|
|
537
|
+
};
|
|
538
|
+
var formatLogData = (context) => {
|
|
539
|
+
const configDetails = extractConfigDetails(context.config);
|
|
540
|
+
if (context.prevState) {
|
|
541
|
+
return {
|
|
542
|
+
config: configDetails,
|
|
543
|
+
prevState: extractConfigDetails(context.prevState)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return configDetails;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// src/middleware/loggerMiddleware.ts
|
|
550
|
+
var LOG_PREFIX = "[UIBlocking]";
|
|
551
|
+
var loggerMiddleware = (context) => {
|
|
552
|
+
const emoji = getActionEmoji(context.action);
|
|
553
|
+
const logData = formatLogData(context);
|
|
554
|
+
console.log(LOG_PREFIX, emoji, context.blockerId, logData);
|
|
555
|
+
};
|
|
556
|
+
|
|
497
557
|
// src/middleware/performanceMiddleware.utils.ts
|
|
498
558
|
var handleAddAction = (blockerId, timestamp, blockStartTimes) => {
|
|
499
559
|
blockStartTimes.set(blockerId, timestamp);
|
|
@@ -511,7 +571,9 @@ var handleRemoveAction = (context, blockStartTimes, slowBlockThreshold, onSlowBl
|
|
|
511
571
|
const duration = context.timestamp - startTime;
|
|
512
572
|
blockStartTimes.delete(context.blockerId);
|
|
513
573
|
if (duration >= slowBlockThreshold) {
|
|
514
|
-
console.warn(
|
|
574
|
+
console.warn(
|
|
575
|
+
`[UIBlocking] Slow block detected: "${context.blockerId}" took ${duration.toString()}ms`
|
|
576
|
+
);
|
|
515
577
|
onSlowBlock?.(context.blockerId, duration);
|
|
516
578
|
}
|
|
517
579
|
};
|