@okyrychenko-dev/react-action-guard 0.5.0 → 0.6.1

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.cjs CHANGED
@@ -24,6 +24,16 @@ function normalizeScopeToArray(scope) {
24
24
  }
25
25
 
26
26
  // src/store/uiBlockingStore.actions.ts
27
+ var normalizePriority = (priority, fallback = DEFAULT_PRIORITY) => {
28
+ return priority !== void 0 ? Math.max(0, priority) : fallback;
29
+ };
30
+ var createMiddlewareContext = (action, blockerId, config, prevState) => ({
31
+ action,
32
+ blockerId,
33
+ config,
34
+ timestamp: Date.now(),
35
+ ...prevState && { prevState }
36
+ });
27
37
  var createUIBlockingActions = (set, get) => ({
28
38
  // State
29
39
  activeBlockers: /* @__PURE__ */ new Map(),
@@ -34,7 +44,6 @@ var createUIBlockingActions = (set, get) => ({
34
44
  newMiddlewares.set(name, middleware);
35
45
  return { middlewares: newMiddlewares };
36
46
  });
37
- console.log(`[UIBlocking] Registered middleware: ${name}`);
38
47
  },
39
48
  unregisterMiddleware: (name) => {
40
49
  set((state) => {
@@ -42,7 +51,6 @@ var createUIBlockingActions = (set, get) => ({
42
51
  newMiddlewares.delete(name);
43
52
  return { middlewares: newMiddlewares };
44
53
  });
45
- console.log(`[UIBlocking] Unregistered middleware: ${name}`);
46
54
  },
47
55
  runMiddlewares: async (context) => {
48
56
  const { middlewares } = get();
@@ -56,11 +64,38 @@ var createUIBlockingActions = (set, get) => ({
56
64
  },
57
65
  // Actions
58
66
  /**
59
- * Add a blocker to the store
60
- *
61
- * @param id - Unique blocker identifier
62
- * @param config - Optional blocker configuration
63
- *
67
+ * Adds a new blocker or overwrites an existing one.
68
+ *
69
+ * If a blocker with the same ID already exists, it will be replaced and any
70
+ * existing timeout will be cleared. The blocker is active immediately after
71
+ * being added. Triggers middleware with 'add' action.
72
+ *
73
+ * @param id - Unique identifier for the blocker. Must be unique within the store.
74
+ * @param config - Blocker configuration. All fields are optional with defaults applied.
75
+ *
76
+ * @example
77
+ * Basic usage
78
+ * ```ts
79
+ * store.getState().addBlocker('save-operation', {
80
+ * scope: 'form',
81
+ * reason: 'Saving data...',
82
+ * priority: 70
83
+ * });
84
+ * ```
85
+ *
86
+ * @example
87
+ * With timeout
88
+ * ```ts
89
+ * store.getState().addBlocker('api-call', {
90
+ * scope: 'global',
91
+ * reason: 'Loading...',
92
+ * timeout: 30000, // Auto-remove after 30 seconds
93
+ * onTimeout: (id) => console.warn(`${id} timed out`)
94
+ * });
95
+ * ```
96
+ *
97
+ * @see {@link removeBlocker} to remove a blocker
98
+ * @see {@link updateBlocker} to update an existing blocker
64
99
  */
65
100
  addBlocker: (id, config = {}) => {
66
101
  const existingBlocker = get().activeBlockers.get(id);
@@ -72,17 +107,14 @@ var createUIBlockingActions = (set, get) => ({
72
107
  timeoutId = setTimeout(() => {
73
108
  if (get().activeBlockers.has(id)) {
74
109
  config.onTimeout?.(id);
75
- void get().runMiddlewares({
76
- action: "timeout",
77
- blockerId: id,
78
- config: {
110
+ void get().runMiddlewares(
111
+ createMiddlewareContext("timeout", id, {
79
112
  scope: config.scope,
80
113
  reason: config.reason,
81
114
  priority: config.priority,
82
115
  timeout: config.timeout
83
- },
84
- timestamp: Date.now()
85
- });
116
+ })
117
+ );
86
118
  get().removeBlocker(id);
87
119
  }
88
120
  }, config.timeout);
@@ -92,7 +124,7 @@ var createUIBlockingActions = (set, get) => ({
92
124
  const storedBlocker = {
93
125
  scope: config.scope ?? DEFAULT_SCOPE,
94
126
  reason: config.reason ?? DEFAULT_REASON,
95
- priority: config.priority ?? DEFAULT_PRIORITY,
127
+ priority: normalizePriority(config.priority),
96
128
  timestamp: config.timestamp ?? Date.now(),
97
129
  timeout: config.timeout,
98
130
  timeoutId,
@@ -101,23 +133,131 @@ var createUIBlockingActions = (set, get) => ({
101
133
  newBlockers.set(id, storedBlocker);
102
134
  return { activeBlockers: newBlockers };
103
135
  });
104
- void get().runMiddlewares({
105
- action: "add",
106
- blockerId: id,
107
- config: {
136
+ void get().runMiddlewares(
137
+ createMiddlewareContext("add", id, {
108
138
  scope: config.scope,
109
139
  reason: config.reason,
110
140
  priority: config.priority,
111
141
  timeout: config.timeout
112
- },
113
- timestamp: Date.now()
142
+ })
143
+ );
144
+ },
145
+ /**
146
+ * Updates an existing blocker or creates it if it doesn't exist.
147
+ *
148
+ * Merges the provided configuration with existing blocker data. If the timeout
149
+ * is changed, the timeout timer is restarted. Triggers middleware with 'update' action.
150
+ *
151
+ * @param id - Identifier of the blocker to update
152
+ * @param config - Partial configuration to merge with existing blocker data
153
+ *
154
+ * @example
155
+ * Update blocker reason
156
+ * ```ts
157
+ * store.getState().updateBlocker('save', {
158
+ * reason: 'Saving additional data...'
159
+ * });
160
+ * ```
161
+ *
162
+ * @example
163
+ * Extend timeout
164
+ * ```ts
165
+ * store.getState().updateBlocker('api-call', {
166
+ * timeout: 60000 // Extend to 60 seconds
167
+ * });
168
+ * ```
169
+ *
170
+ * @see {@link addBlocker} to create a new blocker
171
+ */
172
+ updateBlocker: (id, config = {}) => {
173
+ const existingBlocker = get().activeBlockers.get(id);
174
+ if (!existingBlocker) {
175
+ get().addBlocker(id, config);
176
+ return;
177
+ }
178
+ let timeoutId = existingBlocker.timeoutId;
179
+ const timeoutChanged = config.timeout !== void 0 && config.timeout !== existingBlocker.timeout;
180
+ if (timeoutChanged) {
181
+ if (existingBlocker.timeoutId) {
182
+ clearTimeout(existingBlocker.timeoutId);
183
+ }
184
+ if (config.timeout && config.timeout > 0) {
185
+ const newTimeout = config.timeout;
186
+ const newOnTimeout = config.onTimeout ?? existingBlocker.onTimeout;
187
+ timeoutId = setTimeout(() => {
188
+ if (get().activeBlockers.has(id)) {
189
+ newOnTimeout?.(id);
190
+ void get().runMiddlewares(
191
+ createMiddlewareContext("timeout", id, {
192
+ scope: config.scope ?? existingBlocker.scope,
193
+ reason: config.reason ?? existingBlocker.reason,
194
+ priority: config.priority ?? existingBlocker.priority,
195
+ timeout: newTimeout
196
+ })
197
+ );
198
+ get().removeBlocker(id);
199
+ }
200
+ }, newTimeout);
201
+ } else {
202
+ timeoutId = void 0;
203
+ }
204
+ }
205
+ const updatedBlocker = {
206
+ ...existingBlocker,
207
+ scope: config.scope ?? existingBlocker.scope,
208
+ reason: config.reason ?? existingBlocker.reason,
209
+ priority: normalizePriority(config.priority, existingBlocker.priority),
210
+ timestamp: config.timestamp ?? existingBlocker.timestamp,
211
+ timeout: config.timeout ?? existingBlocker.timeout,
212
+ timeoutId,
213
+ onTimeout: config.onTimeout ?? existingBlocker.onTimeout
214
+ };
215
+ set((state) => {
216
+ const newBlockers = new Map(state.activeBlockers);
217
+ newBlockers.set(id, updatedBlocker);
218
+ return { activeBlockers: newBlockers };
114
219
  });
220
+ void get().runMiddlewares(
221
+ createMiddlewareContext(
222
+ "update",
223
+ id,
224
+ {
225
+ scope: updatedBlocker.scope,
226
+ reason: updatedBlocker.reason,
227
+ priority: updatedBlocker.priority,
228
+ timeout: updatedBlocker.timeout,
229
+ onTimeout: updatedBlocker.onTimeout,
230
+ timestamp: updatedBlocker.timestamp
231
+ },
232
+ {
233
+ scope: existingBlocker.scope,
234
+ reason: existingBlocker.reason,
235
+ priority: existingBlocker.priority,
236
+ timeout: existingBlocker.timeout,
237
+ onTimeout: existingBlocker.onTimeout,
238
+ timestamp: existingBlocker.timestamp
239
+ }
240
+ )
241
+ );
115
242
  },
116
243
  /**
117
- * Remove a blocker from the store
118
- *
119
- * @param id - Blocker identifier to remove
120
- *
244
+ * Removes a blocker from the store.
245
+ *
246
+ * Clears any associated timeout and triggers middleware with 'remove' action.
247
+ * Safe to call even if the blocker doesn't exist (no-op in that case).
248
+ *
249
+ * @param id - Identifier of the blocker to remove
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * // Add and remove a blocker
254
+ * store.getState().addBlocker('temp', { scope: 'form' });
255
+ * // ... later
256
+ * store.getState().removeBlocker('temp');
257
+ * ```
258
+ *
259
+ * @see {@link addBlocker} to add a blocker
260
+ * @see {@link clearAllBlockers} to remove all blockers at once
121
261
  */
122
262
  removeBlocker: (id) => {
123
263
  const prevBlocker = get().activeBlockers.get(id);
@@ -130,21 +270,34 @@ var createUIBlockingActions = (set, get) => ({
130
270
  return { activeBlockers: newBlockers };
131
271
  });
132
272
  if (prevBlocker) {
133
- void get().runMiddlewares({
134
- action: "remove",
135
- blockerId: id,
136
- config: prevBlocker,
137
- timestamp: Date.now(),
138
- prevState: prevBlocker
139
- });
273
+ void get().runMiddlewares(createMiddlewareContext("remove", id, prevBlocker, prevBlocker));
140
274
  }
141
275
  },
142
276
  /**
143
- * Check if a scope is currently blocked
144
- *
145
- * @param scope - Scope or array of scopes to check (default: "global")
146
- * @returns True if any of the scopes are blocked
147
- *
277
+ * Checks if one or more scopes are currently blocked.
278
+ *
279
+ * Returns true if ANY blocker affects the specified scope(s). Global scope
280
+ * blockers will cause this to return true for any scope check.
281
+ *
282
+ * @param scope - Scope(s) to check. Defaults to "global" if not specified.
283
+ * Can be a single scope string or array of scopes.
284
+ * @returns true if any of the specified scopes are currently blocked
285
+ *
286
+ * @example
287
+ * Single scope check
288
+ * ```ts
289
+ * const isFormBlocked = store.getState().isBlocked('form');
290
+ * console.log(isFormBlocked); // true or false
291
+ * ```
292
+ *
293
+ * @example
294
+ * Multiple scopes check
295
+ * ```ts
296
+ * const isBlocked = store.getState().isBlocked(['form', 'navigation']);
297
+ * // Returns true if either 'form' OR 'navigation' has active blockers
298
+ * ```
299
+ *
300
+ * @see {@link getBlockingInfo} for detailed blocker information
148
301
  */
149
302
  isBlocked: (scope = DEFAULT_SCOPE) => {
150
303
  const { activeBlockers } = get();
@@ -161,11 +314,37 @@ var createUIBlockingActions = (set, get) => ({
161
314
  return false;
162
315
  },
163
316
  /**
164
- * Get detailed blocking information for a specific scope
165
- *
317
+ * Gets detailed information about all blockers affecting a specific scope.
318
+ *
319
+ * Returns an array of blocker objects sorted by priority (highest first).
320
+ * Includes global blockers and scope-specific blockers. Each blocker object
321
+ * contains id, reason, priority, scope, timestamp, and timeout information.
322
+ *
166
323
  * @param scope - Scope to get blocking information for
167
- * @returns Array of blockers affecting the scope, sorted by priority (highest first)
168
- *
324
+ * @returns Array of {@link BlockerInfo} objects, sorted by priority descending
325
+ *
326
+ * @example
327
+ * Display blocker details
328
+ * ```ts
329
+ * const blockers = store.getState().getBlockingInfo('form');
330
+ * blockers.forEach(blocker => {
331
+ * console.log(`${blocker.id}: ${blocker.reason}`);
332
+ * console.log(`Priority: ${blocker.priority}`);
333
+ * });
334
+ * ```
335
+ *
336
+ * @example
337
+ * Get highest priority blocker
338
+ * ```ts
339
+ * const blockers = store.getState().getBlockingInfo('navigation');
340
+ * const topBlocker = blockers[0]; // Highest priority
341
+ * if (topBlocker) {
342
+ * console.log(`Top blocker: ${topBlocker.reason}`);
343
+ * }
344
+ * ```
345
+ *
346
+ * @see {@link isBlocked} for a simple boolean check
347
+ * @see {@link BlockerInfo} for the structure of returned objects
169
348
  */
170
349
  getBlockingInfo: (scope) => {
171
350
  const { activeBlockers } = get();
@@ -179,29 +358,71 @@ var createUIBlockingActions = (set, get) => ({
179
358
  return blockers.sort((a, b) => b.priority - a.priority);
180
359
  },
181
360
  /**
182
- * Clear all blockers from the store
361
+ * Removes all active blockers from the store.
362
+ *
363
+ * Clears all timeouts and triggers middleware with 'clear' action.
364
+ * Useful for resetting the blocking state.
365
+ *
366
+ * @example
367
+ * ```ts
368
+ * // Clear all blockers on logout
369
+ * store.getState().clearAllBlockers();
370
+ * ```
371
+ *
372
+ * @see {@link clearBlockersForScope} to clear blockers for a specific scope
183
373
  */
184
374
  clearAllBlockers: () => {
185
375
  const { activeBlockers } = get();
376
+ const count = activeBlockers.size;
186
377
  for (const [, blocker] of activeBlockers) {
187
378
  if (blocker.timeoutId) {
188
379
  clearTimeout(blocker.timeoutId);
189
380
  }
190
381
  }
191
382
  set({ activeBlockers: /* @__PURE__ */ new Map() });
383
+ if (count > 0) {
384
+ void get().runMiddlewares({
385
+ action: "clear",
386
+ blockerId: "*",
387
+ timestamp: Date.now(),
388
+ count
389
+ });
390
+ }
192
391
  },
193
392
  /**
194
- * Clear all blockers for a specific scope
195
- *
393
+ * Removes all blockers that affect a specific scope.
394
+ *
395
+ * Clears timeouts for removed blockers and triggers middleware with
396
+ * 'clear_scope' action. Global scope blockers are NOT cleared by this method.
397
+ *
196
398
  * @param scope - Scope to clear blockers for
197
- *
399
+ *
400
+ * @example
401
+ * Clear form blockers
402
+ * ```ts
403
+ * // Clear all blockers affecting 'form' scope
404
+ * store.getState().clearBlockersForScope('form');
405
+ * ```
406
+ *
407
+ * @example
408
+ * Clear on navigation
409
+ * ```ts
410
+ * // Clear navigation blockers when user navigates away
411
+ * store.getState().clearBlockersForScope('navigation');
412
+ * ```
413
+ *
414
+ * @see {@link clearAllBlockers} to clear all blockers
198
415
  */
199
416
  clearBlockersForScope: (scope) => {
200
417
  const { activeBlockers } = get();
418
+ let count = 0;
201
419
  for (const [, blocker] of activeBlockers) {
202
420
  const blockerScopes = normalizeScopeToArray(blocker.scope);
203
- if (blockerScopes.includes(scope) && blocker.timeoutId) {
204
- clearTimeout(blocker.timeoutId);
421
+ if (blockerScopes.includes(scope)) {
422
+ if (blocker.timeoutId) {
423
+ clearTimeout(blocker.timeoutId);
424
+ }
425
+ count++;
205
426
  }
206
427
  }
207
428
  set((state) => {
@@ -214,6 +435,15 @@ var createUIBlockingActions = (set, get) => ({
214
435
  }
215
436
  return { activeBlockers: newBlockers };
216
437
  });
438
+ if (count > 0) {
439
+ void get().runMiddlewares({
440
+ action: "clear_scope",
441
+ blockerId: "*",
442
+ timestamp: Date.now(),
443
+ scope,
444
+ count
445
+ });
446
+ }
217
447
  }
218
448
  });
219
449
  var {
@@ -273,9 +503,11 @@ var useAsyncAction = (actionId, scope, options) => {
273
503
  addBlocker: state.addBlocker,
274
504
  removeBlocker: state.removeBlocker
275
505
  }));
506
+ const counterRef = react.useRef(0);
276
507
  const executeWithBlocking = react.useCallback(
277
508
  async (asyncFn) => {
278
- const blockerId = `${actionId}-${Date.now().toString()}`;
509
+ counterRef.current += 1;
510
+ const blockerId = `${actionId}-${String(counterRef.current)}`;
279
511
  try {
280
512
  addBlocker(blockerId, {
281
513
  scope,
@@ -610,14 +842,16 @@ var getActionEmoji = (action) => {
610
842
  switch (action) {
611
843
  case "add":
612
844
  return "\u2795";
613
- case "remove":
614
- return "\u2796";
615
845
  case "update":
616
846
  return "\u{1F504}";
617
- case "cancel":
618
- return "\u274C";
847
+ case "remove":
848
+ return "\u2796";
619
849
  case "timeout":
620
850
  return "\u23F1\uFE0F";
851
+ case "clear":
852
+ return "\u{1F9F9}";
853
+ case "clear_scope":
854
+ return "\u{1F3AF}";
621
855
  default:
622
856
  return "\u2753";
623
857
  }
@@ -638,13 +872,23 @@ var extractConfigDetails = (config) => {
638
872
  };
639
873
  var formatLogData = (context) => {
640
874
  const configDetails = extractConfigDetails(context.config);
875
+ const logData = {};
876
+ if (context.scope !== void 0) {
877
+ logData.scope = context.scope;
878
+ }
879
+ if (context.count !== void 0) {
880
+ logData.count = context.count;
881
+ }
882
+ if (Object.keys(configDetails).length > 0) {
883
+ logData.config = configDetails;
884
+ }
641
885
  if (context.prevState) {
642
- return {
643
- config: configDetails,
644
- prevState: extractConfigDetails(context.prevState)
645
- };
886
+ logData.prevState = extractConfigDetails(context.prevState);
887
+ }
888
+ if (Object.keys(logData).length === 0) {
889
+ return configDetails;
646
890
  }
647
- return configDetails;
891
+ return Object.keys(logData).length === 1 && logData.config ? configDetails : logData;
648
892
  };
649
893
 
650
894
  // src/middleware/loggerMiddleware.ts