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