Skip to content

Commit 777734c

Browse files
authored
Merge pull request #3775 from julian-ford/pr/add-listenerapi-cancel
2 parents 9d804fd + 47e6200 commit 777734c

File tree

4 files changed

+51
-9
lines changed

4 files changed

+51
-9
lines changed

docs/api/createListenerMiddleware.mdx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ export interface ListenerEffectAPI<
360360
* Cancels all other running instances of this same listener except for the one that made this call.
361361
*/
362362
cancelActiveListeners: () => void
363+
/**
364+
* Cancels the listener instance that made this call.
365+
*/
366+
cancel: () => void
363367
/**
364368
* An abort signal whose `aborted` property is set to `true`
365369
* if the listener execution is either aborted or completed.
@@ -403,6 +407,7 @@ These can be divided into several categories.
403407
- `unsubscribe: () => void`: removes the listener entry from the middleware, and prevent future instances of the listener from running. (This does _not_ cancel any active instances.)
404408
- `subscribe: () => void`: will re-subscribe the listener entry if it was previously removed, or no-op if currently subscribed
405409
- `cancelActiveListeners: () => void`: cancels all other running instances of this same listener _except_ for the one that made this call. (The cancellation will only have a meaningful effect if the other instances are paused using one of the cancellation-aware APIs like `take/cancel/pause/delay` - see "Cancelation and Task Management" in the "Usage" section for more details)
410+
- `cancel: () => void`: cancels the instance of this listener that made this call.
406411
- `signal: AbortSignal`: An [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) whose `aborted` property will be set to `true` if the listener execution is aborted or completed.
407412

408413
Dynamically unsubscribing and re-subscribing this listener allows for more complex async workflows, such as avoiding duplicate running instances by calling `listenerApi.unsubscribe()` at the start of a listener, or calling `listenerApi.cancelActiveListeners()` to ensure that only the most recent instance is allowed to complete.
@@ -666,7 +671,7 @@ listenerMiddleware.startListening({
666671
667672
### Complex Async Workflows
668673
669-
The provided async workflow primitives (`cancelActiveListeners`, `unsubscribe`, `subscribe`, `take`, `condition`, `pause`, `delay`) can be used to implement behavior that is equivalent to many of the more complex async workflow capabilities found in the Redux-Saga library. This includes effects such as `throttle`, `debounce`, `takeLatest`, `takeLeading`, and `fork/join`. Some examples from the test suite:
674+
The provided async workflow primitives (`cancelActiveListeners`, `cancel`, `unsubscribe`, `subscribe`, `take`, `condition`, `pause`, `delay`) can be used to implement behavior that is equivalent to many of the more complex async workflow capabilities found in the Redux-Saga library. This includes effects such as `throttle`, `debounce`, `takeLatest`, `takeLeading`, and `fork/join`. Some examples from the test suite:
670675
671676
```js
672677
test('debounce / takeLatest', async () => {

packages/toolkit/src/listenerMiddleware/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,13 @@ export function createListenerMiddleware<
419419
}
420420
})
421421
},
422+
cancel: () => {
423+
abortControllerWithReason(
424+
internalTaskController,
425+
listenerCancelled
426+
)
427+
entry.pending.delete(internalTaskController)
428+
},
422429
})
423430
)
424431
)

packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const middlewareApi = {
4545
unsubscribe: expect.any(Function),
4646
subscribe: expect.any(Function),
4747
cancelActiveListeners: expect.any(Function),
48+
cancel: expect.any(Function),
4849
}
4950

5051
const noop = () => {}
@@ -184,7 +185,7 @@ describe('createListenerMiddleware', () => {
184185
middleware: (gDM) => gDM().prepend(listenerMiddleware.middleware),
185186
})
186187

187-
let foundExtra: number | null = null
188+
let foundExtra: number | null = null
188189

189190
const typedAddListener =
190191
listenerMiddleware.startListening as TypedStartListening<
@@ -645,7 +646,32 @@ describe('createListenerMiddleware', () => {
645646
expect(await deferredCompletedSignalReason).toBe(listenerCompleted)
646647
})
647648

648-
test('"can unsubscribe via middleware api', () => {
649+
test('can self-cancel via middleware api', async () => {
650+
const notifyDeferred = createAction<Deferred<string>>('notify-deferred')
651+
652+
startListening({
653+
actionCreator: notifyDeferred,
654+
effect: async ({ payload }, { signal, cancel, delay }) => {
655+
signal.addEventListener(
656+
'abort',
657+
() => {
658+
payload.resolve((signal as AbortSignalWithReason<string>).reason)
659+
},
660+
{ once: true }
661+
)
662+
663+
cancel()
664+
},
665+
})
666+
667+
const deferredCancelledSignalReason = store.dispatch(
668+
notifyDeferred(deferred<string>())
669+
).payload
670+
671+
expect(await deferredCancelledSignalReason).toBe(listenerCancelled)
672+
})
673+
674+
test('can unsubscribe via middleware api', () => {
649675
const effect = jest.fn(
650676
(action: TestAction1, api: ListenerEffectAPI<any, any>) => {
651677
if (action.payload === 'b') {
@@ -1126,7 +1152,7 @@ describe('createListenerMiddleware', () => {
11261152
expect(takeResult).toEqual([increment(), stateCurrent, stateBefore])
11271153
})
11281154

1129-
test("take resolves to `[A, CurrentState, PreviousState] | null` if a possibly undefined timeout parameter is provided", async () => {
1155+
test('take resolves to `[A, CurrentState, PreviousState] | null` if a possibly undefined timeout parameter is provided', async () => {
11301156
const store = configureStore({
11311157
reducer: counterSlice.reducer,
11321158
middleware: (gDM) => gDM().prepend(middleware),
@@ -1160,7 +1186,7 @@ describe('createListenerMiddleware', () => {
11601186
store.dispatch(increment())
11611187

11621188
await delay(25)
1163-
expect(done).toBe(true);
1189+
expect(done).toBe(true)
11641190
})
11651191

11661192
test('condition method resolves promise when the predicate succeeds', async () => {

packages/toolkit/src/listenerMiddleware/types.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export interface ForkOptions {
137137
* If true, causes the parent task to not be marked as complete until
138138
* all autoJoined forks have completed or failed.
139139
*/
140-
autoJoin: boolean;
140+
autoJoin: boolean
141141
}
142142

143143
/** @public */
@@ -186,9 +186,9 @@ export interface ListenerEffectAPI<
186186
* rejects if the listener has been cancelled or is completed.
187187
*
188188
* The return value is `true` if the predicate succeeds or `false` if a timeout is provided and expires first.
189-
*
189+
*
190190
* ### Example
191-
*
191+
*
192192
* ```ts
193193
* const updateBy = createAction<number>('counter/updateBy');
194194
*
@@ -210,7 +210,7 @@ export interface ListenerEffectAPI<
210210
*
211211
* The return value is the `[action, currentState, previousState]` combination that the predicate saw as arguments.
212212
*
213-
* The promise resolves to null if a timeout is provided and expires first,
213+
* The promise resolves to null if a timeout is provided and expires first,
214214
*
215215
* ### Example
216216
*
@@ -233,6 +233,10 @@ export interface ListenerEffectAPI<
233233
* Cancels all other running instances of this same listener except for the one that made this call.
234234
*/
235235
cancelActiveListeners: () => void
236+
/**
237+
* Cancels the instance of this listener that made this call.
238+
*/
239+
cancel: () => void
236240
/**
237241
* An abort signal whose `aborted` property is set to `true`
238242
* if the listener execution is either aborted or completed.

0 commit comments

Comments
 (0)