@okyrychenko-dev/react-action-guard 1.0.4 → 2.0.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/CHANGELOG.md +451 -0
- package/README.md +171 -1026
- package/dist/index.cjs +796 -589
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +172 -1225
- package/dist/index.d.ts +172 -1225
- package/dist/index.js +795 -595
- package/dist/index.js.map +1 -1
- package/package.json +47 -53
package/dist/index.cjs
CHANGED
|
@@ -1,460 +1,505 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var reactZustandToolkit = require('@okyrychenko-dev/react-zustand-toolkit');
|
|
4
|
-
var
|
|
5
|
-
require('zustand/react/shallow');
|
|
6
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
-
var shallow = require('zustand/shallow');
|
|
4
|
+
var typeUtils = require('@okyrychenko-dev/type-utils');
|
|
8
5
|
var middleware = require('zustand/middleware');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
7
|
var react = require('react');
|
|
8
|
+
var shallow = require('zustand/shallow');
|
|
10
9
|
|
|
11
10
|
// src/context/UIBlockingContext.tsx
|
|
12
11
|
|
|
13
|
-
// src/store/
|
|
12
|
+
// src/store/scope/scope.constants.ts
|
|
14
13
|
var DEFAULT_SCOPE = "global";
|
|
14
|
+
var normalizedScopeCache = /* @__PURE__ */ new Map();
|
|
15
|
+
var NORMALIZED_SCOPE_CACHE_MAX_SIZE = 256;
|
|
16
|
+
function getNormalizedScopeCacheKey(scopes) {
|
|
17
|
+
return JSON.stringify(scopes);
|
|
18
|
+
}
|
|
19
|
+
function refreshNormalizedScopeCacheEntry(cacheKey, scopes) {
|
|
20
|
+
normalizedScopeCache.delete(cacheKey);
|
|
21
|
+
normalizedScopeCache.set(cacheKey, scopes);
|
|
22
|
+
return scopes;
|
|
23
|
+
}
|
|
24
|
+
function evictOldestNormalizedScopeCacheEntry() {
|
|
25
|
+
for (const cacheKey of normalizedScopeCache.keys()) {
|
|
26
|
+
normalizedScopeCache.delete(cacheKey);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function getCachedNormalizedScope(scopes) {
|
|
31
|
+
const normalizedScope = Object.freeze([...new Set(scopes)].sort());
|
|
32
|
+
const cacheKey = getNormalizedScopeCacheKey(normalizedScope);
|
|
33
|
+
const cachedScope = normalizedScopeCache.get(cacheKey);
|
|
34
|
+
if (typeUtils.isDefined(cachedScope)) {
|
|
35
|
+
return refreshNormalizedScopeCacheEntry(cacheKey, cachedScope);
|
|
36
|
+
}
|
|
37
|
+
normalizedScopeCache.set(cacheKey, normalizedScope);
|
|
38
|
+
if (normalizedScopeCache.size > NORMALIZED_SCOPE_CACHE_MAX_SIZE) {
|
|
39
|
+
evictOldestNormalizedScopeCacheEntry();
|
|
40
|
+
}
|
|
41
|
+
return normalizedScope;
|
|
42
|
+
}
|
|
43
|
+
function normalizeScope(scope) {
|
|
44
|
+
if (typeUtils.isUndefined(scope)) {
|
|
45
|
+
return getCachedNormalizedScope([DEFAULT_SCOPE]);
|
|
46
|
+
}
|
|
47
|
+
if (typeUtils.isString(scope)) {
|
|
48
|
+
return getCachedNormalizedScope([scope]);
|
|
49
|
+
}
|
|
50
|
+
return getCachedNormalizedScope(scope);
|
|
51
|
+
}
|
|
52
|
+
function resolveScope(explicitScope, inheritedScope) {
|
|
53
|
+
if (typeUtils.isUndefined(explicitScope) || typeUtils.isReadonlyArray(explicitScope) && !typeUtils.isNonEmptyArray(explicitScope)) {
|
|
54
|
+
return inheritedScope;
|
|
55
|
+
}
|
|
56
|
+
return explicitScope;
|
|
57
|
+
}
|
|
58
|
+
function matchesObservedScopes(blockerScope, observedScopes) {
|
|
59
|
+
if (!typeUtils.isNonEmptyArray(observedScopes)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (typeUtils.isString(blockerScope)) {
|
|
63
|
+
return blockerScope === DEFAULT_SCOPE || observedScopes.includes(blockerScope);
|
|
64
|
+
}
|
|
65
|
+
return blockerScope.includes(DEFAULT_SCOPE) || observedScopes.some((scope) => blockerScope.includes(scope));
|
|
66
|
+
}
|
|
67
|
+
function scopeAffectsObservation(blockerScope, observedScope) {
|
|
68
|
+
return matchesObservedScopes(blockerScope, normalizeScope(observedScope));
|
|
69
|
+
}
|
|
70
|
+
function createScopeObservationMatcher(observedScope) {
|
|
71
|
+
const observedScopes = normalizeScope(observedScope);
|
|
72
|
+
return (blockerScope) => matchesObservedScopes(blockerScope, observedScopes);
|
|
73
|
+
}
|
|
74
|
+
function scopeMatchesTarget(blockerScope, targetScope) {
|
|
75
|
+
const blockerScopes = normalizeScope(blockerScope);
|
|
76
|
+
if (blockerScopes.includes(DEFAULT_SCOPE)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const targetScopes = normalizeScope(targetScope);
|
|
80
|
+
return targetScopes.some((scope) => blockerScopes.includes(scope));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/store/blockingInfo/blockingInfo.utils.ts
|
|
84
|
+
function sameScope(first, second) {
|
|
85
|
+
if (first === second) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (typeUtils.isString(first) || typeUtils.isString(second)) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
return first.length === second.length && first.every((scope, index) => scope === second[index]);
|
|
92
|
+
}
|
|
93
|
+
function sameBlocker(first, second) {
|
|
94
|
+
if (first === second) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const { id, scope, reason, priority, timestamp, timeout, onTimeout } = first;
|
|
98
|
+
const {
|
|
99
|
+
id: previousId,
|
|
100
|
+
scope: previousScope,
|
|
101
|
+
reason: previousReason,
|
|
102
|
+
priority: previousPriority,
|
|
103
|
+
timestamp: previousTimestamp,
|
|
104
|
+
timeout: previousTimeout,
|
|
105
|
+
onTimeout: previousOnTimeout
|
|
106
|
+
} = second;
|
|
107
|
+
return id === previousId && sameScope(scope, previousScope) && reason === previousReason && priority === previousPriority && timestamp === previousTimestamp && timeout === previousTimeout && onTimeout === previousOnTimeout;
|
|
108
|
+
}
|
|
109
|
+
function areBlockingInfosEqual(first, second) {
|
|
110
|
+
return first === second || first.length === second.length && first.every((blocker, index) => {
|
|
111
|
+
const previous = second[index];
|
|
112
|
+
return typeUtils.isDefined(previous) && sameBlocker(blocker, previous);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function matchingBlockers(snapshot, matchesScope) {
|
|
116
|
+
return snapshot.filter(({ scope }) => matchesScope(scope));
|
|
117
|
+
}
|
|
118
|
+
function orderedBlockers(blockers) {
|
|
119
|
+
return Object.freeze(
|
|
120
|
+
blockers.sort(({ priority: first }, { priority: second }) => second - first)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
function projectBlockingInfo(snapshot, scope) {
|
|
124
|
+
const matchesScope = createScopeObservationMatcher(scope);
|
|
125
|
+
return orderedBlockers(matchingBlockers(snapshot, matchesScope));
|
|
126
|
+
}
|
|
127
|
+
function createBlockingInfoSelector(scope) {
|
|
128
|
+
const matchesScope = createScopeObservationMatcher(scope);
|
|
129
|
+
let previousSnapshot;
|
|
130
|
+
let previousMatches = [];
|
|
131
|
+
let result = Object.freeze([]);
|
|
132
|
+
return ({ blockingSnapshot }) => {
|
|
133
|
+
if (blockingSnapshot === previousSnapshot) {
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
const matches = matchingBlockers(blockingSnapshot, matchesScope);
|
|
137
|
+
const unchanged = areBlockingInfosEqual(matches, previousMatches);
|
|
138
|
+
previousSnapshot = blockingSnapshot;
|
|
139
|
+
if (!unchanged) {
|
|
140
|
+
previousMatches = matches;
|
|
141
|
+
result = orderedBlockers([...matches]);
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// src/store/uiBlockingStore.constants.ts
|
|
15
148
|
var DEFAULT_REASON = "Unknown";
|
|
16
149
|
var DEFAULT_PRIORITY = 0;
|
|
17
150
|
var ASYNC_ACTION_PRIORITY = 20;
|
|
18
151
|
var DEVTOOLS_NAME = "UIBlocking";
|
|
19
|
-
function
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
152
|
+
function validateTimeout(timeout) {
|
|
153
|
+
if (typeUtils.isUndefined(timeout)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (!Number.isFinite(timeout) || timeout > 0 && Date.now() + timeout > Number.MAX_SAFE_INTEGER) {
|
|
157
|
+
throw new RangeError("Blocker timeout must be finite with a safely representable deadline");
|
|
22
158
|
}
|
|
23
|
-
return Array.from(scope);
|
|
24
159
|
}
|
|
25
160
|
|
|
26
|
-
// src/store/
|
|
27
|
-
function
|
|
28
|
-
return
|
|
161
|
+
// src/store/blockingLifecycle/blockingLifecycle.ts
|
|
162
|
+
function copyScope(scope) {
|
|
163
|
+
return typeUtils.isString(scope) ? scope : Object.freeze([...scope]);
|
|
164
|
+
}
|
|
165
|
+
function publicConfig(blocker) {
|
|
166
|
+
const { scope, reason, priority, timestamp, timeout, onTimeout } = blocker;
|
|
167
|
+
return { scope, reason, priority, timestamp, timeout, onTimeout };
|
|
29
168
|
}
|
|
30
|
-
function
|
|
169
|
+
function freezeBlocker(blocker) {
|
|
170
|
+
return Object.freeze({ ...blocker, scope: copyScope(blocker.scope) });
|
|
171
|
+
}
|
|
172
|
+
function normalizeBlocker(id, config) {
|
|
31
173
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
config,
|
|
35
|
-
|
|
36
|
-
|
|
174
|
+
id,
|
|
175
|
+
scope: copyScope(config.scope ?? DEFAULT_SCOPE),
|
|
176
|
+
reason: config.reason ?? DEFAULT_REASON,
|
|
177
|
+
priority: Math.max(0, config.priority ?? DEFAULT_PRIORITY),
|
|
178
|
+
timestamp: config.timestamp ?? Date.now(),
|
|
179
|
+
timeout: config.timeout,
|
|
180
|
+
onTimeout: config.onTimeout
|
|
37
181
|
};
|
|
38
182
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
},
|
|
57
|
-
runMiddlewares: async (context) => {
|
|
58
|
-
const { middlewares } = get();
|
|
59
|
-
for (const [, middleware] of middlewares) {
|
|
183
|
+
function createBlockingLifecycle() {
|
|
184
|
+
const blockers = /* @__PURE__ */ new Map();
|
|
185
|
+
const subscribers = /* @__PURE__ */ new Map();
|
|
186
|
+
const observers = /* @__PURE__ */ new Map();
|
|
187
|
+
const pendingPublications = [];
|
|
188
|
+
let snapshot = Object.freeze([]);
|
|
189
|
+
let isDelivering = false;
|
|
190
|
+
function getSnapshot() {
|
|
191
|
+
return snapshot;
|
|
192
|
+
}
|
|
193
|
+
function deliverEvent(event2) {
|
|
194
|
+
const eventObservers = [...observers];
|
|
195
|
+
for (const [registration, observer] of eventObservers) {
|
|
196
|
+
if (!observers.has(registration)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
60
199
|
try {
|
|
61
|
-
|
|
200
|
+
void Promise.resolve(observer(event2)).catch(() => void 0);
|
|
62
201
|
} catch {
|
|
63
202
|
}
|
|
64
203
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
*
|
|
70
|
-
* If a blocker with the same ID already exists, it will be replaced and any
|
|
71
|
-
* existing timeout will be cleared. The blocker is active immediately after
|
|
72
|
-
* being added. Triggers middleware with 'add' action.
|
|
73
|
-
*
|
|
74
|
-
* @param id - Unique identifier for the blocker. Must be unique within the store.
|
|
75
|
-
* @param config - Blocker configuration. All fields are optional with defaults applied.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* Basic usage
|
|
79
|
-
* ```ts
|
|
80
|
-
* store.getState().addBlocker('save-operation', {
|
|
81
|
-
* scope: 'form',
|
|
82
|
-
* reason: 'Saving data...',
|
|
83
|
-
* priority: 70
|
|
84
|
-
* });
|
|
85
|
-
* ```
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* With timeout
|
|
89
|
-
* ```ts
|
|
90
|
-
* store.getState().addBlocker('api-call', {
|
|
91
|
-
* scope: 'global',
|
|
92
|
-
* reason: 'Loading...',
|
|
93
|
-
* timeout: 30000, // Auto-remove after 30 seconds
|
|
94
|
-
* onTimeout: (id) => console.warn(`${id} timed out`)
|
|
95
|
-
* });
|
|
96
|
-
* ```
|
|
97
|
-
*
|
|
98
|
-
* @see {@link removeBlocker} to remove a blocker
|
|
99
|
-
* @see {@link updateBlocker} to update an existing blocker
|
|
100
|
-
*/
|
|
101
|
-
addBlocker: (id, config = {}) => {
|
|
102
|
-
const existingBlocker = get().activeBlockers.get(id);
|
|
103
|
-
if (existingBlocker?.timeoutId) {
|
|
104
|
-
clearTimeout(existingBlocker.timeoutId);
|
|
204
|
+
}
|
|
205
|
+
function drainPublications() {
|
|
206
|
+
if (isDelivering) {
|
|
207
|
+
return;
|
|
105
208
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
209
|
+
isDelivering = true;
|
|
210
|
+
try {
|
|
211
|
+
for (const publication of pendingPublications) {
|
|
212
|
+
if (publication.kind === "snapshot") {
|
|
213
|
+
for (const subscriber of subscribers.values()) {
|
|
214
|
+
try {
|
|
215
|
+
subscriber(publication.snapshot);
|
|
216
|
+
} catch {
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (publication.event) {
|
|
220
|
+
deliverEvent(publication.event);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
deliverEvent(publication.event);
|
|
120
224
|
}
|
|
121
|
-
}
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
pendingPublications.length = 0;
|
|
228
|
+
isDelivering = false;
|
|
122
229
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
230
|
+
}
|
|
231
|
+
function publish(transitionEvent) {
|
|
232
|
+
snapshot = Object.freeze(Array.from(blockers.values(), ({ config }) => freezeBlocker(config)));
|
|
233
|
+
pendingPublications.push({ kind: "snapshot", snapshot, event: transitionEvent });
|
|
234
|
+
drainPublications();
|
|
235
|
+
}
|
|
236
|
+
function emit(event2) {
|
|
237
|
+
pendingPublications.push({ kind: "event", event: event2 });
|
|
238
|
+
drainPublications();
|
|
239
|
+
}
|
|
240
|
+
function event(action, blockerId) {
|
|
241
|
+
return { action, blockerId, timestamp: Date.now() };
|
|
242
|
+
}
|
|
243
|
+
function cancelTimeout(blocker) {
|
|
244
|
+
if (typeUtils.isDefined(blocker.timeoutId)) {
|
|
245
|
+
clearTimeout(blocker.timeoutId);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function handleTimeout(id, blocker) {
|
|
249
|
+
if (blockers.get(id) !== blocker) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const { onTimeout } = blocker.config;
|
|
253
|
+
try {
|
|
254
|
+
onTimeout?.(id);
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
const { registration } = blocker;
|
|
258
|
+
const { scope, reason, priority, timeout: duration } = blocker.config;
|
|
259
|
+
emit({
|
|
260
|
+
...event("timeout", id),
|
|
261
|
+
registration,
|
|
262
|
+
config: { scope, reason, priority, timeout: duration }
|
|
136
263
|
});
|
|
137
|
-
|
|
138
|
-
|
|
264
|
+
if (blockers.get(id) === blocker) {
|
|
265
|
+
remove(id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function scheduleTimeout(id, blocker) {
|
|
269
|
+
const { timeout } = blocker.config;
|
|
270
|
+
if (typeUtils.isUndefined(timeout) || timeout <= 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
let remaining = timeout;
|
|
274
|
+
function scheduleChunk() {
|
|
275
|
+
const delay = Math.min(remaining, 2147483647);
|
|
276
|
+
const startedAt = performance.now();
|
|
277
|
+
blocker.timeoutId = setTimeout(() => {
|
|
278
|
+
if (blockers.get(id) !== blocker) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
remaining -= Math.max(delay, performance.now() - startedAt);
|
|
282
|
+
if (remaining <= 0) {
|
|
283
|
+
handleTimeout(id, blocker);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
scheduleChunk();
|
|
287
|
+
}, delay);
|
|
288
|
+
}
|
|
289
|
+
scheduleChunk();
|
|
290
|
+
}
|
|
291
|
+
function add(id, config = {}) {
|
|
292
|
+
validateTimeout(config.timeout);
|
|
293
|
+
const previous = blockers.get(id);
|
|
294
|
+
if (previous) {
|
|
295
|
+
cancelTimeout(previous);
|
|
296
|
+
}
|
|
297
|
+
const blocker = {
|
|
298
|
+
config: normalizeBlocker(id, config),
|
|
299
|
+
registration: Object.freeze({})
|
|
300
|
+
};
|
|
301
|
+
blockers.set(id, blocker);
|
|
302
|
+
scheduleTimeout(id, blocker);
|
|
303
|
+
publish({
|
|
304
|
+
...event("add", id),
|
|
305
|
+
registration: blocker.registration,
|
|
306
|
+
config: {
|
|
139
307
|
scope: config.scope,
|
|
140
308
|
reason: config.reason,
|
|
141
309
|
priority: config.priority,
|
|
142
310
|
timeout: config.timeout
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
* is changed, the timeout timer is restarted. Triggers middleware with 'update' action.
|
|
151
|
-
*
|
|
152
|
-
* @param id - Identifier of the blocker to update
|
|
153
|
-
* @param config - Partial configuration to merge with existing blocker data
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* Update blocker reason
|
|
157
|
-
* ```ts
|
|
158
|
-
* store.getState().updateBlocker('save', {
|
|
159
|
-
* reason: 'Saving additional data...'
|
|
160
|
-
* });
|
|
161
|
-
* ```
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* Extend timeout
|
|
165
|
-
* ```ts
|
|
166
|
-
* store.getState().updateBlocker('api-call', {
|
|
167
|
-
* timeout: 60000 // Extend to 60 seconds
|
|
168
|
-
* });
|
|
169
|
-
* ```
|
|
170
|
-
*
|
|
171
|
-
* @see {@link addBlocker} to create a new blocker
|
|
172
|
-
*/
|
|
173
|
-
updateBlocker: (id, config = {}) => {
|
|
174
|
-
const existingBlocker = get().activeBlockers.get(id);
|
|
175
|
-
if (!existingBlocker) {
|
|
176
|
-
get().addBlocker(id, config);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
function applyUpdate(id, config, replace2) {
|
|
315
|
+
const previous = blockers.get(id);
|
|
316
|
+
if (!previous) {
|
|
317
|
+
add(id, config);
|
|
177
318
|
return;
|
|
178
319
|
}
|
|
179
|
-
|
|
180
|
-
|
|
320
|
+
const oldConfig = previous.config;
|
|
321
|
+
let nextConfig;
|
|
322
|
+
if (replace2) {
|
|
323
|
+
nextConfig = normalizeBlocker(id, {
|
|
324
|
+
...config,
|
|
325
|
+
timestamp: config.timestamp ?? oldConfig.timestamp
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
nextConfig = {
|
|
329
|
+
...oldConfig,
|
|
330
|
+
scope: copyScope(config.scope ?? oldConfig.scope),
|
|
331
|
+
reason: config.reason ?? oldConfig.reason,
|
|
332
|
+
priority: Math.max(0, config.priority ?? oldConfig.priority),
|
|
333
|
+
timestamp: config.timestamp ?? oldConfig.timestamp,
|
|
334
|
+
timeout: config.timeout ?? oldConfig.timeout,
|
|
335
|
+
onTimeout: config.onTimeout ?? oldConfig.onTimeout
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const timeoutChanged = nextConfig.timeout !== oldConfig.timeout;
|
|
181
339
|
if (timeoutChanged) {
|
|
182
|
-
|
|
183
|
-
clearTimeout(existingBlocker.timeoutId);
|
|
184
|
-
}
|
|
185
|
-
if (config.timeout && config.timeout > 0) {
|
|
186
|
-
const newTimeout = config.timeout;
|
|
187
|
-
const newOnTimeout = config.onTimeout ?? existingBlocker.onTimeout;
|
|
188
|
-
timeoutId = setTimeout(() => {
|
|
189
|
-
if (get().activeBlockers.has(id)) {
|
|
190
|
-
newOnTimeout?.(id);
|
|
191
|
-
void get().runMiddlewares(
|
|
192
|
-
createMiddlewareContext("timeout", id, {
|
|
193
|
-
scope: config.scope ?? existingBlocker.scope,
|
|
194
|
-
reason: config.reason ?? existingBlocker.reason,
|
|
195
|
-
priority: config.priority ?? existingBlocker.priority,
|
|
196
|
-
timeout: newTimeout
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
|
-
get().removeBlocker(id);
|
|
200
|
-
}
|
|
201
|
-
}, newTimeout);
|
|
202
|
-
} else {
|
|
203
|
-
timeoutId = void 0;
|
|
204
|
-
}
|
|
340
|
+
validateTimeout(nextConfig.timeout);
|
|
205
341
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
timeout: config.timeout ?? existingBlocker.timeout,
|
|
213
|
-
timeoutId,
|
|
214
|
-
onTimeout: config.onTimeout ?? existingBlocker.onTimeout
|
|
215
|
-
};
|
|
216
|
-
set((state) => {
|
|
217
|
-
const newBlockers = new Map(state.activeBlockers);
|
|
218
|
-
newBlockers.set(id, updatedBlocker);
|
|
219
|
-
return { activeBlockers: newBlockers };
|
|
220
|
-
});
|
|
221
|
-
void get().runMiddlewares(
|
|
222
|
-
createMiddlewareContext(
|
|
223
|
-
"update",
|
|
224
|
-
id,
|
|
225
|
-
{
|
|
226
|
-
scope: updatedBlocker.scope,
|
|
227
|
-
reason: updatedBlocker.reason,
|
|
228
|
-
priority: updatedBlocker.priority,
|
|
229
|
-
timeout: updatedBlocker.timeout,
|
|
230
|
-
onTimeout: updatedBlocker.onTimeout,
|
|
231
|
-
timestamp: updatedBlocker.timestamp
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
scope: existingBlocker.scope,
|
|
235
|
-
reason: existingBlocker.reason,
|
|
236
|
-
priority: existingBlocker.priority,
|
|
237
|
-
timeout: existingBlocker.timeout,
|
|
238
|
-
onTimeout: existingBlocker.onTimeout,
|
|
239
|
-
timestamp: existingBlocker.timestamp
|
|
240
|
-
}
|
|
241
|
-
)
|
|
242
|
-
);
|
|
243
|
-
},
|
|
244
|
-
/**
|
|
245
|
-
* Removes a blocker from the store.
|
|
246
|
-
*
|
|
247
|
-
* Clears any associated timeout and triggers middleware with 'remove' action.
|
|
248
|
-
* Safe to call even if the blocker doesn't exist (no-op in that case).
|
|
249
|
-
*
|
|
250
|
-
* @param id - Identifier of the blocker to remove
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* ```ts
|
|
254
|
-
* // Add and remove a blocker
|
|
255
|
-
* store.getState().addBlocker('temp', { scope: 'form' });
|
|
256
|
-
* // ... later
|
|
257
|
-
* store.getState().removeBlocker('temp');
|
|
258
|
-
* ```
|
|
259
|
-
*
|
|
260
|
-
* @see {@link addBlocker} to add a blocker
|
|
261
|
-
* @see {@link clearAllBlockers} to remove all blockers at once
|
|
262
|
-
*/
|
|
263
|
-
removeBlocker: (id) => {
|
|
264
|
-
const prevBlocker = get().activeBlockers.get(id);
|
|
265
|
-
if (prevBlocker?.timeoutId) {
|
|
266
|
-
clearTimeout(prevBlocker.timeoutId);
|
|
342
|
+
let blocker = previous;
|
|
343
|
+
if (timeoutChanged) {
|
|
344
|
+
cancelTimeout(previous);
|
|
345
|
+
blocker = { config: nextConfig, registration: previous.registration };
|
|
346
|
+
} else {
|
|
347
|
+
blocker.config = nextConfig;
|
|
267
348
|
}
|
|
268
|
-
set(
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return { activeBlockers: newBlockers };
|
|
272
|
-
});
|
|
273
|
-
if (prevBlocker) {
|
|
274
|
-
void get().runMiddlewares(createMiddlewareContext("remove", id, prevBlocker, prevBlocker));
|
|
349
|
+
blockers.set(id, blocker);
|
|
350
|
+
if (timeoutChanged) {
|
|
351
|
+
scheduleTimeout(id, blocker);
|
|
275
352
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*
|
|
294
|
-
* @example
|
|
295
|
-
* Multiple scopes check
|
|
296
|
-
* ```ts
|
|
297
|
-
* const isBlocked = store.getState().isBlocked(['form', 'navigation']);
|
|
298
|
-
* // Returns true if either 'form' OR 'navigation' has active blockers
|
|
299
|
-
* ```
|
|
300
|
-
*
|
|
301
|
-
* @see {@link getBlockingInfo} for detailed blocker information
|
|
302
|
-
*/
|
|
303
|
-
isBlocked: (scope = DEFAULT_SCOPE) => {
|
|
304
|
-
const { activeBlockers } = get();
|
|
305
|
-
const scopes = normalizeScopeToArray(scope);
|
|
306
|
-
for (const [, blocker] of activeBlockers) {
|
|
307
|
-
if (blocker.scope === DEFAULT_SCOPE) {
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
311
|
-
if (scopes.some((s) => blockerScopes.includes(s))) {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
353
|
+
publish({
|
|
354
|
+
...event("update", id),
|
|
355
|
+
registration: blocker.registration,
|
|
356
|
+
config: publicConfig(nextConfig),
|
|
357
|
+
prevState: publicConfig(oldConfig)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function update(id, config = {}) {
|
|
361
|
+
applyUpdate(id, config, false);
|
|
362
|
+
}
|
|
363
|
+
function replace(id, config) {
|
|
364
|
+
applyUpdate(id, config, true);
|
|
365
|
+
}
|
|
366
|
+
function remove(id) {
|
|
367
|
+
const previous = blockers.get(id);
|
|
368
|
+
if (previous) {
|
|
369
|
+
cancelTimeout(previous);
|
|
314
370
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
*
|
|
327
|
-
* @example
|
|
328
|
-
* Display blocker details
|
|
329
|
-
* ```ts
|
|
330
|
-
* const blockers = store.getState().getBlockingInfo('form');
|
|
331
|
-
* blockers.forEach(blocker => {
|
|
332
|
-
* console.log(`${blocker.id}: ${blocker.reason}`);
|
|
333
|
-
* console.log(`Priority: ${blocker.priority}`);
|
|
334
|
-
* });
|
|
335
|
-
* ```
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* Get highest priority blocker
|
|
339
|
-
* ```ts
|
|
340
|
-
* const blockers = store.getState().getBlockingInfo('navigation');
|
|
341
|
-
* const topBlocker = blockers[0]; // Highest priority
|
|
342
|
-
* if (topBlocker) {
|
|
343
|
-
* console.log(`Top blocker: ${topBlocker.reason}`);
|
|
344
|
-
* }
|
|
345
|
-
* ```
|
|
346
|
-
*
|
|
347
|
-
* @see {@link isBlocked} for a simple boolean check
|
|
348
|
-
* @see {@link BlockerInfo} for the structure of returned objects
|
|
349
|
-
*/
|
|
350
|
-
getBlockingInfo: (scope) => {
|
|
351
|
-
const { activeBlockers } = get();
|
|
352
|
-
const blockers = [];
|
|
353
|
-
for (const [id, blocker] of activeBlockers) {
|
|
354
|
-
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
355
|
-
if (blocker.scope === DEFAULT_SCOPE || blockerScopes.includes(scope)) {
|
|
356
|
-
const { timeoutId: _timeoutId, ...publicBlocker } = blocker;
|
|
357
|
-
blockers.push({ id, ...publicBlocker });
|
|
358
|
-
}
|
|
371
|
+
blockers.delete(id);
|
|
372
|
+
if (previous) {
|
|
373
|
+
const config = publicConfig(previous.config);
|
|
374
|
+
publish({
|
|
375
|
+
...event("remove", id),
|
|
376
|
+
registration: previous.registration,
|
|
377
|
+
config,
|
|
378
|
+
prevState: config
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
publish();
|
|
359
382
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
* Clears all timeouts and triggers middleware with 'clear' action.
|
|
366
|
-
* Useful for resetting the blocking state.
|
|
367
|
-
*
|
|
368
|
-
* @example
|
|
369
|
-
* ```ts
|
|
370
|
-
* // Clear all blockers on logout
|
|
371
|
-
* store.getState().clearAllBlockers();
|
|
372
|
-
* ```
|
|
373
|
-
*
|
|
374
|
-
* @see {@link clearBlockersForScope} to clear blockers for a specific scope
|
|
375
|
-
*/
|
|
376
|
-
clearAllBlockers: () => {
|
|
377
|
-
const { activeBlockers } = get();
|
|
378
|
-
const count = activeBlockers.size;
|
|
379
|
-
for (const [, blocker] of activeBlockers) {
|
|
380
|
-
if (blocker.timeoutId) {
|
|
381
|
-
clearTimeout(blocker.timeoutId);
|
|
382
|
-
}
|
|
383
|
+
}
|
|
384
|
+
function clear() {
|
|
385
|
+
const count = blockers.size;
|
|
386
|
+
for (const blocker of blockers.values()) {
|
|
387
|
+
cancelTimeout(blocker);
|
|
383
388
|
}
|
|
384
|
-
|
|
389
|
+
blockers.clear();
|
|
385
390
|
if (count > 0) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
timestamp: Date.now(),
|
|
390
|
-
count
|
|
391
|
-
});
|
|
391
|
+
publish({ ...event("clear", "*"), count });
|
|
392
|
+
} else {
|
|
393
|
+
publish();
|
|
392
394
|
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
* Removes all blockers that affect a specific scope.
|
|
396
|
-
*
|
|
397
|
-
* Clears timeouts for removed blockers and triggers middleware with
|
|
398
|
-
* 'clear_scope' action. Global scope blockers are NOT cleared by this method.
|
|
399
|
-
*
|
|
400
|
-
* @param scope - Scope to clear blockers for
|
|
401
|
-
*
|
|
402
|
-
* @example
|
|
403
|
-
* Clear form blockers
|
|
404
|
-
* ```ts
|
|
405
|
-
* // Clear all blockers affecting 'form' scope
|
|
406
|
-
* store.getState().clearBlockersForScope('form');
|
|
407
|
-
* ```
|
|
408
|
-
*
|
|
409
|
-
* @example
|
|
410
|
-
* Clear on navigation
|
|
411
|
-
* ```ts
|
|
412
|
-
* // Clear navigation blockers when user navigates away
|
|
413
|
-
* store.getState().clearBlockersForScope('navigation');
|
|
414
|
-
* ```
|
|
415
|
-
*
|
|
416
|
-
* @see {@link clearAllBlockers} to clear all blockers
|
|
417
|
-
*/
|
|
418
|
-
clearBlockersForScope: (scope) => {
|
|
419
|
-
const { activeBlockers } = get();
|
|
395
|
+
}
|
|
396
|
+
function clearScope(scope) {
|
|
420
397
|
let count = 0;
|
|
421
|
-
for (const [, blocker] of
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
clearTimeout(blocker.timeoutId);
|
|
426
|
-
}
|
|
398
|
+
for (const [id, blocker] of blockers) {
|
|
399
|
+
if (scopeMatchesTarget(blocker.config.scope, scope)) {
|
|
400
|
+
cancelTimeout(blocker);
|
|
401
|
+
blockers.delete(id);
|
|
427
402
|
count++;
|
|
428
403
|
}
|
|
429
404
|
}
|
|
430
|
-
set((state) => {
|
|
431
|
-
const newBlockers = /* @__PURE__ */ new Map();
|
|
432
|
-
for (const [id, blocker] of state.activeBlockers) {
|
|
433
|
-
const blockerScopes = normalizeScopeToArray(blocker.scope);
|
|
434
|
-
if (!blockerScopes.includes(scope)) {
|
|
435
|
-
newBlockers.set(id, blocker);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
return { activeBlockers: newBlockers };
|
|
439
|
-
});
|
|
440
405
|
if (count > 0) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
timestamp: Date.now(),
|
|
445
|
-
scope,
|
|
446
|
-
count
|
|
447
|
-
});
|
|
406
|
+
publish({ ...event("clear_scope", "*"), scope, count });
|
|
407
|
+
} else {
|
|
408
|
+
publish();
|
|
448
409
|
}
|
|
449
410
|
}
|
|
450
|
-
|
|
411
|
+
function isBlocked(scope = DEFAULT_SCOPE) {
|
|
412
|
+
for (const { config } of blockers.values()) {
|
|
413
|
+
if (scopeAffectsObservation(config.scope, scope)) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
function getBlockingInfo(scope) {
|
|
420
|
+
return projectBlockingInfo(getSnapshot(), scope);
|
|
421
|
+
}
|
|
422
|
+
function subscribe(listener) {
|
|
423
|
+
const registration = /* @__PURE__ */ Symbol();
|
|
424
|
+
subscribers.set(registration, listener);
|
|
425
|
+
return () => {
|
|
426
|
+
subscribers.delete(registration);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function observe(observer) {
|
|
430
|
+
const registration = /* @__PURE__ */ Symbol();
|
|
431
|
+
observers.set(registration, observer);
|
|
432
|
+
return () => {
|
|
433
|
+
observers.delete(registration);
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
add,
|
|
438
|
+
update,
|
|
439
|
+
replace,
|
|
440
|
+
remove,
|
|
441
|
+
clear,
|
|
442
|
+
clearScope,
|
|
443
|
+
isBlocked,
|
|
444
|
+
getBlockingInfo,
|
|
445
|
+
getSnapshot,
|
|
446
|
+
subscribe,
|
|
447
|
+
observe
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/store/uiBlockingStore.actions.ts
|
|
452
|
+
function createActions(lifecycle) {
|
|
453
|
+
return () => ({
|
|
454
|
+
blockingSnapshot: lifecycle.getSnapshot(),
|
|
455
|
+
observeBlockingEvents: lifecycle.observe,
|
|
456
|
+
addBlocker: lifecycle.add,
|
|
457
|
+
updateBlocker: lifecycle.update,
|
|
458
|
+
replaceBlocker: lifecycle.replace,
|
|
459
|
+
removeBlocker: lifecycle.remove,
|
|
460
|
+
clearAllBlockers: lifecycle.clear,
|
|
461
|
+
clearBlockersForScope: lifecycle.clearScope,
|
|
462
|
+
isBlocked: lifecycle.isBlocked,
|
|
463
|
+
getBlockingInfo: lifecycle.getBlockingInfo
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
function createLifecycleStateCreator(enhance) {
|
|
467
|
+
return (set, get, api) => {
|
|
468
|
+
const lifecycle = createBlockingLifecycle();
|
|
469
|
+
function setWithLifecycle(...args) {
|
|
470
|
+
const current = get();
|
|
471
|
+
if (args[1] === true) {
|
|
472
|
+
const [update2] = args;
|
|
473
|
+
const next2 = typeUtils.isFunction(update2) ? update2(current) : update2;
|
|
474
|
+
if (!Object.is(next2, current)) {
|
|
475
|
+
set({ ...next2, blockingSnapshot: lifecycle.getSnapshot() }, true);
|
|
476
|
+
}
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const [update] = args;
|
|
480
|
+
const next = typeUtils.isFunction(update) ? update(current) : update;
|
|
481
|
+
if (!Object.is(next, current)) {
|
|
482
|
+
set({ ...next, blockingSnapshot: lifecycle.getSnapshot() });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
api.setState = setWithLifecycle;
|
|
486
|
+
lifecycle.subscribe((blockingSnapshot) => {
|
|
487
|
+
api.setState({ blockingSnapshot });
|
|
488
|
+
});
|
|
489
|
+
return enhance(lifecycle)(setWithLifecycle, get, api);
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
var createUIBlockingActions = createLifecycleStateCreator(createActions);
|
|
493
|
+
function createUIBlockingActionsWithDevtools(options) {
|
|
494
|
+
return createLifecycleStateCreator((lifecycle) => middleware.devtools(createActions(lifecycle), options));
|
|
495
|
+
}
|
|
451
496
|
var {
|
|
452
497
|
Provider: BaseUIBlockingProvider,
|
|
453
498
|
useContextStoreApi: useUIBlockingContext,
|
|
454
499
|
useContextStore: baseUseUIBlockingStoreFromContext,
|
|
455
500
|
useIsInsideProvider: useIsInsideUIBlockingProvider,
|
|
456
|
-
|
|
457
|
-
} = reactZustandToolkit.createStoreProvider(createUIBlockingActions, "UIBlocking");
|
|
501
|
+
useProviderStoreOptional: useOptionalUIBlockingContext
|
|
502
|
+
} = reactZustandToolkit.createStoreProvider(() => createUIBlockingActions, "UIBlocking");
|
|
458
503
|
function useUIBlockingStoreFromContext(selector, equalityFn) {
|
|
459
504
|
if (selector) {
|
|
460
505
|
return baseUseUIBlockingStoreFromContext(selector, equalityFn);
|
|
@@ -466,11 +511,12 @@ function UIBlockingProvider({
|
|
|
466
511
|
middlewares = []
|
|
467
512
|
}) {
|
|
468
513
|
const handleStoreInit = (store) => {
|
|
469
|
-
|
|
470
|
-
|
|
514
|
+
const { observeBlockingEvents } = store.getState();
|
|
515
|
+
middlewares.forEach((middleware) => {
|
|
516
|
+
observeBlockingEvents(middleware);
|
|
471
517
|
});
|
|
472
518
|
};
|
|
473
|
-
return /* @__PURE__ */ jsxRuntime.jsx(BaseUIBlockingProvider, { onStoreInit: handleStoreInit, children });
|
|
519
|
+
return /* @__PURE__ */ jsxRuntime.jsx(BaseUIBlockingProvider, { input: void 0, onStoreInit: handleStoreInit, children });
|
|
474
520
|
}
|
|
475
521
|
|
|
476
522
|
// src/store/uiBlockingStore.config.ts
|
|
@@ -481,9 +527,9 @@ var devtoolsConfig = {
|
|
|
481
527
|
|
|
482
528
|
// src/store/uiBlockingStore.store.ts
|
|
483
529
|
var storeBindings = reactZustandToolkit.createShallowStore(
|
|
484
|
-
|
|
530
|
+
createUIBlockingActionsWithDevtools(devtoolsConfig)
|
|
485
531
|
);
|
|
486
|
-
var uiBlockingStoreApi = storeBindings.
|
|
532
|
+
var uiBlockingStoreApi = storeBindings.store;
|
|
487
533
|
function useUIBlockingStore(selector, equalityFn) {
|
|
488
534
|
if (selector) {
|
|
489
535
|
return storeBindings.useStore(selector, equalityFn);
|
|
@@ -492,27 +538,55 @@ function useUIBlockingStore(selector, equalityFn) {
|
|
|
492
538
|
}
|
|
493
539
|
|
|
494
540
|
// src/context/useResolvedStore.ts
|
|
495
|
-
|
|
541
|
+
var { useResolvedStoreApi: useToolkitStoreApi, useResolvedValue: useToolkitValue } = reactZustandToolkit.createResolvedStoreHooks(uiBlockingStoreApi, useOptionalUIBlockingContext);
|
|
542
|
+
function useResolvedStoreApi() {
|
|
543
|
+
return useToolkitStoreApi();
|
|
544
|
+
}
|
|
545
|
+
function useResolvedValue(selector, equalityFn) {
|
|
546
|
+
return useToolkitValue(selector, equalityFn);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// src/context/index.ts
|
|
550
|
+
function useOptionalContext() {
|
|
496
551
|
return useOptionalUIBlockingContext();
|
|
497
552
|
}
|
|
498
|
-
function
|
|
499
|
-
|
|
500
|
-
|
|
553
|
+
function useResolvedStore() {
|
|
554
|
+
return useResolvedStoreApi();
|
|
555
|
+
}
|
|
556
|
+
function useResolvedStoreWithSelector(selector) {
|
|
557
|
+
return useResolvedValue(selector);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/store/blockerId.utils.ts
|
|
561
|
+
var storeSequences = /* @__PURE__ */ new WeakMap();
|
|
562
|
+
function allocateBlockerId(store, prefix) {
|
|
563
|
+
let sequence = storeSequences.get(store) ?? 0;
|
|
564
|
+
let blockerId;
|
|
565
|
+
const { blockingSnapshot } = store.getState();
|
|
566
|
+
do {
|
|
567
|
+
sequence += 1;
|
|
568
|
+
blockerId = `${prefix}-${String(sequence)}`;
|
|
569
|
+
} while (blockingSnapshot.some(({ id }) => id === blockerId));
|
|
570
|
+
storeSequences.set(store, sequence);
|
|
571
|
+
return blockerId;
|
|
501
572
|
}
|
|
502
|
-
|
|
573
|
+
|
|
574
|
+
// src/hooks/useBlockerIdAllocator/useBlockerIdAllocator.ts
|
|
575
|
+
function useBlockerIdAllocator() {
|
|
503
576
|
const store = useResolvedStoreApi();
|
|
504
|
-
return
|
|
577
|
+
return react.useCallback((prefix) => allocateBlockerId(store, prefix), [store]);
|
|
505
578
|
}
|
|
579
|
+
|
|
580
|
+
// src/hooks/useAsyncAction/useAsyncAction.ts
|
|
506
581
|
function useAsyncAction(actionId, scope, options) {
|
|
582
|
+
const allocateBlockerId2 = useBlockerIdAllocator();
|
|
507
583
|
const { addBlocker, removeBlocker } = useResolvedValue((state) => ({
|
|
508
584
|
addBlocker: state.addBlocker,
|
|
509
585
|
removeBlocker: state.removeBlocker
|
|
510
586
|
}));
|
|
511
|
-
const counterRef = react.useRef(0);
|
|
512
587
|
const executeWithBlocking = react.useCallback(
|
|
513
588
|
async (asyncFn) => {
|
|
514
|
-
|
|
515
|
-
const blockerId = `${actionId}-${String(counterRef.current)}`;
|
|
589
|
+
const blockerId = allocateBlockerId2(actionId);
|
|
516
590
|
try {
|
|
517
591
|
addBlocker(blockerId, {
|
|
518
592
|
scope,
|
|
@@ -526,28 +600,53 @@ function useAsyncAction(actionId, scope, options) {
|
|
|
526
600
|
removeBlocker(blockerId);
|
|
527
601
|
}
|
|
528
602
|
},
|
|
529
|
-
[
|
|
603
|
+
[
|
|
604
|
+
actionId,
|
|
605
|
+
scope,
|
|
606
|
+
options?.timeout,
|
|
607
|
+
options?.onTimeout,
|
|
608
|
+
allocateBlockerId2,
|
|
609
|
+
addBlocker,
|
|
610
|
+
removeBlocker
|
|
611
|
+
]
|
|
530
612
|
);
|
|
531
613
|
return executeWithBlocking;
|
|
532
614
|
}
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
615
|
+
function useConfigRef(config) {
|
|
616
|
+
const configRef = react.useRef(config);
|
|
617
|
+
react.useEffect(() => {
|
|
618
|
+
configRef.current = config;
|
|
619
|
+
}, [config]);
|
|
620
|
+
return configRef;
|
|
539
621
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
622
|
+
var activeHookRegistrations = /* @__PURE__ */ new WeakMap();
|
|
623
|
+
function trackHookRegistration(store, blockerId) {
|
|
624
|
+
if (process.env.NODE_ENV === "production") {
|
|
625
|
+
return () => void 0;
|
|
543
626
|
}
|
|
544
|
-
|
|
545
|
-
|
|
627
|
+
const registrations = activeHookRegistrations.get(store) ?? /* @__PURE__ */ new Map();
|
|
628
|
+
const count = registrations.get(blockerId) ?? 0;
|
|
629
|
+
activeHookRegistrations.set(store, registrations);
|
|
630
|
+
registrations.set(blockerId, count + 1);
|
|
631
|
+
if (count > 0) {
|
|
632
|
+
console.warn(
|
|
633
|
+
`[react-action-guard] Multiple active useActionBlocker hooks registered ID "${blockerId}" in the same store. Use a unique ID per hook; shared IDs can overwrite configuration or remove another hook's blocker during cleanup.`
|
|
634
|
+
);
|
|
546
635
|
}
|
|
547
|
-
return
|
|
636
|
+
return () => {
|
|
637
|
+
const remaining = (registrations.get(blockerId) ?? 1) - 1;
|
|
638
|
+
if (remaining > 0) {
|
|
639
|
+
registrations.set(blockerId, remaining);
|
|
640
|
+
} else {
|
|
641
|
+
registrations.delete(blockerId);
|
|
642
|
+
}
|
|
643
|
+
if (registrations.size === 0) {
|
|
644
|
+
activeHookRegistrations.delete(store);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
548
647
|
}
|
|
549
648
|
function getScopeSnapshotKey(scope) {
|
|
550
|
-
return JSON.stringify(
|
|
649
|
+
return JSON.stringify(normalizeScope(scope));
|
|
551
650
|
}
|
|
552
651
|
function toBlockerConfigSnapshot(config) {
|
|
553
652
|
return [
|
|
@@ -563,60 +662,96 @@ function areBlockerConfigsEqual(first, second) {
|
|
|
563
662
|
return shallow.shallow(toBlockerConfigSnapshot(first), toBlockerConfigSnapshot(second));
|
|
564
663
|
}
|
|
565
664
|
|
|
566
|
-
// src/hooks/
|
|
567
|
-
function
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
665
|
+
// src/hooks/useActionBlocker/useBlockerRegistration.ts
|
|
666
|
+
function useBlockerRegistration({
|
|
667
|
+
blockerId,
|
|
668
|
+
config,
|
|
669
|
+
endEpisodeOnTimeout = false
|
|
670
|
+
}) {
|
|
671
|
+
const store = useResolvedStoreApi();
|
|
573
672
|
const lastConfigRef = react.useRef(null);
|
|
574
|
-
react.
|
|
575
|
-
|
|
673
|
+
const releaseRef = react.useRef(null);
|
|
674
|
+
const expiredRef = react.useRef(false);
|
|
675
|
+
const configRef = useConfigRef(config);
|
|
676
|
+
const handleTimeout = react.useCallback(
|
|
677
|
+
(id) => {
|
|
678
|
+
expiredRef.current = endEpisodeOnTimeout;
|
|
679
|
+
configRef.current.onTimeout?.(id);
|
|
680
|
+
},
|
|
681
|
+
[configRef, endEpisodeOnTimeout]
|
|
682
|
+
);
|
|
683
|
+
const storeConfig = react.useMemo(() => {
|
|
684
|
+
const shouldTrackTimeout = endEpisodeOnTimeout && typeUtils.isDefined(config.timeout);
|
|
685
|
+
const hasTimeoutCallback = typeUtils.isDefined(config.onTimeout);
|
|
686
|
+
let onTimeout;
|
|
687
|
+
if (shouldTrackTimeout || hasTimeoutCallback) {
|
|
688
|
+
onTimeout = handleTimeout;
|
|
689
|
+
}
|
|
690
|
+
return { ...config, onTimeout };
|
|
691
|
+
}, [config, handleTimeout, endEpisodeOnTimeout]);
|
|
692
|
+
const storeConfigRef = useConfigRef(storeConfig);
|
|
693
|
+
const activate = react.useCallback(() => {
|
|
694
|
+
if (!blockerId || !typeUtils.isNull(releaseRef.current)) {
|
|
576
695
|
return;
|
|
577
696
|
}
|
|
578
|
-
addBlocker(
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
697
|
+
const { addBlocker } = store.getState();
|
|
698
|
+
expiredRef.current = false;
|
|
699
|
+
releaseRef.current = trackHookRegistration(store, blockerId);
|
|
700
|
+
lastConfigRef.current = storeConfigRef.current;
|
|
701
|
+
addBlocker(blockerId, storeConfigRef.current);
|
|
702
|
+
}, [blockerId, store, storeConfigRef]);
|
|
703
|
+
const deactivate = react.useCallback(() => {
|
|
704
|
+
if (typeUtils.isNull(releaseRef.current)) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const { removeBlocker } = store.getState();
|
|
708
|
+
releaseRef.current();
|
|
709
|
+
releaseRef.current = null;
|
|
710
|
+
lastConfigRef.current = null;
|
|
711
|
+
expiredRef.current = false;
|
|
712
|
+
removeBlocker(blockerId);
|
|
713
|
+
}, [blockerId, store]);
|
|
714
|
+
react.useEffect(() => deactivate, [deactivate]);
|
|
585
715
|
react.useEffect(() => {
|
|
586
|
-
if (
|
|
716
|
+
if (typeUtils.isNull(lastConfigRef.current) || expiredRef.current || areBlockerConfigsEqual(lastConfigRef.current, storeConfig)) {
|
|
587
717
|
return;
|
|
588
718
|
}
|
|
589
|
-
|
|
719
|
+
const { replaceBlocker } = store.getState();
|
|
720
|
+
replaceBlocker(blockerId, storeConfig);
|
|
721
|
+
lastConfigRef.current = storeConfig;
|
|
722
|
+
}, [blockerId, store, storeConfig]);
|
|
723
|
+
return { activate, deactivate };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// src/hooks/useActionBlocker/useActionBlocker.ts
|
|
727
|
+
function useActionBlocker(blockerId, config, isActive = true) {
|
|
728
|
+
const { activate, deactivate } = useBlockerRegistration({ blockerId, config });
|
|
729
|
+
react.useEffect(() => {
|
|
730
|
+
if (!isActive) {
|
|
590
731
|
return;
|
|
591
732
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
}, [
|
|
733
|
+
activate();
|
|
734
|
+
return deactivate;
|
|
735
|
+
}, [isActive, activate, deactivate]);
|
|
595
736
|
}
|
|
737
|
+
var useBlocker = useActionBlocker;
|
|
596
738
|
function useBlockingInfo(scope = DEFAULT_SCOPE) {
|
|
597
|
-
const
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
603
|
-
function useConfigRef(config) {
|
|
604
|
-
const configRef = react.useRef(config);
|
|
605
|
-
react.useEffect(() => {
|
|
606
|
-
configRef.current = config;
|
|
607
|
-
}, [config]);
|
|
608
|
-
return configRef;
|
|
739
|
+
const checkedScopes = normalizeScope(scope);
|
|
740
|
+
const selector = react.useMemo(() => {
|
|
741
|
+
return createBlockingInfoSelector(checkedScopes);
|
|
742
|
+
}, [checkedScopes]);
|
|
743
|
+
return useResolvedValue(selector, areBlockingInfosEqual);
|
|
609
744
|
}
|
|
610
|
-
|
|
611
|
-
// src/hooks/useConditionalBlocker/useConditionalBlocker.ts
|
|
745
|
+
var DEFAULT_CHECK_INTERVAL = 1e3;
|
|
612
746
|
function useConditionalBlocker(blockerId, config) {
|
|
613
|
-
const {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
747
|
+
const { activate, deactivate } = useBlockerRegistration({
|
|
748
|
+
blockerId,
|
|
749
|
+
config,
|
|
750
|
+
endEpisodeOnTimeout: true
|
|
751
|
+
});
|
|
618
752
|
const isBlockedRef = react.useRef(false);
|
|
619
753
|
const configRef = useConfigRef(config);
|
|
754
|
+
const checkInterval = !typeUtils.isUndefined(config.checkInterval) && config.checkInterval > 0 ? config.checkInterval : DEFAULT_CHECK_INTERVAL;
|
|
620
755
|
const checkCondition = react.useCallback(() => {
|
|
621
756
|
const currentConfig = configRef.current;
|
|
622
757
|
const shouldBlock = currentConfig.condition(currentConfig.state);
|
|
@@ -624,32 +759,32 @@ function useConditionalBlocker(blockerId, config) {
|
|
|
624
759
|
return;
|
|
625
760
|
}
|
|
626
761
|
if (shouldBlock) {
|
|
627
|
-
|
|
762
|
+
activate();
|
|
628
763
|
} else {
|
|
629
|
-
|
|
764
|
+
deactivate();
|
|
630
765
|
}
|
|
631
766
|
isBlockedRef.current = shouldBlock;
|
|
632
|
-
}, [
|
|
767
|
+
}, [activate, deactivate, configRef]);
|
|
768
|
+
react.useEffect(
|
|
769
|
+
() => () => {
|
|
770
|
+
deactivate();
|
|
771
|
+
isBlockedRef.current = false;
|
|
772
|
+
},
|
|
773
|
+
[deactivate]
|
|
774
|
+
);
|
|
633
775
|
react.useEffect(() => {
|
|
634
776
|
checkCondition();
|
|
635
|
-
const interval =
|
|
636
|
-
intervalRef.current = setInterval(checkCondition, interval);
|
|
777
|
+
const interval = setInterval(checkCondition, checkInterval);
|
|
637
778
|
return () => {
|
|
638
|
-
|
|
639
|
-
clearInterval(intervalRef.current);
|
|
640
|
-
intervalRef.current = null;
|
|
641
|
-
}
|
|
642
|
-
if (isBlockedRef.current) {
|
|
643
|
-
removeBlocker(blockerId);
|
|
644
|
-
isBlockedRef.current = false;
|
|
645
|
-
}
|
|
779
|
+
clearInterval(interval);
|
|
646
780
|
};
|
|
647
|
-
}, [checkCondition,
|
|
781
|
+
}, [checkCondition, checkInterval]);
|
|
648
782
|
}
|
|
649
783
|
function useConfirmableBlocker(blockerId, config) {
|
|
650
784
|
const [isDialogOpen, setIsDialogOpen] = react.useState(false);
|
|
651
785
|
const [isExecuting, setIsExecuting] = react.useState(false);
|
|
652
|
-
|
|
786
|
+
const executionRef = react.useRef(null);
|
|
787
|
+
useActionBlocker(
|
|
653
788
|
blockerId,
|
|
654
789
|
{
|
|
655
790
|
...config,
|
|
@@ -658,31 +793,48 @@ function useConfirmableBlocker(blockerId, config) {
|
|
|
658
793
|
isDialogOpen || isExecuting
|
|
659
794
|
);
|
|
660
795
|
const execute = react.useCallback(() => {
|
|
796
|
+
if (executionRef.current) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
661
799
|
setIsDialogOpen(true);
|
|
662
800
|
}, []);
|
|
663
801
|
const onConfirm = react.useCallback(async () => {
|
|
802
|
+
if (executionRef.current) {
|
|
803
|
+
return executionRef.current;
|
|
804
|
+
}
|
|
664
805
|
setIsDialogOpen(false);
|
|
665
806
|
setIsExecuting(true);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
807
|
+
async function runConfirmation() {
|
|
808
|
+
try {
|
|
809
|
+
await config.onConfirm();
|
|
810
|
+
} finally {
|
|
811
|
+
executionRef.current = null;
|
|
812
|
+
setIsExecuting(false);
|
|
813
|
+
}
|
|
670
814
|
}
|
|
815
|
+
const execution = Promise.resolve().then(runConfirmation);
|
|
816
|
+
executionRef.current = execution;
|
|
817
|
+
return execution;
|
|
671
818
|
}, [config]);
|
|
672
819
|
const onCancel = react.useCallback(() => {
|
|
820
|
+
if (executionRef.current) {
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
673
823
|
setIsDialogOpen(false);
|
|
674
824
|
config.onCancel?.();
|
|
675
825
|
}, [config]);
|
|
826
|
+
const { confirmTitle, confirmMessage, confirmButtonText, cancelButtonText } = config;
|
|
827
|
+
const confirmConfig = {
|
|
828
|
+
title: confirmTitle ?? "Confirm Action",
|
|
829
|
+
message: confirmMessage,
|
|
830
|
+
confirmText: confirmButtonText ?? "Confirm",
|
|
831
|
+
cancelText: cancelButtonText ?? "Cancel"
|
|
832
|
+
};
|
|
676
833
|
return {
|
|
677
834
|
execute,
|
|
678
835
|
isDialogOpen,
|
|
679
836
|
isExecuting,
|
|
680
|
-
confirmConfig
|
|
681
|
-
title: config.confirmTitle ?? "Confirm Action",
|
|
682
|
-
message: config.confirmMessage,
|
|
683
|
-
confirmText: config.confirmButtonText ?? "Confirm",
|
|
684
|
-
cancelText: config.cancelButtonText ?? "Cancel"
|
|
685
|
-
},
|
|
837
|
+
confirmConfig,
|
|
686
838
|
onConfirm,
|
|
687
839
|
onCancel
|
|
688
840
|
};
|
|
@@ -692,52 +844,64 @@ function useConfirmableBlocker(blockerId, config) {
|
|
|
692
844
|
function useIsBlocked(scope) {
|
|
693
845
|
return useResolvedValue((state) => state.isBlocked(scope));
|
|
694
846
|
}
|
|
695
|
-
|
|
696
|
-
// src/hooks/useScheduledBlocker/useScheduledBlocker.utils.ts
|
|
697
847
|
var MAX_TIMEOUT_MS = 2147483647;
|
|
698
848
|
function parseDate(date) {
|
|
699
849
|
return new Date(date).getTime();
|
|
700
850
|
}
|
|
701
851
|
function isValidTimestamp(timestamp) {
|
|
702
|
-
return
|
|
852
|
+
return Number.isFinite(timestamp) && Math.abs(timestamp) <= 864e13;
|
|
703
853
|
}
|
|
704
854
|
function calculateEndTime(schedule, startTime) {
|
|
705
|
-
if (schedule.duration) {
|
|
855
|
+
if (!typeUtils.isUndefined(schedule.duration)) {
|
|
706
856
|
return startTime + schedule.duration;
|
|
707
857
|
}
|
|
708
|
-
if (schedule.end) {
|
|
858
|
+
if (!typeUtils.isUndefined(schedule.end)) {
|
|
709
859
|
const endTime = parseDate(schedule.end);
|
|
710
860
|
return isValidTimestamp(endTime) ? endTime : void 0;
|
|
711
861
|
}
|
|
712
862
|
return void 0;
|
|
713
863
|
}
|
|
714
|
-
function isSafeTimeout(delay) {
|
|
715
|
-
return delay > 0 && delay <= MAX_TIMEOUT_MS;
|
|
716
|
-
}
|
|
717
864
|
function isInBlockingPeriod(startTime, endTime, now) {
|
|
718
865
|
if (startTime > now) {
|
|
719
866
|
return false;
|
|
720
867
|
}
|
|
721
|
-
if (
|
|
868
|
+
if (typeUtils.isUndefined(endTime)) {
|
|
722
869
|
return true;
|
|
723
870
|
}
|
|
724
871
|
return endTime > now;
|
|
725
872
|
}
|
|
726
|
-
function
|
|
727
|
-
|
|
873
|
+
function isValidSchedule(schedule, startTime) {
|
|
874
|
+
const { duration, end } = schedule;
|
|
875
|
+
if (!isValidTimestamp(startTime)) {
|
|
728
876
|
return false;
|
|
729
877
|
}
|
|
730
|
-
|
|
878
|
+
if (!typeUtils.isUndefined(end)) {
|
|
879
|
+
const suppliedEnd = parseDate(end);
|
|
880
|
+
if (!isValidTimestamp(suppliedEnd) || suppliedEnd < startTime) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (!typeUtils.isUndefined(duration)) {
|
|
885
|
+
if (!Number.isFinite(duration) || duration < 0) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
return isValidTimestamp(startTime + duration);
|
|
889
|
+
}
|
|
890
|
+
return true;
|
|
731
891
|
}
|
|
732
892
|
|
|
733
893
|
// src/hooks/useScheduledBlocker/useScheduledBlocker.ts
|
|
734
894
|
function useScheduledBlocker(blockerId, config) {
|
|
735
|
-
const {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
895
|
+
const { activate, deactivate } = useBlockerRegistration({
|
|
896
|
+
blockerId,
|
|
897
|
+
config,
|
|
898
|
+
endEpisodeOnTimeout: true
|
|
899
|
+
});
|
|
739
900
|
const timeoutsRef = react.useRef({});
|
|
740
901
|
const configRef = useConfigRef(config);
|
|
902
|
+
const scheduleStart = parseDate(config.schedule.start);
|
|
903
|
+
const scheduleEnd = typeUtils.isUndefined(config.schedule.end) ? void 0 : parseDate(config.schedule.end);
|
|
904
|
+
const scheduleDuration = config.schedule.duration;
|
|
741
905
|
const cleanup = react.useCallback(() => {
|
|
742
906
|
if (timeoutsRef.current.start) {
|
|
743
907
|
clearTimeout(timeoutsRef.current.start);
|
|
@@ -752,50 +916,69 @@ function useScheduledBlocker(blockerId, config) {
|
|
|
752
916
|
const currentConfig = configRef.current;
|
|
753
917
|
const startTime = parseDate(currentConfig.schedule.start);
|
|
754
918
|
const now = Date.now();
|
|
755
|
-
if (!
|
|
919
|
+
if (!isValidSchedule(currentConfig.schedule, startTime)) {
|
|
920
|
+
if (process.env.NODE_ENV !== "production") {
|
|
921
|
+
console.warn("[react-action-guard] Invalid blocking schedule:", currentConfig.schedule);
|
|
922
|
+
}
|
|
756
923
|
return;
|
|
757
924
|
}
|
|
758
|
-
const
|
|
759
|
-
|
|
760
|
-
|
|
925
|
+
const endTime = calculateEndTime(currentConfig.schedule, startTime);
|
|
926
|
+
if (!typeUtils.isUndefined(endTime) && endTime <= startTime) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const scheduleEnd2 = () => {
|
|
930
|
+
if (typeUtils.isUndefined(endTime)) {
|
|
761
931
|
return;
|
|
762
932
|
}
|
|
763
933
|
const remainingTime = endTime - Date.now();
|
|
764
934
|
if (remainingTime > 0) {
|
|
765
|
-
timeoutsRef.current.end = setTimeout(()
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
935
|
+
timeoutsRef.current.end = setTimeout(scheduleEnd2, Math.min(remainingTime, MAX_TIMEOUT_MS));
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
configRef.current.onScheduleEnd?.();
|
|
940
|
+
} finally {
|
|
941
|
+
deactivate();
|
|
769
942
|
}
|
|
770
943
|
};
|
|
771
944
|
const startBlocking = () => {
|
|
945
|
+
const now2 = Date.now();
|
|
946
|
+
const remainingTime = startTime - now2;
|
|
947
|
+
if (remainingTime > 0) {
|
|
948
|
+
timeoutsRef.current.start = setTimeout(
|
|
949
|
+
startBlocking,
|
|
950
|
+
Math.min(remainingTime, MAX_TIMEOUT_MS)
|
|
951
|
+
);
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (!isInBlockingPeriod(startTime, endTime, now2)) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
772
957
|
const cfg = configRef.current;
|
|
773
|
-
|
|
958
|
+
activate();
|
|
959
|
+
scheduleEnd2();
|
|
774
960
|
cfg.onScheduleStart?.();
|
|
775
|
-
scheduleEnd();
|
|
776
961
|
};
|
|
777
962
|
if (startTime > now) {
|
|
778
963
|
const delay = startTime - now;
|
|
779
|
-
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
timeoutsRef.current.start = setTimeout(startBlocking, delay);
|
|
964
|
+
timeoutsRef.current.start = setTimeout(startBlocking, Math.min(delay, MAX_TIMEOUT_MS));
|
|
783
965
|
} else {
|
|
784
|
-
const endTime = calculateEndTime(currentConfig.schedule, startTime);
|
|
785
966
|
if (isInBlockingPeriod(startTime, endTime, now)) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
967
|
+
try {
|
|
968
|
+
startBlocking();
|
|
969
|
+
} catch (error) {
|
|
970
|
+
cleanup();
|
|
971
|
+
deactivate();
|
|
972
|
+
throw error;
|
|
973
|
+
}
|
|
789
974
|
}
|
|
790
975
|
}
|
|
791
976
|
return () => {
|
|
792
977
|
cleanup();
|
|
793
|
-
|
|
978
|
+
deactivate();
|
|
794
979
|
};
|
|
795
|
-
}, [
|
|
980
|
+
}, [activate, deactivate, cleanup, scheduleStart, scheduleEnd, scheduleDuration, configRef]);
|
|
796
981
|
}
|
|
797
|
-
|
|
798
|
-
// src/middleware/analyticsMiddleware.utils.ts
|
|
799
982
|
function capitalizeFirst(str) {
|
|
800
983
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
801
984
|
}
|
|
@@ -810,13 +993,13 @@ function buildEventData(context) {
|
|
|
810
993
|
blocker_id: context.blockerId
|
|
811
994
|
};
|
|
812
995
|
const { scope, reason, priority } = context.config ?? {};
|
|
813
|
-
if (scope
|
|
814
|
-
eventData.scope =
|
|
996
|
+
if (!typeUtils.isUndefined(scope)) {
|
|
997
|
+
eventData.scope = typeUtils.isArray(scope) ? scope.join(",") : scope;
|
|
815
998
|
}
|
|
816
|
-
if (reason
|
|
999
|
+
if (!typeUtils.isUndefined(reason)) {
|
|
817
1000
|
eventData.reason = reason;
|
|
818
1001
|
}
|
|
819
|
-
if (priority
|
|
1002
|
+
if (!typeUtils.isUndefined(priority)) {
|
|
820
1003
|
eventData.priority = priority;
|
|
821
1004
|
}
|
|
822
1005
|
return eventData;
|
|
@@ -875,21 +1058,14 @@ function createAnalyticsMiddleware(config) {
|
|
|
875
1058
|
}
|
|
876
1059
|
|
|
877
1060
|
// src/middleware/configureMiddleware.ts
|
|
878
|
-
var
|
|
1061
|
+
var configuredReleases = [];
|
|
879
1062
|
function configureMiddleware(middlewares) {
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
});
|
|
886
|
-
middlewares.forEach((middleware, index) => {
|
|
887
|
-
const name = `${CONFIGURE_MIDDLEWARE_PREFIX}${index.toString()}`;
|
|
888
|
-
store.registerMiddleware(name, middleware);
|
|
889
|
-
});
|
|
1063
|
+
const { observeBlockingEvents } = uiBlockingStoreApi.getState();
|
|
1064
|
+
for (const release of configuredReleases) {
|
|
1065
|
+
release();
|
|
1066
|
+
}
|
|
1067
|
+
configuredReleases = middlewares.map(observeBlockingEvents);
|
|
890
1068
|
}
|
|
891
|
-
|
|
892
|
-
// src/middleware/loggerMiddleware.utils.ts
|
|
893
1069
|
function getActionEmoji(action) {
|
|
894
1070
|
switch (action) {
|
|
895
1071
|
case "add":
|
|
@@ -917,7 +1093,7 @@ function extractConfigDetails(config) {
|
|
|
917
1093
|
scope,
|
|
918
1094
|
reason
|
|
919
1095
|
};
|
|
920
|
-
if (priority
|
|
1096
|
+
if (!typeUtils.isUndefined(priority)) {
|
|
921
1097
|
details.priority = priority;
|
|
922
1098
|
}
|
|
923
1099
|
return details;
|
|
@@ -925,19 +1101,19 @@ function extractConfigDetails(config) {
|
|
|
925
1101
|
function formatLogData(context) {
|
|
926
1102
|
const configDetails = extractConfigDetails(context.config);
|
|
927
1103
|
const logData = {};
|
|
928
|
-
if (context.scope
|
|
1104
|
+
if (!typeUtils.isUndefined(context.scope)) {
|
|
929
1105
|
logData.scope = context.scope;
|
|
930
1106
|
}
|
|
931
|
-
if (context.count
|
|
1107
|
+
if (!typeUtils.isUndefined(context.count)) {
|
|
932
1108
|
logData.count = context.count;
|
|
933
1109
|
}
|
|
934
|
-
if (Object.keys(configDetails)
|
|
1110
|
+
if (typeUtils.isNonEmptyArray(Object.keys(configDetails))) {
|
|
935
1111
|
logData.config = configDetails;
|
|
936
1112
|
}
|
|
937
1113
|
if (context.prevState) {
|
|
938
1114
|
logData.prevState = extractConfigDetails(context.prevState);
|
|
939
1115
|
}
|
|
940
|
-
if (Object.keys(logData)
|
|
1116
|
+
if (!typeUtils.isNonEmptyArray(Object.keys(logData))) {
|
|
941
1117
|
return configDetails;
|
|
942
1118
|
}
|
|
943
1119
|
return Object.keys(logData).length === 1 && logData.config ? configDetails : logData;
|
|
@@ -952,43 +1128,78 @@ function loggerMiddleware(context) {
|
|
|
952
1128
|
}
|
|
953
1129
|
|
|
954
1130
|
// src/middleware/performanceMiddleware.utils.ts
|
|
955
|
-
function
|
|
956
|
-
|
|
1131
|
+
function finalizeTrackedBlocks(endedBlocks, timestamp, threshold, onSlowBlock) {
|
|
1132
|
+
for (const [id, tracked] of endedBlocks) {
|
|
1133
|
+
const duration = timestamp - tracked.timestamp;
|
|
1134
|
+
if (duration >= threshold) {
|
|
1135
|
+
try {
|
|
1136
|
+
onSlowBlock?.(id, duration);
|
|
1137
|
+
} catch {
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
957
1141
|
}
|
|
958
1142
|
|
|
959
1143
|
// src/middleware/performanceMiddleware.ts
|
|
960
1144
|
var DEFAULT_SLOW_BLOCK_THRESHOLD = 3e3;
|
|
961
|
-
var ACTION_ADD = "add";
|
|
962
|
-
var ACTION_REMOVE = "remove";
|
|
963
|
-
function handleRemoveAction(context, blockStartTimes, slowBlockThreshold, onSlowBlock) {
|
|
964
|
-
const startTime = blockStartTimes.get(context.blockerId);
|
|
965
|
-
if (startTime === void 0) {
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
const duration = context.timestamp - startTime;
|
|
969
|
-
blockStartTimes.delete(context.blockerId);
|
|
970
|
-
if (duration >= slowBlockThreshold) {
|
|
971
|
-
onSlowBlock?.(context.blockerId, duration);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
1145
|
function createPerformanceMiddleware(config = {}) {
|
|
975
1146
|
const { slowBlockThreshold = DEFAULT_SLOW_BLOCK_THRESHOLD, onSlowBlock } = config;
|
|
976
|
-
const
|
|
1147
|
+
const trackedBlocks = /* @__PURE__ */ new Map();
|
|
1148
|
+
const registrations = /* @__PURE__ */ new WeakMap();
|
|
977
1149
|
return (context) => {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1150
|
+
const { action, blockerId, timestamp, config: blockerConfig, scope, registration } = context;
|
|
1151
|
+
if (action === "add") {
|
|
1152
|
+
const tracked = { timestamp, scope: normalizeScope(blockerConfig?.scope), registration };
|
|
1153
|
+
trackedBlocks.set(blockerId, tracked);
|
|
1154
|
+
if (typeUtils.isDefined(registration)) {
|
|
1155
|
+
registrations.set(registration, tracked);
|
|
1156
|
+
}
|
|
1157
|
+
} else if (action === "update") {
|
|
1158
|
+
const tracked = trackedBlocks.get(blockerId);
|
|
1159
|
+
if (typeUtils.isDefined(tracked) && typeUtils.isDefined(blockerConfig?.scope)) {
|
|
1160
|
+
tracked.scope = normalizeScope(blockerConfig.scope);
|
|
1161
|
+
}
|
|
1162
|
+
} else {
|
|
1163
|
+
const endedBlocks = /* @__PURE__ */ new Map();
|
|
1164
|
+
if (action === "remove" || action === "timeout") {
|
|
1165
|
+
let tracked = trackedBlocks.get(blockerId);
|
|
1166
|
+
if (typeUtils.isDefined(registration)) {
|
|
1167
|
+
tracked = registrations.get(registration);
|
|
1168
|
+
}
|
|
1169
|
+
if (typeUtils.isDefined(tracked)) {
|
|
1170
|
+
endedBlocks.set(blockerId, tracked);
|
|
1171
|
+
if (trackedBlocks.get(blockerId) === tracked) {
|
|
1172
|
+
trackedBlocks.delete(blockerId);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
} else {
|
|
1176
|
+
for (const [id, tracked] of trackedBlocks) {
|
|
1177
|
+
const cleared = action === "clear" || typeUtils.isDefined(scope) && scopeMatchesTarget(tracked.scope, scope);
|
|
1178
|
+
if (cleared) {
|
|
1179
|
+
endedBlocks.set(id, tracked);
|
|
1180
|
+
trackedBlocks.delete(id);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
for (const tracked of endedBlocks.values()) {
|
|
1185
|
+
const { registration: trackedRegistration } = tracked;
|
|
1186
|
+
if (typeUtils.isDefined(trackedRegistration)) {
|
|
1187
|
+
registrations.delete(trackedRegistration);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
finalizeTrackedBlocks(endedBlocks, timestamp, slowBlockThreshold, onSlowBlock);
|
|
982
1191
|
}
|
|
983
1192
|
};
|
|
984
1193
|
}
|
|
985
1194
|
|
|
986
1195
|
// src/createTypedHooks.ts
|
|
987
1196
|
function createTypedHooks() {
|
|
1197
|
+
const useTypedActionBlocker = (blockerId, config, isActive) => {
|
|
1198
|
+
useActionBlocker(blockerId, config, isActive);
|
|
1199
|
+
};
|
|
988
1200
|
return {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
},
|
|
1201
|
+
useActionBlocker: useTypedActionBlocker,
|
|
1202
|
+
useBlocker: useTypedActionBlocker,
|
|
992
1203
|
useIsBlocked: (scope) => {
|
|
993
1204
|
return useIsBlocked(scope);
|
|
994
1205
|
},
|
|
@@ -999,24 +1210,20 @@ function createTypedHooks() {
|
|
|
999
1210
|
};
|
|
1000
1211
|
}
|
|
1001
1212
|
|
|
1002
|
-
|
|
1003
|
-
function useOptionalContext() {
|
|
1004
|
-
return useOptionalUIBlockingContext();
|
|
1005
|
-
}
|
|
1006
|
-
function useResolvedStore() {
|
|
1007
|
-
return useResolvedStoreApi();
|
|
1008
|
-
}
|
|
1009
|
-
function useResolvedStoreWithSelector(selector) {
|
|
1010
|
-
return useResolvedValue(selector);
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1213
|
+
exports.DEFAULT_SCOPE = DEFAULT_SCOPE;
|
|
1013
1214
|
exports.UIBlockingProvider = UIBlockingProvider;
|
|
1014
1215
|
exports.configureMiddleware = configureMiddleware;
|
|
1015
1216
|
exports.createAnalyticsMiddleware = createAnalyticsMiddleware;
|
|
1217
|
+
exports.createBlockingLifecycle = createBlockingLifecycle;
|
|
1016
1218
|
exports.createPerformanceMiddleware = createPerformanceMiddleware;
|
|
1017
1219
|
exports.createTypedHooks = createTypedHooks;
|
|
1018
1220
|
exports.loggerMiddleware = loggerMiddleware;
|
|
1221
|
+
exports.normalizeScope = normalizeScope;
|
|
1222
|
+
exports.resolveScope = resolveScope;
|
|
1223
|
+
exports.scopeAffectsObservation = scopeAffectsObservation;
|
|
1224
|
+
exports.scopeMatchesTarget = scopeMatchesTarget;
|
|
1019
1225
|
exports.uiBlockingStoreApi = uiBlockingStoreApi;
|
|
1226
|
+
exports.useActionBlocker = useActionBlocker;
|
|
1020
1227
|
exports.useAsyncAction = useAsyncAction;
|
|
1021
1228
|
exports.useBlocker = useBlocker;
|
|
1022
1229
|
exports.useBlockingInfo = useBlockingInfo;
|