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