@okyrychenko-dev/react-action-guard 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -1,8 +1,11 @@
1
- import { createContext, useRef, useMemo, useContext, useCallback, useEffect, useState } from 'react';
2
- import { createStore, useStore } from 'zustand';
3
- import { devtools } from 'zustand/middleware';
4
- import { useShallow } from 'zustand/shallow';
1
+ import { useRef, useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { createStoreProvider, createShallowStore } from '@okyrychenko-dev/react-zustand-toolkit';
3
+ export { createShallowStore } from '@okyrychenko-dev/react-zustand-toolkit';
4
+ import { useStore } from 'zustand';
5
+ import 'zustand/react/shallow';
5
6
  import { jsx } from 'react/jsx-runtime';
7
+ import { useShallow } from 'zustand/shallow';
8
+ import { devtools } from 'zustand/middleware';
6
9
 
7
10
  // src/hooks/useAsyncAction/useAsyncAction.ts
8
11
 
@@ -12,13 +15,6 @@ var DEFAULT_REASON = "Unknown";
12
15
  var DEFAULT_PRIORITY = 0;
13
16
  var ASYNC_ACTION_PRIORITY = 20;
14
17
  var DEVTOOLS_NAME = "UIBlocking";
15
- function createShallowStore(storeCreator) {
16
- const storeApi = createStore(storeCreator);
17
- const useScopedStore = (selector) => {
18
- return useStore(storeApi, useShallow(selector));
19
- };
20
- return { useStore: useScopedStore, useStoreApi: storeApi };
21
- }
22
18
  function normalizeScopeToArray(scope) {
23
19
  if (typeof scope === "string") {
24
20
  return [scope];
@@ -27,6 +23,16 @@ function normalizeScopeToArray(scope) {
27
23
  }
28
24
 
29
25
  // src/store/uiBlockingStore.actions.ts
26
+ var normalizePriority = (priority, fallback = DEFAULT_PRIORITY) => {
27
+ return priority !== void 0 ? Math.max(0, priority) : fallback;
28
+ };
29
+ var createMiddlewareContext = (action, blockerId, config, prevState) => ({
30
+ action,
31
+ blockerId,
32
+ config,
33
+ timestamp: Date.now(),
34
+ ...prevState && { prevState }
35
+ });
30
36
  var createUIBlockingActions = (set, get) => ({
31
37
  // State
32
38
  activeBlockers: /* @__PURE__ */ new Map(),
@@ -37,7 +43,6 @@ var createUIBlockingActions = (set, get) => ({
37
43
  newMiddlewares.set(name, middleware);
38
44
  return { middlewares: newMiddlewares };
39
45
  });
40
- console.log(`[UIBlocking] Registered middleware: ${name}`);
41
46
  },
42
47
  unregisterMiddleware: (name) => {
43
48
  set((state) => {
@@ -45,7 +50,6 @@ var createUIBlockingActions = (set, get) => ({
45
50
  newMiddlewares.delete(name);
46
51
  return { middlewares: newMiddlewares };
47
52
  });
48
- console.log(`[UIBlocking] Unregistered middleware: ${name}`);
49
53
  },
50
54
  runMiddlewares: async (context) => {
51
55
  const { middlewares } = get();
@@ -75,17 +79,14 @@ var createUIBlockingActions = (set, get) => ({
75
79
  timeoutId = setTimeout(() => {
76
80
  if (get().activeBlockers.has(id)) {
77
81
  config.onTimeout?.(id);
78
- void get().runMiddlewares({
79
- action: "timeout",
80
- blockerId: id,
81
- config: {
82
+ void get().runMiddlewares(
83
+ createMiddlewareContext("timeout", id, {
82
84
  scope: config.scope,
83
85
  reason: config.reason,
84
86
  priority: config.priority,
85
87
  timeout: config.timeout
86
- },
87
- timestamp: Date.now()
88
- });
88
+ })
89
+ );
89
90
  get().removeBlocker(id);
90
91
  }
91
92
  }, config.timeout);
@@ -95,7 +96,7 @@ var createUIBlockingActions = (set, get) => ({
95
96
  const storedBlocker = {
96
97
  scope: config.scope ?? DEFAULT_SCOPE,
97
98
  reason: config.reason ?? DEFAULT_REASON,
98
- priority: config.priority ?? DEFAULT_PRIORITY,
99
+ priority: normalizePriority(config.priority),
99
100
  timestamp: config.timestamp ?? Date.now(),
100
101
  timeout: config.timeout,
101
102
  timeoutId,
@@ -104,17 +105,92 @@ var createUIBlockingActions = (set, get) => ({
104
105
  newBlockers.set(id, storedBlocker);
105
106
  return { activeBlockers: newBlockers };
106
107
  });
107
- void get().runMiddlewares({
108
- action: "add",
109
- blockerId: id,
110
- config: {
108
+ void get().runMiddlewares(
109
+ createMiddlewareContext("add", id, {
111
110
  scope: config.scope,
112
111
  reason: config.reason,
113
112
  priority: config.priority,
114
113
  timeout: config.timeout
115
- },
116
- timestamp: Date.now()
114
+ })
115
+ );
116
+ },
117
+ /**
118
+ * Update blocker metadata. If timeout is updated, the timer will be restarted.
119
+ *
120
+ * @param id - Unique blocker identifier
121
+ * @param config - Partial blocker configuration to merge
122
+ *
123
+ */
124
+ updateBlocker: (id, config = {}) => {
125
+ const existingBlocker = get().activeBlockers.get(id);
126
+ if (!existingBlocker) {
127
+ get().addBlocker(id, config);
128
+ return;
129
+ }
130
+ let timeoutId = existingBlocker.timeoutId;
131
+ const timeoutChanged = config.timeout !== void 0 && config.timeout !== existingBlocker.timeout;
132
+ if (timeoutChanged) {
133
+ if (existingBlocker.timeoutId) {
134
+ clearTimeout(existingBlocker.timeoutId);
135
+ }
136
+ if (config.timeout && config.timeout > 0) {
137
+ const newTimeout = config.timeout;
138
+ const newOnTimeout = config.onTimeout ?? existingBlocker.onTimeout;
139
+ timeoutId = setTimeout(() => {
140
+ if (get().activeBlockers.has(id)) {
141
+ newOnTimeout?.(id);
142
+ void get().runMiddlewares(
143
+ createMiddlewareContext("timeout", id, {
144
+ scope: config.scope ?? existingBlocker.scope,
145
+ reason: config.reason ?? existingBlocker.reason,
146
+ priority: config.priority ?? existingBlocker.priority,
147
+ timeout: newTimeout
148
+ })
149
+ );
150
+ get().removeBlocker(id);
151
+ }
152
+ }, newTimeout);
153
+ } else {
154
+ timeoutId = void 0;
155
+ }
156
+ }
157
+ const updatedBlocker = {
158
+ ...existingBlocker,
159
+ scope: config.scope ?? existingBlocker.scope,
160
+ reason: config.reason ?? existingBlocker.reason,
161
+ priority: normalizePriority(config.priority, existingBlocker.priority),
162
+ timestamp: config.timestamp ?? existingBlocker.timestamp,
163
+ timeout: config.timeout ?? existingBlocker.timeout,
164
+ timeoutId,
165
+ onTimeout: config.onTimeout ?? existingBlocker.onTimeout
166
+ };
167
+ set((state) => {
168
+ const newBlockers = new Map(state.activeBlockers);
169
+ newBlockers.set(id, updatedBlocker);
170
+ return { activeBlockers: newBlockers };
117
171
  });
172
+ void get().runMiddlewares(
173
+ createMiddlewareContext(
174
+ "update",
175
+ id,
176
+ {
177
+ scope: updatedBlocker.scope,
178
+ reason: updatedBlocker.reason,
179
+ priority: updatedBlocker.priority,
180
+ timeout: updatedBlocker.timeout,
181
+ onTimeout: updatedBlocker.onTimeout,
182
+ timestamp: updatedBlocker.timestamp
183
+ },
184
+ {
185
+ scope: existingBlocker.scope,
186
+ reason: existingBlocker.reason,
187
+ priority: existingBlocker.priority,
188
+ timeout: existingBlocker.timeout,
189
+ onTimeout: existingBlocker.onTimeout,
190
+ timestamp: existingBlocker.timestamp
191
+ }
192
+ )
193
+ );
118
194
  },
119
195
  /**
120
196
  * Remove a blocker from the store
@@ -133,13 +209,7 @@ var createUIBlockingActions = (set, get) => ({
133
209
  return { activeBlockers: newBlockers };
134
210
  });
135
211
  if (prevBlocker) {
136
- void get().runMiddlewares({
137
- action: "remove",
138
- blockerId: id,
139
- config: prevBlocker,
140
- timestamp: Date.now(),
141
- prevState: prevBlocker
142
- });
212
+ void get().runMiddlewares(createMiddlewareContext("remove", id, prevBlocker, prevBlocker));
143
213
  }
144
214
  },
145
215
  /**
@@ -186,12 +256,21 @@ var createUIBlockingActions = (set, get) => ({
186
256
  */
187
257
  clearAllBlockers: () => {
188
258
  const { activeBlockers } = get();
259
+ const count = activeBlockers.size;
189
260
  for (const [, blocker] of activeBlockers) {
190
261
  if (blocker.timeoutId) {
191
262
  clearTimeout(blocker.timeoutId);
192
263
  }
193
264
  }
194
265
  set({ activeBlockers: /* @__PURE__ */ new Map() });
266
+ if (count > 0) {
267
+ void get().runMiddlewares({
268
+ action: "clear",
269
+ blockerId: "*",
270
+ timestamp: Date.now(),
271
+ count
272
+ });
273
+ }
195
274
  },
196
275
  /**
197
276
  * Clear all blockers for a specific scope
@@ -201,10 +280,14 @@ var createUIBlockingActions = (set, get) => ({
201
280
  */
202
281
  clearBlockersForScope: (scope) => {
203
282
  const { activeBlockers } = get();
283
+ let count = 0;
204
284
  for (const [, blocker] of activeBlockers) {
205
285
  const blockerScopes = normalizeScopeToArray(blocker.scope);
206
- if (blockerScopes.includes(scope) && blocker.timeoutId) {
207
- clearTimeout(blocker.timeoutId);
286
+ if (blockerScopes.includes(scope)) {
287
+ if (blocker.timeoutId) {
288
+ clearTimeout(blocker.timeoutId);
289
+ }
290
+ count++;
208
291
  }
209
292
  }
210
293
  set((state) => {
@@ -217,46 +300,44 @@ var createUIBlockingActions = (set, get) => ({
217
300
  }
218
301
  return { activeBlockers: newBlockers };
219
302
  });
303
+ if (count > 0) {
304
+ void get().runMiddlewares({
305
+ action: "clear_scope",
306
+ blockerId: "*",
307
+ timestamp: Date.now(),
308
+ scope,
309
+ count
310
+ });
311
+ }
220
312
  }
221
313
  });
222
- var UIBlockingContext = createContext(null);
314
+ var {
315
+ Provider: BaseUIBlockingProvider,
316
+ useContext: useUIBlockingContext,
317
+ useContextStore: useUIBlockingStoreFromContext,
318
+ useIsInsideProvider: useIsInsideUIBlockingProvider,
319
+ useOptionalContext
320
+ } = createStoreProvider(createUIBlockingActions, "UIBlocking");
223
321
  function UIBlockingProvider({
224
322
  children,
225
323
  enableDevtools = process.env.NODE_ENV === "development",
226
324
  devtoolsName = "UIBlocking",
227
325
  middlewares = []
228
326
  }) {
229
- const middlewaresRef = useRef(middlewares);
230
- const store = useMemo(() => {
231
- const baseStore = enableDevtools ? createStore(
232
- devtools(createUIBlockingActions, {
233
- name: devtoolsName,
234
- enabled: enableDevtools
235
- })
236
- ) : createStore(createUIBlockingActions);
237
- middlewaresRef.current.forEach((mw, index) => {
238
- baseStore.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
327
+ const handleStoreCreate = (store) => {
328
+ middlewares.forEach((mw, index) => {
329
+ store.getState().registerMiddleware(`provider-middleware-${index.toString()}`, mw);
239
330
  });
240
- return baseStore;
241
- }, [enableDevtools, devtoolsName]);
242
- return /* @__PURE__ */ jsx(UIBlockingContext.Provider, { value: store, children });
243
- }
244
- function useUIBlockingContext() {
245
- const store = useContext(UIBlockingContext);
246
- if (!store) {
247
- throw new Error(
248
- "useUIBlockingContext must be used within UIBlockingProvider. Either wrap your app in <UIBlockingProvider> or use the global store via useUIBlockingStore."
249
- );
250
- }
251
- return store;
252
- }
253
- function useIsInsideUIBlockingProvider() {
254
- const store = useContext(UIBlockingContext);
255
- return store !== null;
256
- }
257
- function useUIBlockingStoreFromContext(selector) {
258
- const store = useUIBlockingContext();
259
- return useStore(store, useShallow(selector));
331
+ };
332
+ return /* @__PURE__ */ jsx(
333
+ BaseUIBlockingProvider,
334
+ {
335
+ enableDevtools,
336
+ devtoolsName,
337
+ onStoreCreate: handleStoreCreate,
338
+ children
339
+ }
340
+ );
260
341
  }
261
342
 
262
343
  // src/store/uiBlockingStore.config.ts
@@ -269,8 +350,11 @@ var devtoolsConfig = {
269
350
  var { useStore: useUIBlockingStore, useStoreApi: uiBlockingStoreApi } = createShallowStore(devtools(createUIBlockingActions, devtoolsConfig));
270
351
 
271
352
  // src/context/useResolvedStore.ts
353
+ function useOptionalContextStore() {
354
+ return useOptionalContext();
355
+ }
272
356
  function useResolvedStore() {
273
- const contextStore = useContext(UIBlockingContext);
357
+ const contextStore = useOptionalContextStore();
274
358
  return contextStore ?? uiBlockingStoreApi;
275
359
  }
276
360
  function useResolvedStoreWithSelector(selector) {
@@ -284,9 +368,11 @@ var useAsyncAction = (actionId, scope, options) => {
284
368
  addBlocker: state.addBlocker,
285
369
  removeBlocker: state.removeBlocker
286
370
  }));
371
+ const counterRef = useRef(0);
287
372
  const executeWithBlocking = useCallback(
288
373
  async (asyncFn) => {
289
- const blockerId = `${actionId}-${Date.now().toString()}`;
374
+ counterRef.current += 1;
375
+ const blockerId = `${actionId}-${String(counterRef.current)}`;
290
376
  try {
291
377
  addBlocker(blockerId, {
292
378
  scope,
@@ -621,14 +707,16 @@ var getActionEmoji = (action) => {
621
707
  switch (action) {
622
708
  case "add":
623
709
  return "\u2795";
624
- case "remove":
625
- return "\u2796";
626
710
  case "update":
627
711
  return "\u{1F504}";
628
- case "cancel":
629
- return "\u274C";
712
+ case "remove":
713
+ return "\u2796";
630
714
  case "timeout":
631
715
  return "\u23F1\uFE0F";
716
+ case "clear":
717
+ return "\u{1F9F9}";
718
+ case "clear_scope":
719
+ return "\u{1F3AF}";
632
720
  default:
633
721
  return "\u2753";
634
722
  }
@@ -649,13 +737,23 @@ var extractConfigDetails = (config) => {
649
737
  };
650
738
  var formatLogData = (context) => {
651
739
  const configDetails = extractConfigDetails(context.config);
740
+ const logData = {};
741
+ if (context.scope !== void 0) {
742
+ logData.scope = context.scope;
743
+ }
744
+ if (context.count !== void 0) {
745
+ logData.count = context.count;
746
+ }
747
+ if (Object.keys(configDetails).length > 0) {
748
+ logData.config = configDetails;
749
+ }
652
750
  if (context.prevState) {
653
- return {
654
- config: configDetails,
655
- prevState: extractConfigDetails(context.prevState)
656
- };
751
+ logData.prevState = extractConfigDetails(context.prevState);
752
+ }
753
+ if (Object.keys(logData).length === 0) {
754
+ return configDetails;
657
755
  }
658
- return configDetails;
756
+ return Object.keys(logData).length === 1 && logData.config ? configDetails : logData;
659
757
  };
660
758
 
661
759
  // src/middleware/loggerMiddleware.ts