Skip to content

Commit 46fa03d

Browse files
committed
Merge branch 'master' of https://github.com/reduxjs/redux-toolkit into feat/alm-change-remove-listener-signature
2 parents 37d9ee2 + d5b0eb5 commit 46fa03d

File tree

3 files changed

+146
-7
lines changed

3 files changed

+146
-7
lines changed

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

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
AddListenerOverloads,
1313
AnyActionListenerPredicate,
1414
CreateListenerMiddlewareOptions,
15+
TypedActionCreator,
1516
TypedAddListener,
1617
TypedAddListenerAction,
1718
TypedCreateListenerEntry,
@@ -180,7 +181,7 @@ const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => {
180181
)
181182
}
182183

183-
assertFunction(listener, 'options.listener');
184+
assertFunction(listener, 'options.listener')
184185

185186
return { predicate, type, listener }
186187
}
@@ -206,6 +207,20 @@ export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
206207
return entry
207208
}
208209

210+
const createClearListenerMiddleware = (
211+
listenerMap: Map<string, ListenerEntry>
212+
) => {
213+
return () => {
214+
listenerMap.forEach((entry) => {
215+
entry.pending.forEach((controller) => {
216+
controller.abort()
217+
})
218+
})
219+
220+
listenerMap.clear()
221+
}
222+
}
223+
209224
/**
210225
* Safely reports errors to the `errorHandler` provided.
211226
* Errors that occur inside `errorHandler` are notified in a new task.
@@ -236,6 +251,11 @@ export const addListenerAction = createAction(
236251
`${alm}/add`
237252
) as TypedAddListenerAction<unknown>
238253

254+
/**
255+
* @alpha
256+
*/
257+
export const clearListenerMiddlewareAction = createAction(`${alm}/clear`)
258+
239259
/**
240260
* @alpha
241261
*/
@@ -361,6 +381,8 @@ export function createActionListenerMiddleware<
361381
}
362382
}
363383

384+
const clearListenerMiddleware = createClearListenerMiddleware(listenerMap)
385+
364386
const middleware: Middleware<
365387
{
366388
(action: Action<`${typeof alm}/add`>): Unsubscribe
@@ -371,6 +393,12 @@ export function createActionListenerMiddleware<
371393
if (addListenerAction.match(action)) {
372394
return addListener(action.payload)
373395
}
396+
397+
if (clearListenerMiddlewareAction.match(action)) {
398+
clearListenerMiddleware()
399+
return
400+
}
401+
374402
if (removeListenerAction.match(action)) {
375403
return removeListener(action.payload)
376404
}
@@ -432,7 +460,9 @@ export function createActionListenerMiddleware<
432460
addListener: addListener as TypedAddListener<S, D>,
433461
removeListener: removeListener as TypedRemoveListener<S, D>,
434462
addListenerAction: addListenerAction as TypedAddListenerAction<S>,
435-
removeListenerAction: removeListenerAction as TypedRemoveListenerAction<S>,
463+
removeListenerAction:
464+
removeListenerAction as TypedRemoveListenerAction<S>,
465+
clear: clearListenerMiddleware,
436466
},
437467
{} as WithMiddlewareType<typeof middleware>
438468
)

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

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
addListenerAction,
1515
removeListenerAction,
1616
TaskAbortError,
17+
clearListenerMiddlewareAction,
1718
} from '../index'
1819

