Skip to content

Commit c76d1c5

Browse files
committed
Consolidate sync/async error handling and expose job handle
1 parent b978085 commit c76d1c5

File tree

3 files changed

+53
-37
lines changed

3 files changed

+53
-37
lines changed

packages/action-listener-middleware/src/index.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ import type {
3030
ListenerErrorInfo,
3131
} from './types'
3232

33+
import {
34+
Job,
35+
SupervisorJob,
36+
JobHandle,
37+
JobCancellationReason,
38+
JobCancellationException,
39+
} from './job'
40+
import { Outcome } from './outcome'
41+
3342
export type {
3443
ActionListener,
3544
ActionListenerMiddleware,
@@ -132,6 +141,7 @@ export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
132141
unsubscribe: () => {
133142
throw new Error('Unsubscribe not initialized')
134143
},
144+
parentJob: new SupervisorJob(),
135145
}
136146

137147
return entry
@@ -342,7 +352,6 @@ export function createActionListenerMiddleware<
342352
runListener = false
343353

344354
safelyNotifyError(onError, predicateError, {
345-
async: false,
346355
raisedBy: 'predicate',
347356
phase: currentPhase,
348357
})
@@ -353,38 +362,39 @@ export function createActionListenerMiddleware<
353362
continue
354363
}
355364

356-
try {
357-
let promiseLikeOrUndefined = entry.listener(action, {
358-
...api,
359-
getOriginalState,
360-
condition,
361-
take,
362-
currentPhase,
363-
extra,
364-
unsubscribe: entry.unsubscribe,
365-
subscribe: () => {
366-
listenerMap.set(entry.id, entry)
367-
},
368-
})
369-
370-
if (promiseLikeOrUndefined) {
371-
Promise.resolve(promiseLikeOrUndefined).catch(
372-
(asyncListenerError) => {
373-
safelyNotifyError(onError, asyncListenerError, {
374-
async: true,
375-
raisedBy: 'listener',
376-
phase: currentPhase,
377-
})
378-
}
379-
)
365+
entry.parentJob.launchAndRun(async (jobHandle) => {
366+
const result = await Outcome.try(async () =>
367+
entry.listener(action, {
368+
...api,
369+
getOriginalState,
370+
condition,
371+
take,
372+
currentPhase,
373+
extra,
374+
unsubscribe: entry.unsubscribe,
375+
subscribe: () => {
376+
listenerMap.set(entry.id, entry)
377+
},
378+
job: jobHandle,
379+
cancelPrevious: () => {
380+
entry.parentJob.cancelChildren(
381+
new JobCancellationException(
382+
JobCancellationReason.JobCancelled
383+
),
384+
[jobHandle]
385+
)
386+
},
387+
})
388+
)
389+
if (result.isError()) {
390+
safelyNotifyError(onError, result.error, {
391+
raisedBy: 'listener',
392+
phase: currentPhase,
393+
})
380394
}
381-
} catch (syncListenerError) {
382-
safelyNotifyError(onError, syncListenerError, {
383-
async: false,
384-
raisedBy: 'listener',
385-
phase: currentPhase,
386-
})
387-
}
395+
396+
return Outcome.ok(1)
397+
})
388398
}
389399
if (currentPhase === 'beforeReducer') {
390400
result = next(action)

packages/action-listener-middleware/src/tests/listenerMiddleware.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const middlewareApi = {
3232
currentPhase: expect.stringMatching(/beforeReducer|afterReducer/),
3333
unsubscribe: expect.any(Function),
3434
subscribe: expect.any(Function),
35+
cancelPrevious: expect.any(Function),
36+
job: expect.any(Object),
3537
}
3638

3739
const noop = () => {}
@@ -626,7 +628,7 @@ describe('createActionListenerMiddleware', () => {
626628
])
627629
})
628630

629-
test('Notifies sync listener errors to `onError`, if provided', () => {
631+
test('Notifies sync listener errors to `onError`, if provided', async () => {
630632
const onError = jest.fn()
631633
middleware = createActionListenerMiddleware({
632634
onError,
@@ -649,8 +651,9 @@ describe('createActionListenerMiddleware', () => {
649651
})
650652

651653
store.dispatch(testAction1('a'))
654+
await delay(100)
655+
652656
expect(onError).toBeCalledWith(listenerError, {
653-
async: false,
654657
raisedBy: 'listener',
655658
phase: 'afterReducer',
656659
})
@@ -679,10 +682,9 @@ describe('createActionListenerMiddleware', () => {
679682

680683
store.dispatch(testAction1('a'))
681684

682-
await Promise.resolve()
685+
await delay(100)
683686

684687
expect(onError).toBeCalledWith(listenerError, {
685-
async: true,
686688
raisedBy: 'listener',
687689
phase: 'afterReducer',
688690
})

packages/action-listener-middleware/src/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
ThunkDispatch,
99
} from '@reduxjs/toolkit'
1010

11+
import type { JobHandle } from './job'
12+
1113
/**
1214
* Types copied from RTK
1315
*/
@@ -71,6 +73,8 @@ export interface ActionListenerMiddlewareAPI<S, D extends Dispatch<AnyAction>>
7173
subscribe(): void
7274
condition: ConditionFunction<S>
7375
take: TakePattern<S>
76+
cancelPrevious: () => void
77+
job: JobHandle
7478
currentPhase: MiddlewarePhase
7579
// TODO Figure out how to pass this through the other types correctly
7680
extra: unknown
@@ -282,6 +286,7 @@ export type ListenerEntry<
282286
unsubscribe: () => void
283287
type?: string
284288
predicate: ListenerPredicate<AnyAction, S>
289+
parentJob: JobHandle
285290
}
286291

287292
const declaredMiddlewareType: unique symbol = undefined as any
@@ -322,7 +327,6 @@ export type ListenerPredicateGuardedActionType<T> = T extends ListenerPredicate<
322327
* Additional infos regarding the error raised.
323328
*/
324329
export interface ListenerErrorInfo {
325-
async: boolean
326330
/**
327331
* Which function has generated the exception.
328332
*/

0 commit comments

Comments
 (0)