Skip to content

Commit f6530ee

Browse files
author
Lenz Weber
committed
* add race between payloadCreator and abortedPromise
* simplify createAsyncThunk * remove complicated logic where an AbortError thrown from the `payloadCreator` could influence the return value
1 parent 13ae697 commit f6530ee

File tree

2 files changed

+49
-45
lines changed

2 files changed

+49
-45
lines changed

src/createAsyncThunk.test.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ describe('createAsyncThunk with abortController', () => {
162162
message: 'AbortReason',
163163
name: 'AbortError'
164164
},
165-
meta: { aborted: true, abortReason: 'AbortReason' }
165+
meta: { aborted: true }
166166
}
167167
// abortedAction with reason is dispatched after test/pending is dispatched
168168
expect(store.getState()).toMatchObject([
@@ -172,24 +172,15 @@ describe('createAsyncThunk with abortController', () => {
172172
])
173173

174174
// same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
175-
expect(result).toMatchObject({
176-
...expectedAbortedAction,
177-
error: {
178-
message: 'Was aborted while running',
179-
name: 'AbortError'
180-
}
181-
})
175+
expect(result).toMatchObject(expectedAbortedAction)
182176

183177
// calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
184178
expect(() => unwrapResult(result)).toThrowError(
185-
expect.objectContaining({
186-
message: 'Was aborted while running',
187-
name: 'AbortError'
188-
})
179+
expect.objectContaining(expectedAbortedAction.error)
189180
)
190181
})
191182

192-
test('even when the payloadCreator does not directly support the signal, no further actions ', async () => {
183+
test('even when the payloadCreator does not directly support the signal, no further actions are dispatched', async () => {
193184
const unawareAsyncThunk = createAsyncThunk('unaware', async () => {
194185
await new Promise(resolve => setTimeout(resolve, 100))
195186
return 'finished'
@@ -222,4 +213,24 @@ describe('createAsyncThunk with abortController', () => {
222213
expect.objectContaining(expectedAbortedAction.error)
223214
)
224215
})
216+
217+
test('dispatch(asyncThunk) returns on abort and does not wait for the promiseProvider to finish', async () => {
218+
let running = false
219+
const longRunningAsyncThunk = createAsyncThunk('longRunning', async () => {
220+
running = true
221+
await new Promise(resolve => setTimeout(resolve, 30000))
222+
running = false
223+
})
224+
225+
const promise = store.dispatch(longRunningAsyncThunk())
226+
expect(running).toBeTruthy()
227+
promise.abort()
228+
const result = await promise
229+
expect(running).toBeTruthy()
230+
expect(result).toMatchObject({
231+
type: 'longRunning/rejected',
232+
error: { message: 'Aborted.', name: 'AbortError' },
233+
meta: { aborted: true }
234+
})
235+
})
225236
})

src/createAsyncThunk.ts

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,7 @@ export function createAsyncThunk<
133133
meta: {
134134
arg,
135135
requestId,
136-
aborted,
137-
abortReason: aborted ? error.message : undefined
136+
aborted
138137
}
139138
}
140139
}
@@ -147,53 +146,47 @@ export function createAsyncThunk<
147146
extra: GetExtra<ThunkApiConfig>
148147
) => {
149148
const requestId = nanoid()
149+
150150
const abortController = new AbortController()
151-
let abortAction: ReturnType<typeof rejected> | undefined
151+
let abortReason: string | undefined
152152

153-
function abort(reason: string = 'Aborted.') {
154-
abortController.abort()
155-
abortAction = rejected(
156-
{ name: 'AbortError', message: reason },
157-
requestId,
158-
arg
153+
const abortedPromise = new Promise<never>((_, reject) =>
154+
abortController.signal.addEventListener('abort', () =>
155+
reject({ name: 'AbortError', message: abortReason || 'Aborted.' })
159156
)
160-
dispatch(abortAction)
157+
)
158+
159+
function abort(reason?: string) {
160+
abortReason = reason
161+
abortController.abort()
161162
}
162163

163164
const promise = (async function() {
164165
let finalAction: ReturnType<typeof fulfilled | typeof rejected>
165166
try {
166167
dispatch(pending(requestId, arg))
167-
168-
finalAction = fulfilled(
169-
await payloadCreator(arg, {
170-
dispatch,
171-
getState,
172-
extra,
173-
requestId,
174-
signal: abortController.signal
175-
}),
176-
requestId,
177-
arg
178-
)
168+
finalAction = await Promise.race([
169+
abortedPromise,
170+
Promise.resolve(
171+
payloadCreator(arg, {
172+
dispatch,
173+
getState,
174+
extra,
175+
requestId,
176+
signal: abortController.signal
177+
})
178+
).then(result => fulfilled(result, requestId, arg))
179+
])
179180
} catch (err) {
180-
if (err && err.name === 'AbortError' && abortAction) {
181-
abortAction = { ...abortAction, error: miniSerializeError(err) }
182-
}
183181
finalAction = rejected(err, requestId, arg)
184182
}
185183
// We dispatch the result action _after_ the catch, to avoid having any errors
186184
// here get swallowed by the try/catch block,
187185
// per https://twitter.com/dan_abramov/status/770914221638942720
188186
// and https://redux-toolkit.js.org/tutorials/advanced-tutorial#async-error-handling-logic-in-thunks
189187

190-
// If abortAction has been set, we return that and do not dispatch any more fulfilled/rejected actions.
191-
if (abortAction) {
192-
return abortAction
193-
} else {
194-
dispatch(finalAction)
195-
return finalAction
196-
}
188+
dispatch(finalAction)
189+
return finalAction
197190
})()
198191
return Object.assign(promise, { abort })
199192
}

0 commit comments

Comments
 (0)