1import logger from '../logger'
2import {LogFn} from '../logger/types'
3import * as RS from 'redux-saga'
4import * as Types from '@redux-saga/types'
5import * as Effects from 'redux-saga/effects'
6import {convertToError} from './errors'
7import * as ConfigGen from '../actions/config-gen'
8import {TypedState} from '../constants/reducer'
9import {TypedActions, TypedActionsMap} from '../actions/typed-actions-gen'
10import put from './typed-put'
11import isArray from 'lodash/isArray'
12
13type ActionType = keyof TypedActionsMap
14
15export class SagaLogger {
16  error: LogFn
17  warn: LogFn
18  info: LogFn
19  debug: LogFn
20  isTagged = false
21  constructor(actionType: ActionType, fcnTag: string) {
22    const prefix = `${fcnTag} [${actionType}]:`
23    this.debug = (...args) => logger.debug(prefix, ...args)
24    this.error = (...args) => logger.error(prefix, ...args)
25    this.info = (...args) => logger.info(prefix, ...args)
26    this.warn = (...args) => logger.warn(prefix, ...args)
27  }
28  // call this first in your saga if you want chainAction / chainGenerator to log
29  // before and after you run
30  tag = () => {
31    this.info('->')
32    this.isTagged = true
33  }
34}
35
36// Useful in safeTakeEveryPure when you have an array of effects you want to run in order
37function* sequentially(effects: Array<any>): Generator<any, Array<any>, any> {
38  const results: Array<unknown> = []
39  for (let i = 0; i < effects.length; i++) {
40    results.push(yield effects[i])
41  }
42  return results
43}
44
45export type MaybeAction = void | boolean | TypedActions | TypedActions[] | null
46
47type ActionTypes = keyof TypedActionsMap
48export type ChainActionReturn =
49  | void
50  | TypedActions
51  | null
52  | boolean
53  | Array<ChainActionReturn>
54  | Promise<ChainActionReturn>
55//
56// Get the values of an Array. i.e. ValuesOf<["FOO", "BAR"]> => "FOO" | "BAR"
57type ValuesOf<T extends any[]> = T[number]
58
59interface ChainAction2 {
60  <AT extends ActionTypes>(
61    actions: AT,
62    handler: (state: TypedState, action: TypedActionsMap[AT], logger: SagaLogger) => ChainActionReturn
63  ): Generator<void, void, void>
64
65  <AT extends ActionTypes[]>(
66    actions: AT,
67    handler: (
68      state: TypedState,
69      action: TypedActionsMap[ValuesOf<AT>],
70      logger: SagaLogger
71    ) => ChainActionReturn
72  ): Generator<void, void, void>
73}
74interface ChainAction {
75  <AT extends ActionTypes>(
76    actions: AT,
77    handler: (action: TypedActionsMap[AT], logger: SagaLogger) => ChainActionReturn
78  ): Generator<void, void, void>
79
80  <AT extends ActionTypes[]>(
81    actions: AT,
82    handler: (action: TypedActionsMap[ValuesOf<AT>], logger: SagaLogger) => ChainActionReturn
83  ): Generator<void, void, void>
84}
85
86function* chainAction2Impl<Actions extends {readonly type: string}>(
87  pattern: Types.Pattern<any>,
88  f: (state: TypedState, action: Actions, logger: SagaLogger) => ChainActionReturn
89) {
90  return yield Effects.takeEvery<TypedActions>(pattern as Types.Pattern<any>, function* chainAction2Helper(
91    action: TypedActions
92  ) {
93    const sl = new SagaLogger(action.type as ActionType, f.name ?? 'unknown')
94    try {
95      let state: TypedState = yield* selectState()
96      // @ts-ignore
97      const toPut = yield Effects.call(f, state, action, sl)
98      // release memory
99      // @ts-ignore
100      action = undefined
101      // @ts-ignore
102      state = undefined
103      if (toPut) {
104        const outActions: Array<TypedActions> = isArray(toPut) ? toPut : [toPut]
105        for (var out of outActions) {
106          if (out) {
107            yield Effects.put(out)
108          }
109        }
110      }
111      if (sl.isTagged) {
112        sl.info('-> ok')
113      }
114    } catch (error) {
115      sl.warn(error.message)
116      // Convert to global error so we don't kill the takeEvery loop
117      yield Effects.put(
118        ConfigGen.createGlobalError({
119          globalError: convertToError(error),
120        })
121      )
122    } finally {
123      if (yield Effects.cancelled()) {
124        sl.info('chainAction cancelled')
125      }
126    }
127  })
128}
129
130export const chainAction2: ChainAction2 = (chainAction2Impl as unknown) as any
131
132function* chainActionImpl<Actions extends {readonly type: string}>(
133  pattern: Types.Pattern<any>,
134  f: (action: Actions, logger: SagaLogger) => ChainActionReturn
135) {
136  return yield Effects.takeEvery<TypedActions>(pattern as Types.Pattern<any>, function* chainActionHelper(
137    action: TypedActions
138  ) {
139    const sl = new SagaLogger(action.type as ActionType, f.name ?? 'unknown')
140    try {
141      // @ts-ignore
142      const toPut = yield Effects.call(f, action, sl)
143      // release memory
144      // @ts-ignore
145      action = undefined
146      if (toPut) {
147        const outActions: Array<TypedActions> = isArray(toPut) ? toPut : [toPut]
148        for (var out of outActions) {
149          if (out) {
150            yield Effects.put(out)
151          }
152        }
153      }
154      if (sl.isTagged) {
155        sl.info('-> ok')
156      }
157    } catch (error) {
158      sl.warn(error.message)
159      // Convert to global error so we don't kill the takeEvery loop
160      yield Effects.put(
161        ConfigGen.createGlobalError({
162          globalError: convertToError(error),
163        })
164      )
165    } finally {
166      if (yield Effects.cancelled()) {
167        sl.info('chainAction cancelled')
168      }
169    }
170  })
171}
172export const chainAction: ChainAction = (chainActionImpl as unknown) as any
173
174function* chainGenerator<
175  Actions extends {
176    readonly type: string
177  }
178>(
179  pattern: Types.Pattern<any>,
180  f: (state: TypedState, action: Actions, logger: SagaLogger) => Generator<any, any, any>
181): Generator<any, void, any> {
182  // @ts-ignore TODO fix
183  return yield Effects.takeEvery<Actions>(pattern, function* chainGeneratorHelper(action: Actions) {
184    const sl = new SagaLogger(action.type as ActionType, f.name ?? 'unknown')
185    try {
186      const state: TypedState = yield* selectState()
187      yield* f(state, action, sl)
188      if (sl.isTagged) {
189        sl.info('-> ok')
190      }
191    } catch (error) {
192      sl.warn(error.message)
193      // Convert to global error so we don't kill the takeEvery loop
194      yield Effects.put(
195        ConfigGen.createGlobalError({
196          globalError: convertToError(error),
197        })
198      )
199    } finally {
200      if (yield Effects.cancelled()) {
201        sl.info('chainGenerator cancelled')
202      }
203    }
204  })
205}
206
207function* selectState(): Generator<any, TypedState, void> {
208  // @ts-ignore codemod issue
209  const state: TypedState = yield Effects.select()
210  return state
211}
212
213/**
214 * The return type of an rpc to help typing yields
215 */
216export type RPCPromiseType<F extends (...rest: any[]) => any, RF = ReturnType<F>> = RF extends Promise<
217  infer U
218>
219  ? U
220  : RF
221
222export type Effect<T> = Types.Effect<T>
223export type PutEffect = Effects.PutEffect<TypedActions>
224export type Channel<T> = RS.Channel<T>
225export {buffers, channel, eventChannel} from 'redux-saga'
226export {
227  all,
228  call as callUntyped,
229  cancel,
230  cancelled,
231  delay,
232  fork as _fork, // fork is pretty unsafe so lets mark it unusually
233  join,
234  race,
235  spawn,
236  take,
237  takeEvery,
238  takeLatest,
239  throttle,
240} from 'redux-saga/effects'
241
242export {selectState, put, sequentially, chainGenerator}
243