Skip to content

Commit 5c398cd

Browse files
authored
Merge pull request #2005 from reduxjs/feature/listener-signature-changes
2 parents 793c5d5 + 41f70e8 commit 5c398cd

File tree

7 files changed

+608
-565
lines changed

7 files changed

+608
-565
lines changed

packages/action-listener-middleware/README.md

Lines changed: 128 additions & 87 deletions
Large diffs are not rendered by default.

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

Lines changed: 98 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@ import type {
99
import { createAction, nanoid } from '@reduxjs/toolkit'
1010

1111
import type {
12-
ActionListenerMiddleware,
12+
ListenerMiddleware,
13+
ListenerMiddlewareInstance,
1314
AddListenerOverloads,
14-
AnyActionListenerPredicate,
15+
AnyListenerPredicate,
1516
CreateListenerMiddlewareOptions,
1617
TypedActionCreator,
18+
TypedStartListening,
1719
TypedAddListener,
18-
TypedAddListenerAction,
1920
TypedCreateListenerEntry,
2021
FallbackAddListenerOptions,
2122
ListenerEntry,
2223
ListenerErrorHandler,
2324
Unsubscribe,
24-
WithMiddlewareType,
2525
TakePattern,
2626
ListenerErrorInfo,
2727
ForkedTaskExecutor,
2828
ForkedTask,
29-
TypedRemoveListenerAction,
3029
TypedRemoveListener,
30+
TypedStopListening,
3131
} from './types'
3232
import { assertFunction, catchRejection } from './utils'
3333
import { TaskAbortError } from './exceptions'
@@ -40,16 +40,15 @@ import {
4040
} from './task'
4141
export { TaskAbortError } from './exceptions'
4242
export type {
43-
ActionListener,
44-
ActionListenerMiddleware,
45-
ActionListenerMiddlewareAPI,
46-
ActionListenerOptions,
43+
ListenerEffect,
44+
ListenerMiddleware,
45+
ListenerEffectAPI,
4746
CreateListenerMiddlewareOptions,
4847
ListenerErrorHandler,
48+
TypedStartListening,
4949
TypedAddListener,
50-
TypedAddListenerAction,
50+
TypedStopListening,
5151
TypedRemoveListener,
52-
TypedRemoveListenerAction,
5352
Unsubscribe,
5453
ForkedTaskExecutor,
5554
ForkedTask,
@@ -69,7 +68,7 @@ const { assign } = Object
6968
*/
7069
const INTERNAL_NIL_TOKEN = {} as const
7170

72-
const alm = 'actionListenerMiddleware' as const
71+
const alm = 'listenerMiddleware' as const
7372

7473
const createFork = (parentAbortSignal: AbortSignal) => {
7574
return <T>(taskExecutor: ForkedTaskExecutor<T>): ForkedTask<T> => {
@@ -100,17 +99,17 @@ const createFork = (parentAbortSignal: AbortSignal) => {
10099
}
101100

102101
const createTakePattern = <S>(
103-
addListener: AddListenerOverloads<Unsubscribe, S, Dispatch<AnyAction>>,
102+
startListening: AddListenerOverloads<Unsubscribe, S, Dispatch<AnyAction>>,
104103
signal: AbortSignal
105104
): TakePattern<S> => {
106105
/**
107-
* A function that takes an ActionListenerPredicate and an optional timeout,
106+
* A function that takes a ListenerPredicate and an optional timeout,
108107
* and resolves when either the predicate returns `true` based on an action
109108
* state combination or when the timeout expires.
110109
* If the parent listener is canceled while waiting, this will throw a
111110
* TaskAbortError.
112111
*/
113-
const take = async <P extends AnyActionListenerPredicate<S>>(
112+
const take = async <P extends AnyListenerPredicate<S>>(
114113
predicate: P,
115114
timeout: number | undefined
116115
) => {
@@ -121,9 +120,9 @@ const createTakePattern = <S>(
121120

122121
const tuplePromise = new Promise<[AnyAction, S, S]>((resolve) => {
123122
// Inside the Promise, we synchronously add the listener.
124-
unsubscribe = addListener({
123+
unsubscribe = startListening({
125124
predicate: predicate as any,
126-
listener: (action, listenerApi): void => {
125+
effect: (action, listenerApi): void => {
127126
// One-shot listener that cleans up as soon as the predicate passes
128127
listenerApi.unsubscribe()
129128
// Resolve the promise with the same arguments the predicate saw
@@ -158,14 +157,12 @@ const createTakePattern = <S>(
158157
}
159158
}
160159

161-
return ((
162-
predicate: AnyActionListenerPredicate<S>,
163-
timeout: number | undefined
164-
) => catchRejection(take(predicate, timeout))) as TakePattern<S>
160+
return ((predicate: AnyListenerPredicate<S>, timeout: number | undefined) =>
161+
catchRejection(take(predicate, timeout))) as TakePattern<S>
165162
}
166163

167164
const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => {
168-
let { type, actionCreator, matcher, predicate, listener } = options
165+
let { type, actionCreator, matcher, predicate, effect } = options
169166

170167
if (type) {
171168
predicate = createAction(type).match
@@ -182,21 +179,21 @@ const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => {
182179
)
183180
}
184181

185-
assertFunction(listener, 'options.listener')
182+
assertFunction(effect, 'options.listener')
186183

187-
return { predicate, type, listener }
184+
return { predicate, type, effect }
188185
}
189186

190187
/** Accepts the possible options for creating a listener, and returns a formatted listener entry */
191188
export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
192189
options: FallbackAddListenerOptions
193190
) => {
194-
const { type, predicate, listener } = getListenerEntryPropsFrom(options)
191+
const { type, predicate, effect } = getListenerEntryPropsFrom(options)
195192

196193
const id = nanoid()
197194
const entry: ListenerEntry<unknown> = {
198195
id,
199-
listener,
196+
effect,
200197
type,
201198
predicate,
202199
pending: new Set<AbortController>(),
@@ -248,21 +245,21 @@ const safelyNotifyError = (
248245
/**
249246
* @alpha
250247
*/
251-
export const addListenerAction = createAction(
248+
export const addListener = createAction(
252249
`${alm}/add`
253-
) as TypedAddListenerAction<unknown>
250+
) as TypedAddListener<unknown>
254251

255252
/**
256253
* @alpha
257254
*/
258-
export const clearListenerMiddlewareAction = createAction(`${alm}/clear`)
255+
export const removeAllListeners = createAction(`${alm}/removeAll`)
259256

260257
/**
261258
* @alpha
262259
*/
263-
export const removeListenerAction = createAction(
260+
export const removeListener = createAction(
264261
`${alm}/remove`
265-
) as TypedRemoveListenerAction<unknown>
262+
) as TypedRemoveListener<unknown>
266263

267264
const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
268265
console.error(`${alm}/error`, ...args)
@@ -271,9 +268,8 @@ const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => {
271268
/**
272269
* @alpha
273270
*/
274-
export function createActionListenerMiddleware<
271+
export function createListenerMiddleware<
275272
S = unknown,
276-
// TODO Carry through the thunk extra arg somehow?
277273
D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>,
278274
ExtraArgument = unknown
279275
>(middlewareOptions: CreateListenerMiddlewareOptions<ExtraArgument> = {}) {
@@ -301,9 +297,9 @@ export function createActionListenerMiddleware<
301297
return undefined
302298
}
303299

304-
const addListener = (options: FallbackAddListenerOptions) => {
300+
const startListening = (options: FallbackAddListenerOptions) => {
305301
let entry = findListenerEntry(
306-
(existingEntry) => existingEntry.listener === options.listener
302+
(existingEntry) => existingEntry.effect === options.effect
307303
)
308304

309305
if (!entry) {
@@ -313,16 +309,16 @@ export function createActionListenerMiddleware<
313309
return insertEntry(entry)
314310
}
315311

316-
const removeListener = (options: FallbackAddListenerOptions): boolean => {
317-
const { type, listener, predicate } = getListenerEntryPropsFrom(options)
312+
const stopListening = (options: FallbackAddListenerOptions): boolean => {
313+
const { type, effect, predicate } = getListenerEntryPropsFrom(options)
318314

319315
const entry = findListenerEntry((entry) => {
320316
const matchPredicateOrType =
321317
typeof type === 'string'
322318
? entry.type === type
323319
: entry.predicate === predicate
324320

325-
return matchPredicateOrType && entry.listener === listener
321+
return matchPredicateOrType && entry.effect === effect
326322
})
327323

328324
entry?.unsubscribe()
@@ -337,18 +333,21 @@ export function createActionListenerMiddleware<
337333
getOriginalState: () => S
338334
) => {
339335
const internalTaskController = new AbortController()
340-
const take = createTakePattern(addListener, internalTaskController.signal)
336+
const take = createTakePattern(
337+
startListening,
338+
internalTaskController.signal
339+
)
341340

342341
try {
343342
entry.pending.add(internalTaskController)
344343
await Promise.resolve(
345-
entry.listener(
344+
entry.effect(
346345
action,
347346
// Use assign() rather than ... to avoid extra helper functions added to bundle
348347
assign({}, api, {
349348
getOriginalState,
350349
condition: (
351-
predicate: AnyActionListenerPredicate<any>,
350+
predicate: AnyListenerPredicate<any>,
352351
timeout?: number
353352
) => take(predicate, timeout).then(Boolean),
354353
take,
@@ -375,7 +374,7 @@ export function createActionListenerMiddleware<
375374
} catch (listenerError) {
376375
if (!(listenerError instanceof TaskAbortError)) {
377376
safelyNotifyError(onError, listenerError, {
378-
raisedBy: 'listener',
377+
raisedBy: 'effect',
379378
})
380379
}
381380
} finally {
@@ -386,84 +385,76 @@ export function createActionListenerMiddleware<
386385

387386
const clearListenerMiddleware = createClearListenerMiddleware(listenerMap)
388387

389-
const middleware: Middleware<
390-
{
391-
(action: Action<`${typeof alm}/add`>): Unsubscribe
392-
},
393-
S,
394-
D
395-
> = (api) => (next) => (action) => {
396-
if (addListenerAction.match(action)) {
397-
return addListener(action.payload)
398-
}
388+
const middleware: ListenerMiddleware<S, D, ExtraArgument> =
389+
(api) => (next) => (action) => {
390+
if (addListener.match(action)) {
391+
return startListening(action.payload)
392+
}
399393

400-
if (clearListenerMiddlewareAction.match(action)) {
401-
clearListenerMiddleware()
402-
return
403-
}
394+
if (removeAllListeners.match(action)) {
395+
clearListenerMiddleware()
396+
return
397+
}
404398

405-
if (removeListenerAction.match(action)) {
406-
return removeListener(action.payload)
407-
}
399+
if (removeListener.match(action)) {
400+
return stopListening(action.payload)
401+
}
408402

409-
// Need to get this state _before_ the reducer processes the action
410-
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
403+
// Need to get this state _before_ the reducer processes the action
404+
let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState()
411405

412-
// `getOriginalState` can only be called synchronously.
413-
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
414-
const getOriginalState = (): S => {
415-
if (originalState === INTERNAL_NIL_TOKEN) {
416-
throw new Error(
417-
`${alm}: getOriginalState can only be called synchronously`
418-
)
406+
// `getOriginalState` can only be called synchronously.
407+
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
408+
const getOriginalState = (): S => {
409+
if (originalState === INTERNAL_NIL_TOKEN) {
410+
throw new Error(
411+
`${alm}: getOriginalState can only be called synchronously`
412+
)
413+
}
414+
415+
return originalState as S
419416
}
420417

421-
return originalState as S
422-
}
418+
let result: unknown
423419

424-
let result: unknown
420+
try {
421+
// Actually forward the action to the reducer before we handle listeners
422+
result = next(action)
425423

426-
try {
427-
// Actually forward the action to the reducer before we handle listeners
428-
result = next(action)
429-
430-
if (listenerMap.size > 0) {
431-
let currentState = api.getState()
432-
for (let entry of listenerMap.values()) {
433-
let runListener = false
434-
435-
try {
436-
runListener = entry.predicate(action, currentState, originalState)
437-
} catch (predicateError) {
438-
runListener = false
439-
440-
safelyNotifyError(onError, predicateError, {
441-
raisedBy: 'predicate',
442-
})
443-
}
424+
if (listenerMap.size > 0) {
425+
let currentState = api.getState()
426+
for (let entry of listenerMap.values()) {
427+
let runListener = false
444428

445-
if (!runListener) {
446-
continue
447-
}
429+
try {
430+
runListener = entry.predicate(action, currentState, originalState)
431+
} catch (predicateError) {
432+
runListener = false
448433

449-
notifyListener(entry, action, api, getOriginalState)
434+
safelyNotifyError(onError, predicateError, {
435+
raisedBy: 'predicate',
436+
})
437+
}
438+
439+
if (!runListener) {
440+
continue
441+
}
442+
443+
notifyListener(entry, action, api, getOriginalState)
444+
}
450445
}
446+
} finally {
447+
// Remove `originalState` store from this scope.
448+
originalState = INTERNAL_NIL_TOKEN
451449
}
452-
} finally {
453-
// Remove `originalState` store from this scope.
454-
originalState = INTERNAL_NIL_TOKEN
455-
}
456450

457-
return result
458-
}
451+
return result
452+
}
459453

460-
return assign(
454+
return {
461455
middleware,
462-
{
463-
addListener: addListener as TypedAddListener<S, D>,
464-
removeListener: removeListener as TypedRemoveListener<S, D>,
465-
clearListeners: clearListenerMiddleware,
466-
},
467-
{} as WithMiddlewareType<typeof middleware>
468-
)
456+
startListening,
457+
stopListening,
458+
clearListeners: clearListenerMiddleware,
459+
} as ListenerMiddlewareInstance<S, D, ExtraArgument>
469460
}

0 commit comments

Comments
 (0)