UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

176 lines (151 loc) 5.86 kB
import { configureStore, createAction, createSlice, isAnyOf, } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' import { createListenerMiddleware } from '../index' import type { TypedAddListener } from '../index' import { TaskAbortError } from '../exceptions' interface CounterState { value: number } const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 } as CounterState, reducers: { increment(state) { state.value += 1 }, decrement(state) { state.value -= 1 }, // Use the PayloadAction type to declare the contents of `action.payload` incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, }, }) const { increment, decrement, incrementByAmount } = counterSlice.actions describe('Saga-style Effects Scenarios', () => { let listenerMiddleware = createListenerMiddleware<CounterState>() let { middleware, startListening, stopListening } = listenerMiddleware let store = configureStore({ reducer: counterSlice.reducer, middleware: (gDM) => gDM().prepend(middleware), }) const testAction1 = createAction<string>('testAction1') type TestAction1 = ReturnType<typeof testAction1> const testAction2 = createAction<string>('testAction2') type TestAction2 = ReturnType<typeof testAction2> const testAction3 = createAction<string>('testAction3') type TestAction3 = ReturnType<typeof testAction3> type RootState = ReturnType<typeof store.getState> function delay(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } beforeEach(() => { listenerMiddleware = createListenerMiddleware<CounterState>() middleware = listenerMiddleware.middleware startListening = listenerMiddleware.startListening store = configureStore({ reducer: counterSlice.reducer, middleware: (gDM) => gDM().prepend(middleware), }) }) test('Long polling loop', async () => { // Reimplementation of a saga-based long-polling loop that is controlled // by "start/stop" actions. The infinite loop waits for a message from the // server, processes it somehow, and waits for the next message. // Ref: https://gist.github.com/markerikson/5203e71a69fa9dff203c9e27c3d84154 const eventPollingStarted = createAction('serverPolling/started') const eventPollingStopped = createAction('serverPolling/stopped') // For this example, we're going to fake up a "server event poll" async // function by wrapping an event emitter so that every call returns a // promise that is resolved the next time an event is emitted. // This is the tiniest event emitter I could find to copy-paste in here. let createNanoEvents = () => ({ events: {} as Record<string, any>, emit(event: string, ...args: any[]) { ;(this.events[event] || []).forEach((i: any) => i(...args)) }, on(event: string, cb: (...args: any[]) => void) { ;(this.events[event] = this.events[event] || []).push(cb) return () => (this.events[event] = (this.events[event] || []).filter( (l: any) => l !== cb, )) }, }) const emitter = createNanoEvents() // Rig up a dummy "receive a message from the server" API we can trigger manually function pollForEvent() { return new Promise<{ type: string }>((resolve, reject) => { const unsubscribe = emitter.on('serverEvent', (arg1: string) => { unsubscribe() resolve({ type: arg1 }) }) }) } // Track how many times each message was processed by the loop const receivedMessages = { a: 0, b: 0, c: 0, } let pollingTaskStarted = false let pollingTaskCanceled = false startListening({ actionCreator: eventPollingStarted, effect: async (action, listenerApi) => { listenerApi.unsubscribe() // Start a child job that will infinitely loop receiving messages const pollingTask = listenerApi.fork(async (forkApi) => { pollingTaskStarted = true try { while (true) { // Cancelation-aware pause for a new server message const serverEvent = await forkApi.pause(pollForEvent()) // Process the message. In this case, just count the times we've seen this message. if (serverEvent.type in receivedMessages) { receivedMessages[ serverEvent.type as keyof typeof receivedMessages ]++ } } } catch (err) { if (err instanceof TaskAbortError) { pollingTaskCanceled = true } } return 0 }) // Wait for the "stop polling" action await listenerApi.condition(eventPollingStopped.match) pollingTask.cancel() }, }) store.dispatch(eventPollingStarted()) await delay(5) expect(pollingTaskStarted).toBe(true) await delay(5) emitter.emit('serverEvent', 'a') // Promise resolution await delay(1) emitter.emit('serverEvent', 'b') // Promise resolution await delay(1) store.dispatch(eventPollingStopped()) // Have to break out of the event loop to let the cancelation promise // kick in - emitting before this would still resolve pollForEvent() await delay(1) emitter.emit('serverEvent', 'c') // A and B were processed earlier. The first C was processed because the // emitter synchronously resolved the `pollForEvents` promise before // the cancelation took effect, but after another pause, the // cancelation kicked in and the second C is ignored. expect(receivedMessages).toEqual({ a: 1, b: 1, c: 0 }) expect(pollingTaskCanceled).toBe(true) }) })