1 | import type {
|
2 | ActionFromMatcher,
|
3 | Matcher,
|
4 | UnionToIntersection,
|
5 | } from './tsHelpers'
|
6 | import { hasMatchFunction } from './tsHelpers'
|
7 | import type {
|
8 | AsyncThunk,
|
9 | AsyncThunkFulfilledActionCreator,
|
10 | AsyncThunkPendingActionCreator,
|
11 | AsyncThunkRejectedActionCreator,
|
12 | } from './createAsyncThunk'
|
13 |
|
14 |
|
15 | export type ActionMatchingAnyOf<
|
16 | Matchers extends [Matcher<any>, ...Matcher<any>[]]
|
17 | > = ActionFromMatcher<Matchers[number]>
|
18 |
|
19 |
|
20 | export type ActionMatchingAllOf<
|
21 | Matchers extends [Matcher<any>, ...Matcher<any>[]]
|
22 | > = UnionToIntersection<ActionMatchingAnyOf<Matchers>>
|
23 |
|
24 | const matches = (matcher: Matcher<any>, action: any) => {
|
25 | if (hasMatchFunction(matcher)) {
|
26 | return matcher.match(action)
|
27 | } else {
|
28 | return matcher(action)
|
29 | }
|
30 | }
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | export function isAnyOf<Matchers extends [Matcher<any>, ...Matcher<any>[]]>(
|
42 | ...matchers: Matchers
|
43 | ) {
|
44 | return (action: any): action is ActionMatchingAnyOf<Matchers> => {
|
45 | return matchers.some((matcher) => matches(matcher, action))
|
46 | }
|
47 | }
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 | export function isAllOf<Matchers extends [Matcher<any>, ...Matcher<any>[]]>(
|
59 | ...matchers: Matchers
|
60 | ) {
|
61 | return (action: any): action is ActionMatchingAllOf<Matchers> => {
|
62 | return matchers.every((matcher) => matches(matcher, action))
|
63 | }
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 |
|
71 |
|
72 | export function hasExpectedRequestMetadata(
|
73 | action: any,
|
74 | validStatus: readonly string[]
|
75 | ) {
|
76 | if (!action || !action.meta) return false
|
77 |
|
78 | const hasValidRequestId = typeof action.meta.requestId === 'string'
|
79 | const hasValidRequestStatus =
|
80 | validStatus.indexOf(action.meta.requestStatus) > -1
|
81 |
|
82 | return hasValidRequestId && hasValidRequestStatus
|
83 | }
|
84 |
|
85 | function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] {
|
86 | return (
|
87 | typeof a[0] === 'function' &&
|
88 | 'pending' in a[0] &&
|
89 | 'fulfilled' in a[0] &&
|
90 | 'rejected' in a[0]
|
91 | )
|
92 | }
|
93 |
|
94 | export type UnknownAsyncThunkPendingAction = ReturnType<
|
95 | AsyncThunkPendingActionCreator<unknown>
|
96 | >
|
97 |
|
98 | export type PendingActionFromAsyncThunk<T extends AnyAsyncThunk> =
|
99 | ActionFromMatcher<T['pending']>
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | export function isPending(): (
|
109 | action: any
|
110 | ) => action is UnknownAsyncThunkPendingAction
|
111 | /**
|
112 | * A higher-order function that returns a function that may be used to check
|
113 | * whether an action belongs to one of the provided async thunk action creators,
|
114 | * and that the action is pending.
|
115 | *
|
116 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
117 | *
|
118 | * @public
|
119 | */
|
120 | export function isPending<
|
121 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
122 | >(
|
123 | ...asyncThunks: AsyncThunks
|
124 | ): (action: any) => action is PendingActionFromAsyncThunk<AsyncThunks[number]>
|
125 | /**
|
126 | * Tests if `action` is a pending thunk action
|
127 | * @public
|
128 | */
|
129 | export function isPending(action: any): action is UnknownAsyncThunkPendingAction
|
130 | export function isPending<
|
131 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
132 | >(...asyncThunks: AsyncThunks | [any]) {
|
133 | if (asyncThunks.length === 0) {
|
134 | return (action: any) => hasExpectedRequestMetadata(action, ['pending'])
|
135 | }
|
136 |
|
137 | if (!isAsyncThunkArray(asyncThunks)) {
|
138 | return isPending()(asyncThunks[0])
|
139 | }
|
140 |
|
141 | return (
|
142 | action: any
|
143 | ): action is PendingActionFromAsyncThunk<AsyncThunks[number]> => {
|
144 | // note: this type will be correct because we have at least 1 asyncThunk
|
145 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
146 | (asyncThunk) => asyncThunk.pending
|
147 | ) as any
|
148 |
|
149 | const combinedMatcher = isAnyOf(...matchers)
|
150 |
|
151 | return combinedMatcher(action)
|
152 | }
|
153 | }
|
154 |
|
155 | export type UnknownAsyncThunkRejectedAction = ReturnType<
|
156 | AsyncThunkRejectedActionCreator<unknown, unknown>
|
157 | >
|
158 |
|
159 | export type RejectedActionFromAsyncThunk<T extends AnyAsyncThunk> =
|
160 | ActionFromMatcher<T['rejected']>
|
161 |
|
162 | /**
|
163 | * A higher-order function that returns a function that may be used to check
|
164 | * whether an action was created by an async thunk action creator, and that
|
165 | * the action is rejected.
|
166 | *
|
167 | * @public
|
168 | */
|
169 | export function isRejected(): (
|
170 | action: any
|
171 | ) => action is UnknownAsyncThunkRejectedAction
|
172 | /**
|
173 | * A higher-order function that returns a function that may be used to check
|
174 | * whether an action belongs to one of the provided async thunk action creators,
|
175 | * and that the action is rejected.
|
176 | *
|
177 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
178 | *
|
179 | * @public
|
180 | */
|
181 | export function isRejected<
|
182 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
183 | >(
|
184 | ...asyncThunks: AsyncThunks
|
185 | ): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
|
186 | /**
|
187 | * Tests if `action` is a rejected thunk action
|
188 | * @public
|
189 | */
|
190 | export function isRejected(
|
191 | action: any
|
192 | ): action is UnknownAsyncThunkRejectedAction
|
193 | export function isRejected<
|
194 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
195 | >(...asyncThunks: AsyncThunks | [any]) {
|
196 | if (asyncThunks.length === 0) {
|
197 | return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
|
198 | }
|
199 |
|
200 | if (!isAsyncThunkArray(asyncThunks)) {
|
201 | return isRejected()(asyncThunks[0])
|
202 | }
|
203 |
|
204 | return (
|
205 | action: any
|
206 | ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
|
207 | // note: this type will be correct because we have at least 1 asyncThunk
|
208 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
209 | (asyncThunk) => asyncThunk.rejected
|
210 | ) as any
|
211 |
|
212 | const combinedMatcher = isAnyOf(...matchers)
|
213 |
|
214 | return combinedMatcher(action)
|
215 | }
|
216 | }
|
217 |
|
218 | export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
|
219 | AsyncThunkRejectedActionCreator<unknown, unknown>
|
220 | >
|
221 |
|
222 | export type RejectedWithValueActionFromAsyncThunk<T extends AnyAsyncThunk> =
|
223 | ActionFromMatcher<T['rejected']> &
|
224 | (T extends AsyncThunk<any, any, { rejectValue: infer RejectedValue }>
|
225 | ? { payload: RejectedValue }
|
226 | : unknown)
|
227 |
|
228 | /**
|
229 | * A higher-order function that returns a function that may be used to check
|
230 | * whether an action was created by an async thunk action creator, and that
|
231 | * the action is rejected with value.
|
232 | *
|
233 | * @public
|
234 | */
|
235 | export function isRejectedWithValue(): (
|
236 | action: any
|
237 | ) => action is UnknownAsyncThunkRejectedAction
|
238 | /**
|
239 | * A higher-order function that returns a function that may be used to check
|
240 | * whether an action belongs to one of the provided async thunk action creators,
|
241 | * and that the action is rejected with value.
|
242 | *
|
243 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
244 | *
|
245 | * @public
|
246 | */
|
247 | export function isRejectedWithValue<
|
248 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
249 | >(
|
250 | ...asyncThunks: AsyncThunks
|
251 | ): (
|
252 | action: any
|
253 | ) => action is RejectedWithValueActionFromAsyncThunk<AsyncThunks[number]>
|
254 | /**
|
255 | * Tests if `action` is a rejected thunk action with value
|
256 | * @public
|
257 | */
|
258 | export function isRejectedWithValue(
|
259 | action: any
|
260 | ): action is UnknownAsyncThunkRejectedAction
|
261 | export function isRejectedWithValue<
|
262 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
263 | >(...asyncThunks: AsyncThunks | [any]) {
|
264 | const hasFlag = (action: any): action is any => {
|
265 | return action && action.meta && action.meta.rejectedWithValue
|
266 | }
|
267 |
|
268 | if (asyncThunks.length === 0) {
|
269 | return (action: any) => {
|
270 | const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)
|
271 |
|
272 | return combinedMatcher(action)
|
273 | }
|
274 | }
|
275 |
|
276 | if (!isAsyncThunkArray(asyncThunks)) {
|
277 | return isRejectedWithValue()(asyncThunks[0])
|
278 | }
|
279 |
|
280 | return (
|
281 | action: any
|
282 | ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
|
283 | const combinedMatcher = isAllOf(isRejected(...asyncThunks), hasFlag)
|
284 |
|
285 | return combinedMatcher(action)
|
286 | }
|
287 | }
|
288 |
|
289 | export type UnknownAsyncThunkFulfilledAction = ReturnType<
|
290 | AsyncThunkFulfilledActionCreator<unknown, unknown>
|
291 | >
|
292 |
|
293 | export type FulfilledActionFromAsyncThunk<T extends AnyAsyncThunk> =
|
294 | ActionFromMatcher<T['fulfilled']>
|
295 |
|
296 | /**
|
297 | * A higher-order function that returns a function that may be used to check
|
298 | * whether an action was created by an async thunk action creator, and that
|
299 | * the action is fulfilled.
|
300 | *
|
301 | * @public
|
302 | */
|
303 | export function isFulfilled(): (
|
304 | action: any
|
305 | ) => action is UnknownAsyncThunkFulfilledAction
|
306 | /**
|
307 | * A higher-order function that returns a function that may be used to check
|
308 | * whether an action belongs to one of the provided async thunk action creators,
|
309 | * and that the action is fulfilled.
|
310 | *
|
311 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
312 | *
|
313 | * @public
|
314 | */
|
315 | export function isFulfilled<
|
316 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
317 | >(
|
318 | ...asyncThunks: AsyncThunks
|
319 | ): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
|
320 | /**
|
321 | * Tests if `action` is a fulfilled thunk action
|
322 | * @public
|
323 | */
|
324 | export function isFulfilled(
|
325 | action: any
|
326 | ): action is UnknownAsyncThunkFulfilledAction
|
327 | export function isFulfilled<
|
328 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
329 | >(...asyncThunks: AsyncThunks | [any]) {
|
330 | if (asyncThunks.length === 0) {
|
331 | return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
|
332 | }
|
333 |
|
334 | if (!isAsyncThunkArray(asyncThunks)) {
|
335 | return isFulfilled()(asyncThunks[0])
|
336 | }
|
337 |
|
338 | return (
|
339 | action: any
|
340 | ): action is FulfilledActionFromAsyncThunk<AsyncThunks[number]> => {
|
341 | // note: this type will be correct because we have at least 1 asyncThunk
|
342 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
343 | (asyncThunk) => asyncThunk.fulfilled
|
344 | ) as any
|
345 |
|
346 | const combinedMatcher = isAnyOf(...matchers)
|
347 |
|
348 | return combinedMatcher(action)
|
349 | }
|
350 | }
|
351 |
|
352 | export type UnknownAsyncThunkAction =
|
353 | | UnknownAsyncThunkPendingAction
|
354 | | UnknownAsyncThunkRejectedAction
|
355 | | UnknownAsyncThunkFulfilledAction
|
356 |
|
357 | export type AnyAsyncThunk = {
|
358 | pending: { match: (action: any) => action is any }
|
359 | fulfilled: { match: (action: any) => action is any }
|
360 | rejected: { match: (action: any) => action is any }
|
361 | }
|
362 |
|
363 | export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
|
364 | | ActionFromMatcher<T['pending']>
|
365 | | ActionFromMatcher<T['fulfilled']>
|
366 | | ActionFromMatcher<T['rejected']>
|
367 |
|
368 | /**
|
369 | * A higher-order function that returns a function that may be used to check
|
370 | * whether an action was created by an async thunk action creator.
|
371 | *
|
372 | * @public
|
373 | */
|
374 | export function isAsyncThunkAction(): (
|
375 | action: any
|
376 | ) => action is UnknownAsyncThunkAction
|
377 | /**
|
378 | * A higher-order function that returns a function that may be used to check
|
379 | * whether an action belongs to one of the provided async thunk action creators.
|
380 | *
|
381 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
382 | *
|
383 | * @public
|
384 | */
|
385 | export function isAsyncThunkAction<
|
386 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
387 | >(
|
388 | ...asyncThunks: AsyncThunks
|
389 | ): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
|
390 | /**
|
391 | * Tests if `action` is a thunk action
|
392 | * @public
|
393 | */
|
394 | export function isAsyncThunkAction(
|
395 | action: any
|
396 | ): action is UnknownAsyncThunkAction
|
397 | export function isAsyncThunkAction<
|
398 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
399 | >(...asyncThunks: AsyncThunks | [any]) {
|
400 | if (asyncThunks.length === 0) {
|
401 | return (action: any) =>
|
402 | hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
|
403 | }
|
404 |
|
405 | if (!isAsyncThunkArray(asyncThunks)) {
|
406 | return isAsyncThunkAction()(asyncThunks[0])
|
407 | }
|
408 |
|
409 | return (
|
410 | action: any
|
411 | ): action is ActionsFromAsyncThunk<AsyncThunks[number]> => {
|
412 | // note: this type will be correct because we have at least 1 asyncThunk
|
413 | const matchers: [Matcher<any>, ...Matcher<any>[]] = [] as any
|
414 |
|
415 | for (const asyncThunk of asyncThunks) {
|
416 | matchers.push(
|
417 | asyncThunk.pending,
|
418 | asyncThunk.rejected,
|
419 | asyncThunk.fulfilled
|
420 | )
|
421 | }
|
422 |
|
423 | const combinedMatcher = isAnyOf(...matchers)
|
424 |
|
425 | return combinedMatcher(action)
|
426 | }
|
427 | }
|
428 |
|
\ | No newline at end of file |