@okyrychenko-dev/react-action-guard 0.2.2 → 0.2.3

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/dist/index.d.cts CHANGED
@@ -1,17 +1,18 @@
1
1
  import * as zustand from 'zustand';
2
2
 
3
- type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
4
- type MiddlewareBlockerConfig = BlockerConfig;
5
- interface MiddlewareContext {
6
- action: BlockingAction;
7
- blockerId: string;
8
- config?: MiddlewareBlockerConfig;
9
- timestamp: number;
10
- prevState?: MiddlewareBlockerConfig;
11
- }
12
- type Middleware = (context: MiddlewareContext) => void | Promise<void>;
13
-
14
- declare const loggerMiddleware: Middleware;
3
+ /**
4
+ * Hook to wrap async functions with automatic blocking/unblocking
5
+ *
6
+ * Creates a wrapper function that automatically adds a blocker before executing
7
+ * the async function and removes it after completion (success or failure).
8
+ *
9
+ * @template T - The return type of the async function
10
+ * @param actionId - Unique identifier for the action (used in blocker ID and reason)
11
+ * @param scope - Scope(s) to block during execution (default: "global")
12
+ * @returns Function that wraps async functions with blocking behavior
13
+ *
14
+ */
15
+ declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
15
16
 
16
17
  type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
17
18
  type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
@@ -29,8 +30,21 @@ interface CustomAnalyticsConfig {
29
30
  }
30
31
  type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
31
32
 
33
+ type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
34
+ type MiddlewareBlockerConfig = BlockerConfig;
35
+ interface MiddlewareContext {
36
+ action: BlockingAction;
37
+ blockerId: string;
38
+ config?: MiddlewareBlockerConfig;
39
+ timestamp: number;
40
+ prevState?: MiddlewareBlockerConfig;
41
+ }
42
+ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
43
+
32
44
  declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
33
45
 
