Skip to content

Commit 5f97ff4

Browse files
committed
Refactor internals to track entries by ID and use matchers
1 parent f5539e3 commit 5f97ff4

File tree

1 file changed

+100
-126
lines changed
  • packages/action-listener-middleware/src

1 file changed

+100
-126
lines changed

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

Lines changed: 100 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
createAction,
3+
nanoid,
34
PayloadAction,
45
Middleware,
56
Dispatch,
@@ -16,8 +17,14 @@ interface BaseActionCreator<P, T extends string, M = never, E = never> {
1617
interface TypedActionCreator<Type extends string> {
1718
(...args: any[]): Action<Type>
1819
type: Type
20+
match: MatchFunction<any>
1921
}
2022

23+
type ListenerPredicate<Action extends AnyAction, State = unknown> = (
24+
action: Action,
25+
state?: State
26+
) => boolean
27+
2128
type MatchFunction<T> = (v: any) => v is T
2229

2330
export interface HasMatchFunction<T> {
@@ -192,16 +199,14 @@ export function createActionListenerMiddleware<
192199
D extends Dispatch<AnyAction> = Dispatch
193200
>() {
194201
type ListenerEntry = ActionListenerOptions & {
202+
id: string
195203
listener: ActionListener<any, S, D, any>
196204
unsubscribe: () => void
205+
type?: string
206+
predicate: ListenerPredicate<any>
197207
}
198208

199-
type ListenerEntryWithMatcher = ListenerEntry & {
200-
matcher: MatchFunction<any>
201-
}
202-
203-
const listenerMap: Record<string, Set<ListenerEntry> | undefined> = {}
204-
const matcherListeners = new Set<ListenerEntryWithMatcher>()
209+
const listenerMap = new Map<string, ListenerEntry>()
205210

206211
const middleware: Middleware<
207212
{
@@ -225,114 +230,59 @@ export function createActionListenerMiddleware<
225230

226231
return
227232
}
228-
// @ts-ignore
229-
const listeners = listenerMap[action.type]
230-
let matchedMatcherListeners: ListenerEntry[] = []
231-
232-
if (matcherListeners.size > 0) {
233-
matchedMatcherListeners = Array.from(matcherListeners).filter((entry) => {
234-
return entry.matcher(action)
235-
})
236-
}
237233

238-
if (listeners || matcherListeners.size > 0) {
239-
const allListeners = Array.from(listeners ?? []).concat(
240-
matchedMatcherListeners
241-
)
242-
const defaultWhen = 'after'
243-
let result: unknown
244-
for (const phase of ['before', 'after'] as const) {
245-
for (const entry of allListeners) {
246-
if (phase !== (entry.when || defaultWhen)) {
247-
continue
248-
}
249-
let stoppedPropagation = false
250-
let currentPhase = phase
251-
let synchronousListenerFinished = false
252-
entry.listener(action, {
253-
...api,
254-
stopPropagation() {
255-
if (currentPhase === 'before') {
256-
if (!synchronousListenerFinished) {
257-
stoppedPropagation = true
258-
} else {
259-
throw new Error(
260-
'stopPropagation can only be called synchronously'
261-
)
262-
}
234+
let stateBefore = api.getState()
235+
236+
const defaultWhen: When = 'after'
237+
let result: unknown
238+
for (const phase of ['before', 'after'] as const) {
239+
let stateNow = api.getState()
240+
for (let entry of listenerMap.values()) {
241+
if (
242+
(entry.when || defaultWhen) !== phase ||
243+
!entry.predicate(action, stateNow)
244+
) {
245+
continue
246+
}
247+
248+
let stoppedPropagation = false
249+
let currentPhase = phase
250+
let synchronousListenerFinished = false
251+
entry.listener(action, {
252+
...api,
253+
stopPropagation() {
254+
if (currentPhase === 'before') {
255+
if (!synchronousListenerFinished) {
256+
stoppedPropagation = true
263257
} else {
264258
throw new Error(
265-
'stopPropagation can only be called by action listeners with the `when` option set to "before"'
259+
'stopPropagation can only be called synchronously'
266260
)
267261
}
268-
},
269-
unsubscribe: entry.unsubscribe,
270-
})
271-
synchronousListenerFinished = true
272-
if (stoppedPropagation) {
273-
return action
274-
}
275-
}
276-
if (phase === 'before') {
277-
result = next(action)
278-
} else {
279-
return result
262+
} else {
263+
throw new Error(
264+
'stopPropagation can only be called by action listeners with the `when` option set to "before"'
265+
)
266+
}
267+
},
268+
unsubscribe: entry.unsubscribe,
269+
})
270+
synchronousListenerFinished = true
271+
if (stoppedPropagation) {
272+
return action
280273
}
281274
}
275+
if (phase === 'before') {
276+
result = next(action)
277+
} else {
278+
return result
279+
}
282280
}
283281
return next(action)
284282
}
285283

286284
type Unsubscribe = () => void
287285

288-
function addStringListener<T extends string, O extends ActionListenerOptions>(
289-
type: T,
290-
listener: ActionListener<Action<T>, S, D, O>,
291-
options?: O
292-
): Unsubscribe {
293-
const listeners = getListenerMap(type)
294-
let entry = findListenerEntry(listeners, listener)
295-
296-
if (!entry) {
297-
entry = {
298-
...options,
299-
listener,
300-
unsubscribe: () => listeners.delete(entry!),
301-
}
302-
303-
listeners.add(entry)
304-
}
305-
306-
return entry.unsubscribe
307-
}
308-
309-
function addMatcherListener<
310-
MA extends AnyAction,
311-
M extends MatchFunction<MA>,
312-
O extends ActionListenerOptions
313-
>(
314-
matcher: M,
315-
listener: ActionListener<MA, S, D, O>,
316-
options?: O
317-
): Unsubscribe {
318-
let entry = findListenerEntry(matcherListeners, listener) as
319-
| ListenerEntryWithMatcher
320-
| undefined
321-
322-
if (!entry) {
323-
entry = {
324-
...options,
325-
listener,
326-
matcher,
327-
unsubscribe: () => matcherListeners.delete(entry!),
328-
}
329-
330-
matcherListeners.add(entry)
331-
}
332-
333-
return entry.unsubscribe
334-
}
335-
336286
type GuardedType<T> = T extends (x: any) => x is infer T ? T : never
337287

338288
function addListener<
@@ -358,28 +308,55 @@ export function createActionListenerMiddleware<
358308
matcher: M,
359309
listener: ActionListener<GuardedType<M>, S, D, O>,
360310
options?: O
311+
): Unsubscribe // eslint-disable-next-line no-redeclare
312+
function addListener<
313+
MA extends AnyAction,
314+
M extends ListenerPredicate<MA>,
315+
O extends ActionListenerOptions
316+
>(
317+
matcher: M,
318+
listener: ActionListener<AnyAction, S, D, O>,
319+
options?: O
361320
): Unsubscribe
362321
// eslint-disable-next-line no-redeclare
363322
function addListener(
364323
typeOrActionCreator: string | TypedActionCreator<any>,
365324
listener: ActionListener<AnyAction, S, D, any>,
366325
options?: ActionListenerOptions
367326
): Unsubscribe {
368-
if (typeof typeOrActionCreator === 'string') {
369-
return addStringListener(typeOrActionCreator, listener, options)
370-
} else if (typeof typeOrActionCreator.type === 'string') {
371-
return addStringListener(typeOrActionCreator.type, listener, options)
372-
} else {
373-
const matcher = typeOrActionCreator as unknown as MatchFunction<any>
374-
return addMatcherListener(matcher, listener, options)
375-
}
376-
}
327+
let predicate: ListenerPredicate<any>
328+
let type: string | undefined
377329

378-
function getListenerMap(type: string) {
379-
if (!listenerMap[type]) {
380-
listenerMap[type] = new Set()
330+
let entry = findListenerEntry(
331+
(existingEntry) => existingEntry.listener === listener
332+
)
333+
334+
if (!entry) {
335+
if (typeof typeOrActionCreator === 'string') {
336+
type = typeOrActionCreator
337+
predicate = (action: any) => action.type === type
338+
} else if (typeof typeOrActionCreator.type === 'string') {
339+
type = typeOrActionCreator.type
340+
predicate = typeOrActionCreator.match
341+
} else {
342+
predicate = typeOrActionCreator as unknown as ListenerPredicate<any>
343+
}
344+
345+
const id = nanoid()
346+
const unsubscribe = () => listenerMap.delete(id)
347+
entry = {
348+
...options,
349+
id,
350+
listener,
351+
type,
352+
predicate,
353+
unsubscribe,
354+
}
355+
356+
listenerMap.set(id, entry)
381357
}
382-
return listenerMap[type]!
358+
359+
return entry.unsubscribe
383360
}
384361

385362
function removeListener<C extends TypedActionCreator<any>>(
@@ -401,31 +378,28 @@ export function createActionListenerMiddleware<
401378
? typeOrActionCreator
402379
: typeOrActionCreator.type
403380

404-
const listeners = listenerMap[type]
405-
406-
if (!listeners) {
407-
return false
408-
}
409-
410-
let entry = findListenerEntry(listeners, listener)
381+
let entry = findListenerEntry(
382+
(entry) => entry.type === type && entry.listener === listener
383+
)
411384

412385
if (!entry) {
413386
return false
414387
}
415388

416-
listeners.delete(entry)
389+
listenerMap.delete(entry.id)
417390
return true
418391
}
419392

420393
function findListenerEntry(
421-
entries: Set<ListenerEntry>,
422-
listener: Function
394+
comparator: (entry: ListenerEntry) => boolean
423395
): ListenerEntry | undefined {
424-
for (const entry of entries) {
425-
if (entry.listener === listener) {
396+
for (const entry of listenerMap.values()) {
397+
if (comparator(entry)) {
426398
return entry
427399
}
428400
}
401+
402+
return undefined
429403
}
430404

431405
return Object.assign(

0 commit comments

Comments
 (0)