1 | import {
|
2 | ActionFromMatcher,
|
3 | hasMatchFunction,
|
4 | Matcher,
|
5 | UnionToIntersection
|
6 | } from './tsHelpers'
|
7 | import {
|
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(action: any, validStatus: string[]) {
|
73 | if (!action || !action.meta) return false
|
74 |
|
75 | const hasValidRequestId = typeof action.meta.requestId === 'string'
|
76 | const hasValidRequestStatus =
|
77 | validStatus.indexOf(action.meta.requestStatus) > -1
|
78 |
|
79 | return hasValidRequestId && hasValidRequestStatus
|
80 | }
|
81 |
|
82 | function isAsyncThunkArray(a: [any] | AnyAsyncThunk[]): a is AnyAsyncThunk[] {
|
83 | return (
|
84 | typeof a[0] === 'function' &&
|
85 | 'pending' in a[0] &&
|
86 | 'fulfilled' in a[0] &&
|
87 | 'rejected' in a[0]
|
88 | )
|
89 | }
|
90 |
|
91 | export type UnknownAsyncThunkPendingAction = ReturnType<
|
92 | AsyncThunkPendingActionCreator<unknown>
|
93 | >
|
94 |
|
95 | export type PendingActionFromAsyncThunk<
|
96 | T extends AnyAsyncThunk
|
97 | > = ActionFromMatcher<T['pending']>
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 | export function isPending(): (
|
107 | action: any
|
108 | ) => action is UnknownAsyncThunkPendingAction
|
109 | /**
|
110 | * A higher-order function that returns a function that may be used to check
|
111 | * whether an action belongs to one of the provided async thunk action creators,
|
112 | * and that the action is pending.
|
113 | *
|
114 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
115 | *
|
116 | * @public
|
117 | */
|
118 | export function isPending<
|
119 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
120 | >(
|
121 | ...asyncThunks: AsyncThunks
|
122 | ): (action: any) => action is PendingActionFromAsyncThunk<AsyncThunks[number]>
|
123 | /**
|
124 | * Tests if `action` is a pending thunk action
|
125 | * @public
|
126 | */
|
127 | export function isPending(action: any): action is UnknownAsyncThunkPendingAction
|
128 | export function isPending<
|
129 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
130 | >(...asyncThunks: AsyncThunks | [any]) {
|
131 | if (asyncThunks.length === 0) {
|
132 | return (action: any) => hasExpectedRequestMetadata(action, ['pending'])
|
133 | }
|
134 |
|
135 | if (!isAsyncThunkArray(asyncThunks)) {
|
136 | return isPending()(asyncThunks[0])
|
137 | }
|
138 |
|
139 | return (
|
140 | action: any
|
141 | ): action is PendingActionFromAsyncThunk<AsyncThunks[number]> => {
|
142 | // note: this type will be correct because we have at least 1 asyncThunk
|
143 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
144 | asyncThunk => asyncThunk.pending
|
145 | ) as any
|
146 |
|
147 | const combinedMatcher = isAnyOf(...matchers)
|
148 |
|
149 | return combinedMatcher(action)
|
150 | }
|
151 | }
|
152 |
|
153 | export type UnknownAsyncThunkRejectedAction = ReturnType<
|
154 | AsyncThunkRejectedActionCreator<unknown, unknown>
|
155 | >
|
156 |
|
157 | export type RejectedActionFromAsyncThunk<
|
158 | T extends AnyAsyncThunk
|
159 | > = ActionFromMatcher<T['rejected']>
|
160 |
|
161 | /**
|
162 | * A higher-order function that returns a function that may be used to check
|
163 | * whether an action was created by an async thunk action creator, and that
|
164 | * the action is rejected.
|
165 | *
|
166 | * @public
|
167 | */
|
168 | export function isRejected(): (
|
169 | action: any
|
170 | ) => action is UnknownAsyncThunkRejectedAction
|
171 | /**
|
172 | * A higher-order function that returns a function that may be used to check
|
173 | * whether an action belongs to one of the provided async thunk action creators,
|
174 | * and that the action is rejected.
|
175 | *
|
176 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
177 | *
|
178 | * @public
|
179 | */
|
180 | export function isRejected<
|
181 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
182 | >(
|
183 | ...asyncThunks: AsyncThunks
|
184 | ): (action: any) => action is RejectedActionFromAsyncThunk<AsyncThunks[number]>
|
185 | /**
|
186 | * Tests if `action` is a rejected thunk action
|
187 | * @public
|
188 | */
|
189 | export function isRejected(
|
190 | action: any
|
191 | ): action is UnknownAsyncThunkRejectedAction
|
192 | export function isRejected<
|
193 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
194 | >(...asyncThunks: AsyncThunks | [any]) {
|
195 | if (asyncThunks.length === 0) {
|
196 | return (action: any) => hasExpectedRequestMetadata(action, ['rejected'])
|
197 | }
|
198 |
|
199 | if (!isAsyncThunkArray(asyncThunks)) {
|
200 | return isRejected()(asyncThunks[0])
|
201 | }
|
202 |
|
203 | return (
|
204 | action: any
|
205 | ): action is RejectedActionFromAsyncThunk<AsyncThunks[number]> => {
|
206 | // note: this type will be correct because we have at least 1 asyncThunk
|
207 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
208 | asyncThunk => asyncThunk.rejected
|
209 | ) as any
|
210 |
|
211 | const combinedMatcher = isAnyOf(...matchers)
|
212 |
|
213 | return combinedMatcher(action)
|
214 | }
|
215 | }
|
216 |
|
217 | export type UnknownAsyncThunkRejectedWithValueAction = ReturnType<
|
218 | AsyncThunkRejectedActionCreator<unknown, unknown>
|
219 | >
|
220 |
|
221 | export type RejectedWithValueActionFromAsyncThunk<
|
222 | 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<
|
294 | T extends AnyAsyncThunk
|
295 | > = ActionFromMatcher<T['fulfilled']>
|
296 |
|
297 | /**
|
298 | * A higher-order function that returns a function that may be used to check
|
299 | * whether an action was created by an async thunk action creator, and that
|
300 | * the action is fulfilled.
|
301 | *
|
302 | * @public
|
303 | */
|
304 | export function isFulfilled(): (
|
305 | action: any
|
306 | ) => action is UnknownAsyncThunkFulfilledAction
|
307 | /**
|
308 | * A higher-order function that returns a function that may be used to check
|
309 | * whether an action belongs to one of the provided async thunk action creators,
|
310 | * and that the action is fulfilled.
|
311 | *
|
312 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
313 | *
|
314 | * @public
|
315 | */
|
316 | export function isFulfilled<
|
317 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
318 | >(
|
319 | ...asyncThunks: AsyncThunks
|
320 | ): (action: any) => action is FulfilledActionFromAsyncThunk<AsyncThunks[number]>
|
321 | /**
|
322 | * Tests if `action` is a fulfilled thunk action
|
323 | * @public
|
324 | */
|
325 | export function isFulfilled(
|
326 | action: any
|
327 | ): action is UnknownAsyncThunkFulfilledAction
|
328 | export function isFulfilled<
|
329 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
330 | >(...asyncThunks: AsyncThunks | [any]) {
|
331 | if (asyncThunks.length === 0) {
|
332 | return (action: any) => hasExpectedRequestMetadata(action, ['fulfilled'])
|
333 | }
|
334 |
|
335 | if (!isAsyncThunkArray(asyncThunks)) {
|
336 | return isFulfilled()(asyncThunks[0])
|
337 | }
|
338 |
|
339 | return (
|
340 | action: any
|
341 | ): action is FulfilledActionFromAsyncThunk<AsyncThunks[number]> => {
|
342 | // note: this type will be correct because we have at least 1 asyncThunk
|
343 | const matchers: [Matcher<any>, ...Matcher<any>[]] = asyncThunks.map(
|
344 | asyncThunk => asyncThunk.fulfilled
|
345 | ) as any
|
346 |
|
347 | const combinedMatcher = isAnyOf(...matchers)
|
348 |
|
349 | return combinedMatcher(action)
|
350 | }
|
351 | }
|
352 |
|
353 | export type UnknownAsyncThunkAction =
|
354 | | UnknownAsyncThunkPendingAction
|
355 | | UnknownAsyncThunkRejectedAction
|
356 | | UnknownAsyncThunkFulfilledAction
|
357 |
|
358 | export type AnyAsyncThunk = AsyncThunk<any, any, any>
|
359 |
|
360 | export type ActionsFromAsyncThunk<T extends AnyAsyncThunk> =
|
361 | | ActionFromMatcher<T['pending']>
|
362 | | ActionFromMatcher<T['fulfilled']>
|
363 | | ActionFromMatcher<T['rejected']>
|
364 |
|
365 | /**
|
366 | * A higher-order function that returns a function that may be used to check
|
367 | * whether an action was created by an async thunk action creator.
|
368 | *
|
369 | * @public
|
370 | */
|
371 | export function isAsyncThunkAction(): (
|
372 | action: any
|
373 | ) => action is UnknownAsyncThunkAction
|
374 | /**
|
375 | * A higher-order function that returns a function that may be used to check
|
376 | * whether an action belongs to one of the provided async thunk action creators.
|
377 | *
|
378 | * @param asyncThunks (optional) The async thunk action creators to match against.
|
379 | *
|
380 | * @public
|
381 | */
|
382 | export function isAsyncThunkAction<
|
383 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
384 | >(
|
385 | ...asyncThunks: AsyncThunks
|
386 | ): (action: any) => action is ActionsFromAsyncThunk<AsyncThunks[number]>
|
387 | /**
|
388 | * Tests if `action` is a thunk action
|
389 | * @public
|
390 | */
|
391 | export function isAsyncThunkAction(
|
392 | action: any
|
393 | ): action is UnknownAsyncThunkAction
|
394 | export function isAsyncThunkAction<
|
395 | AsyncThunks extends [AnyAsyncThunk, ...AnyAsyncThunk[]]
|
396 | >(...asyncThunks: AsyncThunks | [any]) {
|
397 | if (asyncThunks.length === 0) {
|
398 | return (action: any) =>
|
399 | hasExpectedRequestMetadata(action, ['pending', 'fulfilled', 'rejected'])
|
400 | }
|
401 |
|
402 | if (!isAsyncThunkArray(asyncThunks)) {
|
403 | return isAsyncThunkAction()(asyncThunks[0])
|
404 | }
|
405 |
|
406 | return (
|
407 | action: any
|
408 | ): action is ActionsFromAsyncThunk<AsyncThunks[number]> => {
|
409 | // note: this type will be correct because we have at least 1 asyncThunk
|
410 | const matchers: [Matcher<any>, ...Matcher<any>[]] = [] as any
|
411 |
|
412 | for (const asyncThunk of asyncThunks) {
|
413 | matchers.push(
|
414 | asyncThunk.pending,
|
415 | asyncThunk.rejected,
|
416 | asyncThunk.fulfilled
|
417 | )
|
418 | }
|
419 |
|
420 | const combinedMatcher = isAnyOf(...matchers)
|
421 |
|
422 | return combinedMatcher(action)
|
423 | }
|
424 | }
|
425 |
|
\ | No newline at end of file |