Skip to content

Commit 41f70e8

Browse files
committed
Rename listener middleware public APIs and rework return value
This commit makes major breaking changes to the listener middleware. First, we've renamed all the public methods and action creators, so that we have a consistent naming scheme without clashes: - API: `createListenerMiddleware` - Returns: `{middleware, startListening, stopListening, clearListeners}` - Docs examples show the object as `listenerMiddleware` - One entry: "listener" - Field in that listener entry: `effect` - Action creators: `addListener`, `removeListener`, `removeAllListeners` Next, `createListenerMiddleware` now returns an object containing `{middleware, start/stop/clear}`, rather than the middleware itself with those methods attached. This works around the TS inference issues from attaching the add and remove functions directly to the middleware function itself, and allows `const unsub = dispatch(addListener())` to have correct TS types for the return value.
1 parent faf5438 commit 41f70e8

File tree

7 files changed

+579
-556
lines changed

7 files changed

+579
-556
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: 97 additions & 105 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)
@@ -300,9 +297,9 @@ export function createListenerMiddleware<
300297
return undefined
301298
}
302299

303-
const addListener = (options: FallbackAddListenerOptions) => {
300+
const startListening = (options: FallbackAddListenerOptions) => {
304301
let entry = findListenerEntry(
305-
(existingEntry) => existingEntry.listener === options.listener
302+
(existingEntry) => existingEntry.effect === options.effect
306303
)
307304

308305
if (!entry) {
@@ -312,16 +309,16 @@ export function createListenerMiddleware<
312309
return insertEntry(entry)
313310
}
314311

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

318315
const entry = findListenerEntry((entry) => {
319316
const matchPredicateOrType =
320317
typeof type === 'string'
321318
? entry.type === type
322319
: entry.predicate === predicate
323320

324-
return matchPredicateOrType && entry.listener === listener
321+
return matchPredicateOrType && entry.effect === effect
325322
})
326323

327324
entry?.unsubscribe()
@@ -336,18 +333,21 @@ export function createListenerMiddleware<
336333
getOriginalState: () => S
337334
) => {
338335
const internalTaskController = new AbortController()
339-
const take = createTakePattern(addListener, internalTaskController.signal)
336+
const take = createTakePattern(
337+
startListening,
338+
internalTaskController.signal
339+
)
340340

341341
try {
342342
entry.pending.add(internalTaskController)
343343
await Promise.resolve(
344-
entry.listener(
344+
entry.effect(
345345
action,
346346
// Use assign() rather than ... to avoid extra helper functions added to bundle
347347
assign({}, api, {
348348
getOriginalState,
349349
condition: (
350-
predicate: AnyActionListenerPredicate<any>,
350+
predicate: AnyListenerPredicate<any>,
351351
timeout?: number
352352
) => take(predicate, timeout).then(Boolean),
353353
take,
@@ -374,7 +374,7 @@ export function createListenerMiddleware<
374374
} catch (listenerError) {
375375
if (!(listenerError instanceof TaskAbortError)) {
376376
safelyNotifyError(onError, listenerError, {
377-
raisedBy: 'listener',
377+
raisedBy: 'effect',
378378
})
379379
}
380380
} finally {
@@ -385,84 +385,76 @@ export function createListenerMiddleware<
385385

386386
const clearListenerMiddleware = createClearListenerMiddleware(listenerMap)
387387

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

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

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

408-
// Need to get this state _before_ the reducer processes the action
409-
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()
410405

411-
// `getOriginalState` can only be called synchronously.
412-
// @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820
413-
const getOriginalState = (): S => {
414-
if (originalState === INTERNAL_NIL_TOKEN) {
415-
throw new Error(
416-
`${alm}: getOriginalState can only be called synchronously`
417-
)
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
418416
}
419417

420-
return originalState as S
421-
}
418+
let result: unknown
422419

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

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

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

448-
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+
}
449445
}
446+
} finally {
447+
// Remove `originalState` store from this scope.
448+
originalState = INTERNAL_NIL_TOKEN
450449
}
451-
} finally {
452-
// Remove `originalState` store from this scope.
453-
originalState = INTERNAL_NIL_TOKEN
454-
}
455450

456-
return result
457-
}
451+
return result
452+
}
458453

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

0 commit comments

Comments
 (0)