Skip to content

Commit

Permalink
Merge pull request reduxjs#1787 from reduxjs/feature/reorganize-middl…
Browse files Browse the repository at this point in the history
…eware
  • Loading branch information
markerikson authored Dec 1, 2021
2 parents 1ab9c2b + 2c60a3e commit 09939c1
Show file tree
Hide file tree
Showing 3 changed files with 365 additions and 292 deletions.
325 changes: 36 additions & 289 deletions packages/action-listener-middleware/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,47 @@
import type {
PayloadAction,
Middleware,
Dispatch,
AnyAction,
MiddlewareAPI,
Action,
ThunkDispatch,
} from '@reduxjs/toolkit'
import { createAction, nanoid } from '@reduxjs/toolkit'

interface BaseActionCreator<P, T extends string, M = never, E = never> {
type: T
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
}

interface TypedActionCreator<Type extends string> {
(...args: any[]): Action<Type>
type: Type
match: MatchFunction<any>
}

type AnyActionListenerPredicate<State> = (
action: AnyAction,
currentState: State,
originalState: State
) => boolean

type ListenerPredicate<Action extends AnyAction, State> = (
action: AnyAction,
currentState: State,
originalState: State
) => action is Action

interface ConditionFunction<State> {
(
predicate: AnyActionListenerPredicate<State>,
timeout?: number
): Promise<boolean>
(
predicate: AnyActionListenerPredicate<State>,
timeout?: number
): Promise<boolean>
(predicate: () => boolean, timeout?: number): Promise<boolean>
}

type MatchFunction<T> = (v: any) => v is T

type TakePatternOutputWithoutTimeout<
State,
Predicate extends AnyActionListenerPredicate<State>
> = Predicate extends MatchFunction<infer Action>
? Promise<[Action, State, State]>
: Promise<[AnyAction, State, State]>

type TakePatternOutputWithTimeout<
State,
Predicate extends AnyActionListenerPredicate<State>
> = Predicate extends MatchFunction<infer Action>
? Promise<[Action, State, State] | null>
: Promise<[AnyAction, State, State] | null>

interface TakePattern<State> {
<Predicate extends AnyActionListenerPredicate<State>>(
predicate: Predicate
): TakePatternOutputWithoutTimeout<State, Predicate>
<Predicate extends AnyActionListenerPredicate<State>>(
predicate: Predicate,
timeout: number
): TakePatternOutputWithTimeout<State, Predicate>;
<Predicate extends AnyActionListenerPredicate<State>>(
predicate: Predicate,
timeout?: number | undefined
): Promise<[AnyAction, State, State] | null>;
}

export interface HasMatchFunction<T> {
match: MatchFunction<T>
}
import type {
ActionListener,
AddListenerOverloads,
BaseActionCreator,
AnyActionListenerPredicate,
CreateListenerMiddlewareOptions,
ConditionFunction,
ListenerPredicate,
TypedActionCreator,
TypedAddListener,
TypedAddListenerAction,
TypedCreateListenerEntry,
RemoveListenerAction,
FallbackAddListenerOptions,
ListenerEntry,
ListenerErrorHandler,
Unsubscribe,
MiddlewarePhase,
WithMiddlewareType,
TakePattern,
} from './types'

export type {
ActionListener,
ActionListenerMiddleware,
ActionListenerMiddlewareAPI,
ActionListenerOptions,
CreateListenerMiddlewareOptions,
MiddlewarePhase,
When,
ListenerErrorHandler,
TypedAddListener,
TypedAddListenerAction,
Unsubscribe,
} from './types'

function assertFunction(
func: unknown,
Expand All @@ -87,196 +52,11 @@ function assertFunction(
}
}

type Unsubscribe = () => void

type GuardedType<T> = T extends (x: any, ...args: unknown[]) => x is infer T
? T
: never

type ListenerPredicateGuardedActionType<T> = T extends ListenerPredicate<
infer Action,
any
>
? Action
: never

const declaredMiddlewareType: unique symbol = undefined as any
export type WithMiddlewareType<T extends Middleware<any, any, any>> = {
[declaredMiddlewareType]: T
}

export type MiddlewarePhase = 'beforeReducer' | 'afterReducer'

const defaultWhen: MiddlewarePhase = 'afterReducer'
const actualMiddlewarePhases = ['beforeReducer', 'afterReducer'] as const

export type When = MiddlewarePhase | 'both' | undefined

/**
* @alpha
*/
export interface ActionListenerMiddlewareAPI<S, D extends Dispatch<AnyAction>>
extends MiddlewareAPI<D, S> {
getOriginalState: () => S
unsubscribe(): void
subscribe(): void
condition: ConditionFunction<S>
take: TakePattern<S>
currentPhase: MiddlewarePhase
// TODO Figure out how to pass this through the other types correctly
extra: unknown
}

/**
* @alpha
*/
export type ActionListener<
A extends AnyAction,
S,
D extends Dispatch<AnyAction>
> = (action: A, api: ActionListenerMiddlewareAPI<S, D>) => void

export interface ListenerErrorHandler {
(error: unknown): void
}

export interface ActionListenerOptions {
/**
* Determines if the listener runs 'before' or 'after' the reducers have been called.
* If set to 'before', calling `api.stopPropagation()` from the listener becomes possible.
* Defaults to 'before'.
*/
when?: When
}

export interface CreateListenerMiddlewareOptions<ExtraArgument = unknown> {
extra?: ExtraArgument
/**
* Receives synchronous errors that are raised by `listener` and `listenerOption.predicate`.
*/
onError?: ListenerErrorHandler
}

