1 | import type { Dispatch, AnyAction } from 'redux'
|
2 | import type {
|
3 | PayloadAction,
|
4 | ActionCreatorWithPreparedPayload,
|
5 | } from './createAction'
|
6 | import { createAction } from './createAction'
|
7 | import type { ThunkDispatch } from 'redux-thunk'
|
8 | import type { FallbackIfUnknown, IsAny, IsUnknown } from './tsHelpers'
|
9 | import { nanoid } from './nanoid'
|
10 |
|
11 |
|
12 | type _Keep = PayloadAction | ActionCreatorWithPreparedPayload<any, unknown>
|
13 |
|
14 | export type BaseThunkAPI<
|
15 | S,
|
16 | E,
|
17 | D extends Dispatch = Dispatch,
|
18 | RejectedValue = undefined,
|
19 | RejectedMeta = unknown,
|
20 | FulfilledMeta = unknown
|
21 | > = {
|
22 | dispatch: D
|
23 | getState: () => S
|
24 | extra: E
|
25 | requestId: string
|
26 | signal: AbortSignal
|
27 | rejectWithValue: IsUnknown<
|
28 | RejectedMeta,
|
29 | (value: RejectedValue) => RejectWithValue<RejectedValue, RejectedMeta>,
|
30 | (
|
31 | value: RejectedValue,
|
32 | meta: RejectedMeta
|
33 | ) => RejectWithValue<RejectedValue, RejectedMeta>
|
34 | >
|
35 | fulfillWithValue: IsUnknown<
|
36 | FulfilledMeta,
|
37 | <FulfilledValue>(
|
38 | value: FulfilledValue
|
39 | ) => FulfillWithMeta<FulfilledValue, FulfilledMeta>,
|
40 | <FulfilledValue>(
|
41 | value: FulfilledValue,
|
42 | meta: FulfilledMeta
|
43 | ) => FulfillWithMeta<FulfilledValue, FulfilledMeta>
|
44 | >
|
45 | }
|
46 |
|
47 |
|
48 |
|
49 |
|
50 | export interface SerializedError {
|
51 | name?: string
|
52 | message?: string
|
53 | stack?: string
|
54 | code?: string
|
55 | }
|
56 |
|
57 | const commonProperties: Array<keyof SerializedError> = [
|
58 | 'name',
|
59 | 'message',
|
60 | 'stack',
|
61 | 'code',
|
62 | ]
|
63 |
|
64 | class RejectWithValue<Payload, RejectedMeta> {
|
65 | |
66 |
|
67 |
|
68 |
|
69 | private readonly _type!: 'RejectWithValue'
|
70 | constructor(
|
71 | public readonly payload: Payload,
|
72 | public readonly meta: RejectedMeta
|
73 | ) {}
|
74 | }
|
75 |
|
76 | class FulfillWithMeta<Payload, FulfilledMeta> {
|
77 | |
78 |
|
79 |
|
80 |
|
81 | private readonly _type!: 'FulfillWithMeta'
|
82 | constructor(
|
83 | public readonly payload: Payload,
|
84 | public readonly meta: FulfilledMeta
|
85 | ) {}
|
86 | }
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | export const miniSerializeError = (value: any): SerializedError => {
|
95 | if (typeof value === 'object' && value !== null) {
|
96 | const simpleError: SerializedError = {}
|
97 | for (const property of commonProperties) {
|
98 | if (typeof value[property] === 'string') {
|
99 | simpleError[property] = value[property]
|
100 | }
|
101 | }
|
102 |
|
103 | return simpleError
|
104 | }
|
105 |
|
106 | return { message: String(value) }
|
107 | }
|
108 |
|
109 | type AsyncThunkConfig = {
|
110 | state?: unknown
|
111 | dispatch?: Dispatch
|
112 | extra?: unknown
|
113 | rejectValue?: unknown
|
114 | serializedErrorType?: unknown
|
115 | pendingMeta?: unknown
|
116 | fulfilledMeta?: unknown
|
117 | rejectedMeta?: unknown
|
118 | }
|
119 |
|
120 | type GetState<ThunkApiConfig> = ThunkApiConfig extends {
|
121 | state: infer State
|
122 | }
|
123 | ? State
|
124 | : unknown
|
125 | type GetExtra<ThunkApiConfig> = ThunkApiConfig extends { extra: infer Extra }
|
126 | ? Extra
|
127 | : unknown
|
128 | type GetDispatch<ThunkApiConfig> = ThunkApiConfig extends {
|
129 | dispatch: infer Dispatch
|
130 | }
|
131 | ? FallbackIfUnknown<
|
132 | Dispatch,
|
133 | ThunkDispatch<
|
134 | GetState<ThunkApiConfig>,
|
135 | GetExtra<ThunkApiConfig>,
|
136 | AnyAction
|
137 | >
|
138 | >
|
139 | : ThunkDispatch<GetState<ThunkApiConfig>, GetExtra<ThunkApiConfig>, AnyAction>
|
140 |
|
141 | type GetThunkAPI<ThunkApiConfig> = BaseThunkAPI<
|
142 | GetState<ThunkApiConfig>,
|
143 | GetExtra<ThunkApiConfig>,
|
144 | GetDispatch<ThunkApiConfig>,
|
145 | GetRejectValue<ThunkApiConfig>,
|
146 | GetRejectedMeta<ThunkApiConfig>,
|
147 | GetFulfilledMeta<ThunkApiConfig>
|
148 | >
|
149 |
|
150 | type GetRejectValue<ThunkApiConfig> = ThunkApiConfig extends {
|
151 | rejectValue: infer RejectValue
|
152 | }
|
153 | ? RejectValue
|
154 | : unknown
|
155 |
|
156 | type GetPendingMeta<ThunkApiConfig> = ThunkApiConfig extends {
|
157 | pendingMeta: infer PendingMeta
|
158 | }
|
159 | ? PendingMeta
|
160 | : unknown
|
161 |
|
162 | type GetFulfilledMeta<ThunkApiConfig> = ThunkApiConfig extends {
|
163 | fulfilledMeta: infer FulfilledMeta
|
164 | }
|
165 | ? FulfilledMeta
|
166 | : unknown
|
167 |
|
168 | type GetRejectedMeta<ThunkApiConfig> = ThunkApiConfig extends {
|
169 | rejectedMeta: infer RejectedMeta
|
170 | }
|
171 | ? RejectedMeta
|
172 | : unknown
|
173 |
|
174 | type GetSerializedErrorType<ThunkApiConfig> = ThunkApiConfig extends {
|
175 | serializedErrorType: infer GetSerializedErrorType
|
176 | }
|
177 | ? GetSerializedErrorType
|
178 | : SerializedError
|
179 |
|
180 | type MaybePromise<T> = T | Promise<T> | (T extends any ? Promise<T> : never)
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 | export type AsyncThunkPayloadCreatorReturnValue<
|
189 | Returned,
|
190 | ThunkApiConfig extends AsyncThunkConfig
|
191 | > = MaybePromise<
|
192 | | IsUnknown<
|
193 | GetFulfilledMeta<ThunkApiConfig>,
|
194 | Returned,
|
195 | FulfillWithMeta<Returned, GetFulfilledMeta<ThunkApiConfig>>
|
196 | >
|
197 | | RejectWithValue<
|
198 | GetRejectValue<ThunkApiConfig>,
|
199 | GetRejectedMeta<ThunkApiConfig>
|
200 | >
|
201 | >
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | export type AsyncThunkPayloadCreator<
|
209 | Returned,
|
210 | ThunkArg = void,
|
211 | ThunkApiConfig extends AsyncThunkConfig = {}
|
212 | > = (
|
213 | arg: ThunkArg,
|
214 | thunkAPI: GetThunkAPI<ThunkApiConfig>
|
215 | ) => AsyncThunkPayloadCreatorReturnValue<Returned, ThunkApiConfig>
|
216 |
|
217 |
|
218 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 | export type AsyncThunkAction<
|
227 | Returned,
|
228 | ThunkArg,
|
229 | ThunkApiConfig extends AsyncThunkConfig
|
230 | > = (
|
231 | dispatch: GetDispatch<ThunkApiConfig>,
|
232 | getState: () => GetState<ThunkApiConfig>,
|
233 | extra: GetExtra<ThunkApiConfig>
|
234 | ) => Promise<
|
235 | | ReturnType<AsyncThunkFulfilledActionCreator<Returned, ThunkArg>>
|
236 | | ReturnType<AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>>
|
237 | > & {
|
238 | abort: (reason?: string) => void
|
239 | requestId: string
|
240 | arg: ThunkArg
|
241 | unwrap: () => Promise<Returned>
|
242 | }
|
243 |
|
244 | type AsyncThunkActionCreator<
|
245 | Returned,
|
246 | ThunkArg,
|
247 | ThunkApiConfig extends AsyncThunkConfig
|
248 | > = IsAny<
|
249 | ThunkArg,
|
250 |
|
251 | (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
|
252 |
|
253 | unknown extends ThunkArg
|
254 | ? (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
|
255 | : [ThunkArg] extends [void] | [undefined]
|
256 | ? () => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
|
257 | : [void] extends [ThunkArg]
|
258 | ? (arg?: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
|
259 | : [undefined] extends [ThunkArg]
|
260 | ? WithStrictNullChecks<
|
261 |
|
262 | (
|
263 | arg?: ThunkArg
|
264 | ) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>,
|
265 |
|
266 | (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
|
267 | >
|
268 | : (arg: ThunkArg) => AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig>
|
269 | >
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 | export type AsyncThunkOptions<
|
277 | ThunkArg = void,
|
278 | ThunkApiConfig extends AsyncThunkConfig = {}
|
279 | > = {
|
280 | |
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | condition?(
|
287 | arg: ThunkArg,
|
288 | api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>
|
289 | ): boolean | undefined
|
290 | |
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 | dispatchConditionRejection?: boolean
|
298 |
|
299 | serializeError?: (x: unknown) => GetSerializedErrorType<ThunkApiConfig>
|
300 |
|
301 | |
302 |
|
303 |
|
304 |
|
305 |
|
306 | idGenerator?: () => string
|
307 | } & IsUnknown<
|
308 | GetPendingMeta<ThunkApiConfig>,
|
309 | {
|
310 | |
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | getPendingMeta?(
|
317 | base: {
|
318 | arg: ThunkArg
|
319 | requestId: string
|
320 | },
|
321 | api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>
|
322 | ): GetPendingMeta<ThunkApiConfig>
|
323 | },
|
324 | {
|
325 | |
326 |
|
327 |
|
328 | getPendingMeta(
|
329 | base: {
|
330 | arg: ThunkArg
|
331 | requestId: string
|
332 | },
|
333 | api: Pick<GetThunkAPI<ThunkApiConfig>, 'getState' | 'extra'>
|
334 | ): GetPendingMeta<ThunkApiConfig>
|
335 | }
|
336 | >
|
337 |
|
338 | export type AsyncThunkPendingActionCreator<
|
339 | ThunkArg,
|
340 | ThunkApiConfig = {}
|
341 | > = ActionCreatorWithPreparedPayload<
|
342 | [string, ThunkArg, GetPendingMeta<ThunkApiConfig>?],
|
343 | undefined,
|
344 | string,
|
345 | never,
|
346 | {
|
347 | arg: ThunkArg
|
348 | requestId: string
|
349 | requestStatus: 'pending'
|
350 | } & GetPendingMeta<ThunkApiConfig>
|
351 | >
|
352 |
|
353 | export type AsyncThunkRejectedActionCreator<
|
354 | ThunkArg,
|
355 | ThunkApiConfig = {}
|
356 | > = ActionCreatorWithPreparedPayload<
|
357 | [
|
358 | Error | null,
|
359 | string,
|
360 | ThunkArg,
|
361 | GetRejectValue<ThunkApiConfig>?,
|
362 | GetRejectedMeta<ThunkApiConfig>?
|
363 | ],
|
364 | GetRejectValue<ThunkApiConfig> | undefined,
|
365 | string,
|
366 | GetSerializedErrorType<ThunkApiConfig>,
|
367 | {
|
368 | arg: ThunkArg
|
369 | requestId: string
|
370 | requestStatus: 'rejected'
|
371 | aborted: boolean
|
372 | condition: boolean
|
373 | } & (
|
374 | | ({ rejectedWithValue: false } & {
|
375 | [K in keyof GetRejectedMeta<ThunkApiConfig>]?: undefined
|
376 | })
|
377 | | ({ rejectedWithValue: true } & GetRejectedMeta<ThunkApiConfig>)
|
378 | )
|
379 | >
|
380 |
|
381 | export type AsyncThunkFulfilledActionCreator<
|
382 | Returned,
|
383 | ThunkArg,
|
384 | ThunkApiConfig = {}
|
385 | > = ActionCreatorWithPreparedPayload<
|
386 | [Returned, string, ThunkArg, GetFulfilledMeta<ThunkApiConfig>?],
|
387 | Returned,
|
388 | string,
|
389 | never,
|
390 | {
|
391 | arg: ThunkArg
|
392 | requestId: string
|
393 | requestStatus: 'fulfilled'
|
394 | } & GetFulfilledMeta<ThunkApiConfig>
|
395 | >
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 | export type AsyncThunk<
|
404 | Returned,
|
405 | ThunkArg,
|
406 | ThunkApiConfig extends AsyncThunkConfig
|
407 | > = AsyncThunkActionCreator<Returned, ThunkArg, ThunkApiConfig> & {
|
408 | pending: AsyncThunkPendingActionCreator<ThunkArg, ThunkApiConfig>
|
409 | rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig>
|
410 | fulfilled: AsyncThunkFulfilledActionCreator<
|
411 | Returned,
|
412 | ThunkArg,
|
413 | ThunkApiConfig
|
414 | >
|
415 | typePrefix: string
|
416 | }
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 |
|
424 |
|
425 |
|
426 | export function createAsyncThunk<
|
427 | Returned,
|
428 | ThunkArg = void,
|
429 | ThunkApiConfig extends AsyncThunkConfig = {}
|
430 | >(
|
431 | typePrefix: string,
|
432 | payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg, ThunkApiConfig>,
|
433 | options?: AsyncThunkOptions<ThunkArg, ThunkApiConfig>
|
434 | ): AsyncThunk<Returned, ThunkArg, ThunkApiConfig> {
|
435 | type RejectedValue = GetRejectValue<ThunkApiConfig>
|
436 | type PendingMeta = GetPendingMeta<ThunkApiConfig>
|
437 | type FulfilledMeta = GetFulfilledMeta<ThunkApiConfig>
|
438 | type RejectedMeta = GetRejectedMeta<ThunkApiConfig>
|
439 |
|
440 | const fulfilled: AsyncThunkFulfilledActionCreator<
|
441 | Returned,
|
442 | ThunkArg,
|
443 | ThunkApiConfig
|
444 | > = createAction(
|
445 | typePrefix + '/fulfilled',
|
446 | (
|
447 | payload: Returned,
|
448 | requestId: string,
|
449 | arg: ThunkArg,
|
450 | meta?: FulfilledMeta
|
451 | ) => ({
|
452 | payload,
|
453 | meta: {
|
454 | ...((meta as any) || {}),
|
455 | arg,
|
456 | requestId,
|
457 | requestStatus: 'fulfilled' as const,
|
458 | },
|
459 | })
|
460 | )
|
461 |
|
462 | const pending: AsyncThunkPendingActionCreator<ThunkArg, ThunkApiConfig> =
|
463 | createAction(
|
464 | typePrefix + '/pending',
|
465 | (requestId: string, arg: ThunkArg, meta?: PendingMeta) => ({
|
466 | payload: undefined,
|
467 | meta: {
|
468 | ...((meta as any) || {}),
|
469 | arg,
|
470 | requestId,
|
471 | requestStatus: 'pending' as const,
|
472 | },
|
473 | })
|
474 | )
|
475 |
|
476 | const rejected: AsyncThunkRejectedActionCreator<ThunkArg, ThunkApiConfig> =
|
477 | createAction(
|
478 | typePrefix + '/rejected',
|
479 | (
|
480 | error: Error | null,
|
481 | requestId: string,
|
482 | arg: ThunkArg,
|
483 | payload?: RejectedValue,
|
484 | meta?: RejectedMeta
|
485 | ) => ({
|
486 | payload,
|
487 | error: ((options && options.serializeError) || miniSerializeError)(
|
488 | error || 'Rejected'
|
489 | ) as GetSerializedErrorType<ThunkApiConfig>,
|
490 | meta: {
|
491 | ...((meta as any) || {}),
|
492 | arg,
|
493 | requestId,
|
494 | rejectedWithValue: !!payload,
|
495 | requestStatus: 'rejected' as const,
|
496 | aborted: error?.name === 'AbortError',
|
497 | condition: error?.name === 'ConditionError',
|
498 | },
|
499 | })
|
500 | )
|
501 |
|
502 | let displayedWarning = false
|
503 |
|
504 | const AC =
|
505 | typeof AbortController !== 'undefined'
|
506 | ? AbortController
|
507 | : class implements AbortController {
|
508 | signal: AbortSignal = {
|
509 | aborted: false,
|
510 | addEventListener() {},
|
511 | dispatchEvent() {
|
512 | return false
|
513 | },
|
514 | onabort() {},
|
515 | removeEventListener() {},
|
516 | }
|
517 | abort() {
|
518 | if (process.env.NODE_ENV !== 'production') {
|
519 | if (!displayedWarning) {
|
520 | displayedWarning = true
|
521 | console.info(
|
522 | `This platform does not implement AbortController.
|
523 | If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.`
|
524 | )
|
525 | }
|
526 | }
|
527 | }
|
528 | }
|
529 |
|
530 | function actionCreator(
|
531 | arg: ThunkArg
|
532 | ): AsyncThunkAction<Returned, ThunkArg, ThunkApiConfig> {
|
533 | return (dispatch, getState, extra) => {
|
534 | const requestId = (options?.idGenerator ?? nanoid)()
|
535 |
|
536 | const abortController = new AC()
|
537 | let abortReason: string | undefined
|
538 |
|
539 | const abortedPromise = new Promise<never>((_, reject) =>
|
540 | abortController.signal.addEventListener('abort', () =>
|
541 | reject({ name: 'AbortError', message: abortReason || 'Aborted' })
|
542 | )
|
543 | )
|
544 |
|
545 | let started = false
|
546 | function abort(reason?: string) {
|
547 | if (started) {
|
548 | abortReason = reason
|
549 | abortController.abort()
|
550 | }
|
551 | }
|
552 |
|
553 | const promise = (async function () {
|
554 | let finalAction: ReturnType<typeof fulfilled | typeof rejected>
|
555 | try {
|
556 | if (
|
557 | options &&
|
558 | options.condition &&
|
559 | options.condition(arg, { getState, extra }) === false
|
560 | ) {
|
561 |
|
562 | throw {
|
563 | name: 'ConditionError',
|
564 | message: 'Aborted due to condition callback returning false.',
|
565 | }
|
566 | }
|
567 | started = true
|
568 | dispatch(
|
569 | pending(
|
570 | requestId,
|
571 | arg,
|
572 | options?.getPendingMeta?.({ requestId, arg }, { getState, extra })
|
573 | )
|
574 | )
|
575 | finalAction = await Promise.race([
|
576 | abortedPromise,
|
577 | Promise.resolve(
|
578 | payloadCreator(arg, {
|
579 | dispatch,
|
580 | getState,
|
581 | extra,
|
582 | requestId,
|
583 | signal: abortController.signal,
|
584 | rejectWithValue: ((
|
585 | value: RejectedValue,
|
586 | meta?: RejectedMeta
|
587 | ) => {
|
588 | return new RejectWithValue(value, meta)
|
589 | }) as any,
|
590 | fulfillWithValue: ((value: unknown, meta?: FulfilledMeta) => {
|
591 | return new FulfillWithMeta(value, meta)
|
592 | }) as any,
|
593 | })
|
594 | ).then((result) => {
|
595 | if (result instanceof RejectWithValue) {
|
596 | throw result
|
597 | }
|
598 | if (result instanceof FulfillWithMeta) {
|
599 | return fulfilled(result.payload, requestId, arg, result.meta)
|
600 | }
|
601 | return fulfilled(result as any, requestId, arg)
|
602 | }),
|
603 | ])
|
604 | } catch (err) {
|
605 | finalAction =
|
606 | err instanceof RejectWithValue
|
607 | ? rejected(null, requestId, arg, err.payload, err.meta)
|
608 | : rejected(err as any, requestId, arg)
|
609 | }
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 | const skipDispatch =
|
616 | options &&
|
617 | !options.dispatchConditionRejection &&
|
618 | rejected.match(finalAction) &&
|
619 | (finalAction as any).meta.condition
|
620 |
|
621 | if (!skipDispatch) {
|
622 | dispatch(finalAction)
|
623 | }
|
624 | return finalAction
|
625 | })()
|
626 | return Object.assign(promise as Promise<any>, {
|
627 | abort,
|
628 | requestId,
|
629 | arg,
|
630 | unwrap() {
|
631 | return promise.then<any>(unwrapResult)
|
632 | },
|
633 | })
|
634 | }
|
635 | }
|
636 |
|
637 | return Object.assign(
|
638 | actionCreator as AsyncThunkActionCreator<
|
639 | Returned,
|
640 | ThunkArg,
|
641 | ThunkApiConfig
|
642 | >,
|
643 | {
|
644 | pending,
|
645 | rejected,
|
646 | fulfilled,
|
647 | typePrefix,
|
648 | }
|
649 | )
|
650 | }
|
651 |
|
652 | interface UnwrappableAction {
|
653 | payload: any
|
654 | meta?: any
|
655 | error?: any
|
656 | }
|
657 |
|
658 | type UnwrappedActionPayload<T extends UnwrappableAction> = Exclude<
|
659 | T,
|
660 | { error: any }
|
661 | >['payload']
|
662 |
|
663 | /**
|
664 | * @public
|
665 | */
|
666 | export function unwrapResult<R extends UnwrappableAction>(
|
667 | action: R
|
668 | ): UnwrappedActionPayload<R> {
|
669 | if (action.meta && action.meta.rejectedWithValue) {
|
670 | throw action.payload
|
671 | }
|
672 | if (action.error) {
|
673 | throw action.error
|
674 | }
|
675 | return action.payload
|
676 | }
|
677 |
|
678 | type WithStrictNullChecks<True, False> = undefined extends boolean
|
679 | ? False
|
680 | : True
|
681 |
|
\ | No newline at end of file |