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