1920
import type {
@@ -23,10 +24,7 @@ import type {
2324
Unsubscribe,
2425
ActionListenerMiddleware,
2526
} from '../index'
26-
import type {
27-
ActionListener,
28-
AddListenerOverloads,
29-
} from '../types'
27+
import type { ActionListener, AddListenerOverloads } from '../types'
3028

3129
const middlewareApi = {
3230
getState: expect.any(Function),
@@ -428,7 +426,11 @@ describe('createActionListenerMiddleware', () => {
428426
const addListenerOptions: [
429427
string,
430428
Omit<
431-
AddListenerOverloads<() => void, typeof store.getState, typeof store.dispatch>,
429+
AddListenerOverloads<
430+
() => void,
431+
typeof store.getState,
432+
typeof store.dispatch
433+
>,
432434
'listener'
433435
>
434436
][] = [
@@ -540,6 +542,109 @@ describe('createActionListenerMiddleware', () => {
540542
})
541543
})
542544

545+
describe('clear listeners', () => {
546+
test('dispatch(clearListenerAction()) cancels running listeners and removes all subscriptions', async () => {
547+
const listener1Test = deferred()
548+
let listener1Calls = 0
549+
let listener2Calls = 0
550+
let listener3Calls = 0
551+
552+
middleware.addListener({
553+
actionCreator: testAction1,
554+
async listener(_, listenerApi) {
555+
listener1Calls++
556+
listenerApi.signal.addEventListener(
557+
'abort',
558+
() => listener1Test.resolve(listener1Calls),
559+
{ once: true }
560+
)
561+
await listenerApi.condition(() => true)
562+
listener1Test.reject(new Error('unreachable: listener1Test'))
563+
},
564+
})
565+
566+
middleware.addListener({
567+
actionCreator: clearListenerMiddlewareAction,
568+
listener() {
569+
listener2Calls++
570+
},
571+
})
572+
573+
middleware.addListener({
574+
predicate: () => true,
575+
listener() {
576+
listener3Calls++
577+
},
578+
})
579+
580+
store.dispatch(testAction1('a'))
581+
store.dispatch(clearListenerMiddlewareAction())
582+
store.dispatch(testAction1('b'))
583+
expect(await listener1Test).toBe(1)
584+
expect(listener1Calls).toBe(1)
585+
expect(listener3Calls).toBe(1)
586+
expect(listener2Calls).toBe(0)
587+
})
588+
589+
test('clear() cancels running listeners and removes all subscriptions', async () => {
590+
const listener1Test = deferred()
591+
592+
let listener1Calls = 0
593+
let listener2Calls = 0
594+
595+
middleware.addListener({
596+
actionCreator: testAction1,
597+
async listener(_, listenerApi) {
598+
listener1Calls++
599+
listenerApi.signal.addEventListener(
600+
'abort',
601+
() => listener1Test.resolve(listener1Calls),
602+
{ once: true }
603+
)
604+
await listenerApi.condition(() => true)
605+
listener1Test.reject(new Error('unreachable: listener1Test'))
606+
},
607+
})
608+
609+
middleware.addListener({
610+
actionCreator: testAction2,
611+
listener() {
612+
listener2Calls++
613+
},
614+
})
615+
616+
store.dispatch(testAction1('a'))
617+
618+
middleware.clear()
619+
store.dispatch(testAction1('b'))
620+
store.dispatch(testAction2('c'))
621+
622+
expect(listener2Calls).toBe(0)
623+
expect(await listener1Test).toBe(1)
624+
})
625+
626+
test('clear() cancels all running forked tasks', async () => {
627+
const fork1Test = deferred()
628+
629+
middleware.addListener({
630+
actionCreator: testAction1,
631+
async listener(_, { fork }) {
632+
const taskResult = await fork(() => {
633+
return 3
634+
}).result
635+
fork1Test.resolve(taskResult)
636+
},
637+
})
638+
639+
store.dispatch(testAction1('a'))
640+
641+
middleware.clear()
642+
store.dispatch(testAction1('b'))
643+
644+
expect(await fork1Test).toHaveProperty('status', 'cancelled')
645+
})
646+
})
647+
543648
describe('Listener API', () => {
544649
test('Passes both getState and getOriginalState in the API', () => {
545650
const store = configureStore({

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ export type ActionListenerMiddleware<
219219
removeListener: RemoveListenerOverloads<S, D>
220220
addListenerAction: TypedAddListenerAction<S, D>
221221
removeListenerAction: TypedRemoveListenerAction<S, D>
222+
/**
223+
* Unsubscribes all listeners, cancels running listeners and tasks.
224+
*/
225+
clear: () => void
222226
}
223227

224228
/**

0 commit comments

Comments
 (0)