46
+ declare const loggerMiddleware: Middleware;
47
+
34
48
  interface PerformanceConfig {
35
49
  slowBlockThreshold?: number;
36
50
  onSlowBlock?: (blockerId: string, duration: number) => void;
@@ -131,32 +145,6 @@ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UI
131
145
  */
132
146
  declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
133
147
 
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
148
  /**
161
149
  * Configuration for conditional blocker hook.
162
150
  *
@@ -233,6 +221,18 @@ interface UseConfirmableBlockerReturn {
233
221
  */
234
222
  declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
235
223
 
224
+ /**
225
+ * Hook to check if a scope is currently blocked
226
+ *
227
+ * Returns a boolean indicating whether the specified scope(s) are blocked.
228
+ * Automatically re-renders when the blocking state changes.
229
+ *
230
+ * @param scope - Scope or array of scopes to check (default: "global")
231
+ * @returns True if any of the specified scopes are blocked
232
+ *
233
+ */
234
+ declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
235
+
236
236
  /**
237
237
  * Schedule configuration for time-based blocking.
238
238
  *
package/dist/index.d.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  import * as zustand from 'zustand';
2
2
 
3
- type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
4
- type MiddlewareBlockerConfig = BlockerConfig;
5
- interface MiddlewareContext {
6
- action: BlockingAction;
7
- blockerId: string;
8
- config?: MiddlewareBlockerConfig;
9
- timestamp: number;
10
- prevState?: MiddlewareBlockerConfig;
11
- }
12
- type Middleware = (context: MiddlewareContext) => void | Promise<void>;
13
-
14
- declare const loggerMiddleware: Middleware;
3
+ /**
4
+ * Hook to wrap async functions with automatic blocking/unblocking
5
+ *
6
+ * Creates a wrapper function that automatically adds a blocker before executing
7
+ * the async function and removes it after completion (success or failure).
8
+ *
9
+ * @template T - The return type of the async function
10
+ * @param actionId - Unique identifier for the action (used in blocker ID and reason)
11
+ * @param scope - Scope(s) to block during execution (default: "global")
12
+ * @returns Function that wraps async functions with blocking behavior
13
+ *
14
+ */
15
+ declare const useAsyncAction: <T = unknown>(actionId: string, scope?: string | ReadonlyArray<string>) => ((asyncFn: () => Promise<T>) => Promise<T>);
15
16
 
16
17
  type AnalyticsEventData = Record<string, string | number | ReadonlyArray<string> | undefined>;
17
18
  type AnalyticsProvider = "ga" | "mixpanel" | "amplitude";
@@ -29,8 +30,21 @@ interface CustomAnalyticsConfig {
29
30
  }
30
31
  type AnalyticsConfig = GoogleAnalyticsConfig | MixpanelConfig | AmplitudeConfig | CustomAnalyticsConfig;
31
32
 
33
+ type BlockingAction = "add" | "remove" | "update" | "cancel" | "timeout";
34
+ type MiddlewareBlockerConfig = BlockerConfig;
35
+ interface MiddlewareContext {
36
+ action: BlockingAction;
37
+ blockerId: string;
38
+ config?: MiddlewareBlockerConfig;
39
+ timestamp: number;
40
+ prevState?: MiddlewareBlockerConfig;
41
+ }
42
+ type Middleware = (context: MiddlewareContext) => void | Promise<void>;
43
+
32
44
  declare const createAnalyticsMiddleware: (config: AnalyticsConfig) => Middleware;
33
45
 
46
+ declare const loggerMiddleware: Middleware;
47
+
34
48
  interface PerformanceConfig {
35
49
  slowBlockThreshold?: number;
36
50
  onSlowBlock?: (blockerId: string, duration: number) => void;
@@ -131,32 +145,6 @@ declare const useUIBlockingStore: zustand.UseBoundStore<Omit<zustand.StoreApi<UI
131
145
  */
132
146
  declare const useBlocker: (blockerId: string, config: BlockerConfig, isActive?: boolean) => void;
133
147
 
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
148
  /**
161
149
  * Configuration for conditional blocker hook.
162
150
  *
@@ -233,6 +221,18 @@ interface UseConfirmableBlockerReturn {
233
221
  */
234
222
  declare const useConfirmableBlocker: (blockerId: string, config: ConfirmableBlockerConfig) => UseConfirmableBlockerReturn;
235
223
 
224
+ /**
225
+ * Hook to check if a scope is currently blocked
226
+ *
227
+ * Returns a boolean indicating whether the specified scope(s) are blocked.
228
+ * Automatically re-renders when the blocking state changes.
229
+ *
230
+ * @param scope - Scope or array of scopes to check (default: "global")
231
+ * @returns True if any of the specified scopes are blocked
232
+ *
233
+ */
234
+ declare const useIsBlocked: (scope?: string | ReadonlyArray<string>) => boolean;
235
+
236
236
  /**
237
237
  * Schedule configuration for time-based blocking.
238
238
  *
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
- import { useEffect, useCallback, useRef, useState } from 'react';
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
2
  import { create } from 'zustand';
3
3
  import { devtools } from 'zustand/middleware';
4
4
 
5
- // src/hooks/useBlocker.ts
5
+ // src/hooks/useAsyncAction/useAsyncAction.ts
6
6
 
7
7
  // src/store/uiBlockingStore.constants.ts
8
8
  var DEFAULT_SCOPE = "global";
@@ -180,24 +180,7 @@ var useUIBlockingStore = create()(
180
180
  devtools(createUIBlockingActions, devtoolsConfig)
181
181
  );
182
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
- };
196
-
197
- // src/hooks/useIsBlocked.ts
198
- var useIsBlocked = (scope) => {
199
- return useUIBlockingStore((state) => state.isBlocked(scope));
200
- };
183
+ // src/hooks/useAsyncAction/useAsyncAction.ts
201
184
  var useAsyncAction = (actionId, scope) => {
202
185
  const { addBlocker, removeBlocker } = useUIBlockingStore();
203
186
  const executeWithBlocking = useCallback(
@@ -218,15 +201,20 @@ var useAsyncAction = (actionId, scope) => {
218
201
  );
219
202
  return executeWithBlocking;
220
203
  };
221
- function useConfigRef(config) {
222
- const configRef = useRef(config);
204
+ var useBlocker = (blockerId, config, isActive = true) => {
205
+ const { addBlocker, removeBlocker } = useUIBlockingStore();
223
206
  useEffect(() => {
224
- configRef.current = config;
225
- }, [config]);
226
- return configRef;
227
- }
207
+ if (!isActive || !blockerId) {
208
+ return;
209
+ }
210
+ addBlocker(blockerId, config);
211
+ return () => {
212
+ removeBlocker(blockerId);
213
+ };
214
+ }, [blockerId, isActive, addBlocker, removeBlocker]);
215
+ };
228
216
 
229
- // src/hooks/useBlocker.utils.ts
217
+ // src/hooks/useBlocker/useBlocker.utils.ts
230
218
  function createBlockerConfig(config) {
231
219
  return {
232
220
  scope: config.scope,
@@ -234,8 +222,15 @@ function createBlockerConfig(config) {
234
222
  priority: config.priority
235
223
  };
236
224
  }
225
+ function useConfigRef(config) {
226
+ const configRef = useRef(config);
227
+ useEffect(() => {
228
+ configRef.current = config;
229
+ }, [config]);
230
+ return configRef;
231
+ }
237
232
 
238
- // src/hooks/useConditionalBlocker.ts
233
+ // src/hooks/useConditionalBlocker/useConditionalBlocker.ts
239
234
  var useConditionalBlocker = (blockerId, config) => {
240
235
  const { addBlocker, removeBlocker } = useUIBlockingStore();
241
236
  const intervalRef = useRef(null);
@@ -249,11 +244,10 @@ var useConditionalBlocker = (blockerId, config) => {
249
244
  }
250
245
  if (shouldBlock) {
251
246
  addBlocker(blockerId, createBlockerConfig(currentConfig));
252
- isBlockedRef.current = true;
253
247
  } else {
254
248
  removeBlocker(blockerId);
255
- isBlockedRef.current = false;
256
249
  }
250
+ isBlockedRef.current = shouldBlock;
257
251
  }, [blockerId, addBlocker, removeBlocker, configRef]);
258
252
  useEffect(() => {
259
253
  checkCondition();
@@ -315,6 +309,50 @@ var useConfirmableBlocker = (blockerId, config) => {
315
309
  onCancel
316
310
  };
317
311
  };
312
+
313
+ // src/hooks/useIsBlocked/useIsBlocked.ts
314
+ var useIsBlocked = (scope) => {
315
+ return useUIBlockingStore((state) => state.isBlocked(scope));
316
+ };
317
+
318
+ // src/hooks/useScheduledBlocker/useScheduledBlocker.utils.ts
319
+ var MAX_TIMEOUT_MS = 2147483647;
320
+ function parseDate(date) {
321
+ return new Date(date).getTime();
322
+ }
323
+ function isValidTimestamp(timestamp) {
324
+ return !Number.isNaN(timestamp);
325
+ }
326
+ function calculateEndTime(schedule, startTime) {
327
+ if (schedule.duration) {
328
+ return startTime + schedule.duration;
329
+ }
330
+ if (schedule.end) {
331
+ const endTime = parseDate(schedule.end);
332
+ return isValidTimestamp(endTime) ? endTime : void 0;
333
+ }
334
+ return void 0;
335
+ }
336
+ function isSafeTimeout(delay) {
337
+ return delay > 0 && delay <= MAX_TIMEOUT_MS;
338
+ }
339
+ function isInBlockingPeriod(startTime, endTime, now) {
340
+ if (startTime > now) {
341
+ return false;
342
+ }
343
+ if (!endTime) {
344
+ return true;
345
+ }
346
+ return endTime > now;
347
+ }
348
+ function isScheduleInPast(startTime, endTime, now) {
349
+ if (!endTime) {
350
+ return false;
351
+ }
352
+ return endTime <= now && startTime <= now;
353
+ }
354
+
355
+ // src/hooks/useScheduledBlocker/useScheduledBlocker.ts
318
356
  var useScheduledBlocker = (blockerId, config) => {
319
357
  const { addBlocker, removeBlocker } = useUIBlockingStore();
320
358
  const timeoutsRef = useRef({});
@@ -331,41 +369,47 @@ var useScheduledBlocker = (blockerId, config) => {
331
369
  }, []);
332
370
  useEffect(() => {
333
371
  const currentConfig = configRef.current;
334
- const startTime = new Date(currentConfig.schedule.start).getTime();
372
+ const startTime = parseDate(currentConfig.schedule.start);
335
373
  const now = Date.now();
336
- let endTime;
337
- if (currentConfig.schedule.duration) {
338
- endTime = startTime + currentConfig.schedule.duration;
339
- } else if (currentConfig.schedule.end) {
340
- endTime = new Date(currentConfig.schedule.end).getTime();
374
+ if (!isValidTimestamp(startTime)) {
375
+ console.error(`[UIBlocking] Invalid start time for blocker "${blockerId}"`);
376
+ return;
341
377
  }
342
- const scheduleEnd = (end) => {
343
- const remainingTime = end - Date.now();
378
+ const scheduleEnd = () => {
379
+ const endTime = calculateEndTime(currentConfig.schedule, startTime);
380
+ if (!endTime) {
381
+ return;
382
+ }
383
+ const remainingTime = endTime - Date.now();
344
384
  if (remainingTime > 0) {
345
385
  timeoutsRef.current.end = setTimeout(() => {
346
- removeBlocker(blockerId);
347
386
  configRef.current.onScheduleEnd?.();
387
+ removeBlocker(blockerId);
348
388
  }, remainingTime);
349
389
  }
350
390
  };
391
+ const startBlocking = () => {
392
+ const cfg = configRef.current;
393
+ addBlocker(blockerId, createBlockerConfig(cfg));
394
+ cfg.onScheduleStart?.();
395
+ scheduleEnd();
396
+ };
351
397
  if (startTime > now) {
352
- timeoutsRef.current.start = setTimeout(() => {
353
- const cfg = configRef.current;
354
- addBlocker(blockerId, createBlockerConfig(cfg));
355
- cfg.onScheduleStart?.();
356
- if (endTime) {
357
- scheduleEnd(endTime);
358
- }
359
- }, startTime - now);
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?.();
398
+ const delay = startTime - now;
399
+ if (!isSafeTimeout(delay)) {
400
+ console.warn(
401
+ `[UIBlocking] Schedule delay exceeds maximum timeout (${MAX_TIMEOUT_MS.toString()}ms) for blocker "${blockerId}"`
402
+ );
403
+ return;
404
+ }
405
+ timeoutsRef.current.start = setTimeout(startBlocking, delay);
367
406
  } else {
368
- console.warn(`[UIBlocking] Schedule is in the past for blocker "${blockerId}"`);
407
+ const endTime = calculateEndTime(currentConfig.schedule, startTime);
408
+ if (isInBlockingPeriod(startTime, endTime, now)) {
409
+ startBlocking();
410
+ } else if (isScheduleInPast(startTime, endTime, now)) {
411
+ console.warn(`[UIBlocking] Schedule is in the past for blocker "${blockerId}"`);
412
+ }
369
413
  }
370
414
  return () => {
371
415
  cleanup();
@@ -374,56 +418,6 @@ var useScheduledBlocker = (blockerId, config) => {
374
418
  }, [blockerId, addBlocker, removeBlocker, cleanup]);
375
419
  };
376
420
 
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
421
  // src/middleware/analyticsMiddleware.utils.ts
428
422
  var capitalizeFirst = (str) => {
429
423
  return str.charAt(0).toUpperCase() + str.slice(1);
@@ -494,6 +488,56 @@ var createAnalyticsMiddleware = (config) => {
494
488
  };
495
489
  };
496
490
 
491
+ // src/middleware/loggerMiddleware.utils.ts
492
+ var getActionEmoji = (action) => {
493
+ switch (action) {
494
+ case "add":
495
+ return "\u2795";
496
+ case "remove":
497
+ return "\u2796";
498
+ case "update":
499
+ return "\u{1F504}";
500
+ case "cancel":
501
+ return "\u274C";
502
+ case "timeout":
503
+ return "\u23F1\uFE0F";
504
+ default:
505
+ return "\u2753";
506
+ }
507
+ };
508
+ var extractConfigDetails = (config) => {
509
+ if (!config) {
510
+ return {};
511
+ }
512
+ const { scope, reason, priority } = config;
513
+ const details = {
514
+ scope,
515
+ reason
516
+ };
517
+ if (priority !== void 0) {
518
+ details.priority = priority;
519
+ }
520
+ return details;
521
+ };
522
+ var formatLogData = (context) => {
523
+ const configDetails = extractConfigDetails(context.config);
524
+ if (context.prevState) {
525
+ return {
526
+ config: configDetails,
527
+ prevState: extractConfigDetails(context.prevState)
528
+ };
529
+ }
530
+ return configDetails;
531
+ };
532
+
533
+ // src/middleware/loggerMiddleware.ts
534
+ var LOG_PREFIX = "[UIBlocking]";
535
+ var loggerMiddleware = (context) => {
536
+ const emoji = getActionEmoji(context.action);
537
+ const logData = formatLogData(context);
538
+ console.log(LOG_PREFIX, emoji, context.blockerId, logData);
539
+ };
540
+
497
541
  // src/middleware/performanceMiddleware.utils.ts
498
542
  var handleAddAction = (blockerId, timestamp, blockStartTimes) => {
499
543
  blockStartTimes.set(blockerId, timestamp);
@@ -511,7 +555,9 @@ var handleRemoveAction = (context, blockStartTimes, slowBlockThreshold, onSlowBl
511
555
  const duration = context.timestamp - startTime;
512
556
  blockStartTimes.delete(context.blockerId);
513
557
  if (duration >= slowBlockThreshold) {
514
- console.warn(`[UIBlocking] Slow block detected: "${context.blockerId}" took ${duration.toString()}ms`);
558
+ console.warn(
559
+ `[UIBlocking] Slow block detected: "${context.blockerId}" took ${duration.toString()}ms`
560
+ );
515
561
  onSlowBlock?.(context.blockerId, duration);
516
562
  }
517
563
  };