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