/**
* The possible overloads and options for defining a listener. The return type of each function is specified as a generic arg, so the overloads can be reused for multiple different functions
*/
interface AddListenerOverloads<
Return,
S = unknown,
D extends Dispatch = ThunkDispatch<S, unknown, AnyAction>
> {
/** Accepts a "listener predicate" that is also a TS type predicate for the action*/
<MA extends AnyAction, LP extends ListenerPredicate<MA, S>>(
options: {
actionCreator?: never
type?: never
matcher?: never
predicate: LP
listener: ActionListener<ListenerPredicateGuardedActionType<LP>, S, D>
} & ActionListenerOptions
): Return

/** Accepts an RTK action creator, like `incrementByAmount` */
<C extends TypedActionCreator<any>>(
options: {
actionCreator: C
type?: never
matcher?: never
predicate?: never
listener: ActionListener<ReturnType<C>, S, D>
} & ActionListenerOptions
): Return

/** Accepts a specific action type string */
<T extends string>(
options: {
actionCreator?: never
type: T
matcher?: never
predicate?: never
listener: ActionListener<Action<T>, S, D>
} & ActionListenerOptions
): Return

/** Accepts an RTK matcher function, such as `incrementByAmount.match` */
<MA extends AnyAction, M extends MatchFunction<MA>>(
options: {
actionCreator?: never
type?: never
matcher: M
predicate?: never
listener: ActionListener<GuardedType<M>, S, D>
} & ActionListenerOptions
): Return

/** Accepts a "listener predicate" that just returns a boolean, no type assertion */
<LP extends AnyActionListenerPredicate<S>>(
options: {
actionCreator?: never
type?: never
matcher?: never
predicate: LP
listener: ActionListener<AnyAction, S, D>
} & ActionListenerOptions
): Return
}

interface RemoveListenerOverloads<
S = unknown,
D extends Dispatch = ThunkDispatch<S, unknown, AnyAction>
> {
<C extends TypedActionCreator<any>>(
actionCreator: C,
listener: ActionListener<ReturnType<C>, S, D>
): boolean
(type: string, listener: ActionListener<AnyAction, S, D>): boolean
}

/** A "pre-typed" version of `addListenerAction`, so the listener args are well-typed */
export type TypedAddListenerAction<
S,
D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>,
Payload = ListenerEntry<S, D>,
T extends string = 'actionListenerMiddleware/add'
> = BaseActionCreator<Payload, T> &
AddListenerOverloads<PayloadAction<Payload, T>, S, D>

/** A "pre-typed" version of `middleware.addListener`, so the listener args are well-typed */
export type TypedAddListener<
S,
D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>
> = AddListenerOverloads<Unsubscribe, S, D>

/** @internal An single listener entry */
type ListenerEntry<
S = unknown,
D extends Dispatch<AnyAction> = Dispatch<AnyAction>
> = {
id: string
when: When
listener: ActionListener<any, S, D>
unsubscribe: () => void
type?: string
predicate: ListenerPredicate<AnyAction, S>
}

/** A "pre-typed" version of `createListenerEntry`, so the listener args are well-typed */
export type TypedCreateListenerEntry<
S,
D extends Dispatch<AnyAction> = ThunkDispatch<S, unknown, AnyAction>
> = AddListenerOverloads<ListenerEntry<S, D>, S, D>

// A shorthand form of the accepted args, solely so that `createListenerEntry` has validly-typed conditional logic when checking the options contents
type FallbackAddListenerOptions = (
| { actionCreator: TypedActionCreator<string> }
| { type: string }
| { matcher: MatchFunction<any> }
| { predicate: ListenerPredicate<any, any> }
) &
ActionListenerOptions & { listener: ActionListener<any, any, any> }

function createTakePattern<S>(
addListener: AddListenerOverloads<Unsubscribe, S,Dispatch<AnyAction>>
addListener: AddListenerOverloads<Unsubscribe, S, Dispatch<AnyAction>>
): TakePattern<S> {
async function take<P extends AnyActionListenerPredicate<S>>(
predicate: P,
Expand Down Expand Up @@ -352,27 +132,6 @@ export const createListenerEntry: TypedCreateListenerEntry<unknown> = (
return entry
}

export type ActionListenerMiddleware<
S = unknown,
// TODO Carry through the thunk extra arg somehow?
D extends ThunkDispatch<S, unknown, AnyAction> = ThunkDispatch<
S,
unknown,
AnyAction
>,
ExtraArgument = unknown
> = Middleware<
{
(action: Action<'actionListenerMiddleware/add'>): Unsubscribe
},
S,
D
> & {
addListener: AddListenerOverloads<Unsubscribe, S, D>
removeListener: RemoveListenerOverloads<S, D>
addListenerAction: TypedAddListenerAction<S, D>
}

/**
* Safely reports errors to the `errorHandler` provided.
* Errors that occur inside `errorHandler` are notified in a new task.
Expand Down Expand Up @@ -412,18 +171,6 @@ export const addListenerAction = createAction(
}
) as TypedAddListenerAction<unknown>

interface RemoveListenerAction<
A extends AnyAction,
S,
D extends Dispatch<AnyAction>
> {
type: 'actionListenerMiddleware/remove'
payload: {
type: string
listener: ActionListener<A, S, D>
}
}

/**
* @alpha
*/
Expand Down
Loading

0 comments on commit 09939c1

Please sign in to comment.