src/insist.ts   A
last analyzed

Complexity

Total Complexity 38
Complexity/F 0

Size

Lines of Code 119
Function Count 0

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 38
eloc 97
mnd 38
bc 38
fnc 0
dl 0
loc 119
rs 9.36
bpm 0
cpm 0
noi 0
c 0
b 0
f 0
1
export enum InsistPolicy {
2
  RETRY = 'RETRY',
3
  THROW = 'THROW',
4
}
5
6
type FallibleFunction<R> = (() => Promise<R>) | (() => R)
7
type ErrorTypeConstructor = new (...args: any[]) => Error
8
type SimpleErrorClassifier = (_: any) => InsistPolicy | null
9
type ComplexErrorClassifier = (_: any, setRetryAfter: (t: number) => void) => InsistPolicy | null
10
type ErrorClassifier = SimpleErrorClassifier | ComplexErrorClassifier
11
type PromiseInnerType<P> = {
12
  0: P extends Promise<infer T> ? PromiseInnerType<T> : never
13
  1: P
14
}[P extends Promise<infer T> ? 0 : 1]
15
16
export type InsistOptions = {
17
  maxRetries?: number
18
  retryInterval?: number
19
  intervalIncrements?: number
20
  maxRetryInterval?: number
21
  insistPolicy?: InsistPolicy
22
  alwaysRetryOn?: ErrorTypeConstructor[]
23
  alwaysThrowOn?: ErrorTypeConstructor[]
24
  errorClassifier?: ErrorClassifier
25
}
26
export type BasicInsistOptions = Omit<InsistOptions, 'alwaysRetryOn' | 'alwaysThrowOn' | 'insistPolicy'>
27
28
const isSimpleErrorClassifier = (v: ErrorClassifier): v is SimpleErrorClassifier => v.length === 1
29
30
const getSimpleErrorClassifier = (
31
  errorClassifier: ErrorClassifier | undefined,
32
  setRetryAfter: (t: number) => void
33
): SimpleErrorClassifier => {
34
  if (errorClassifier === undefined) {
35
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
36
    return (_: any) => null
37
  }
38
  return isSimpleErrorClassifier(errorClassifier) ? errorClassifier : (_: any) => errorClassifier(_, setRetryAfter)
39
}
40
41
const _sleep = async (ms: number): Promise<void> => {
42
  return new Promise<void>(resolve => setTimeout(resolve, ms))
43
}
44
45
export const insist = async <R>(
46
  fn: FallibleFunction<R>,
47
  _retryOptions?: InsistOptions
48
): Promise<PromiseInnerType<R>> => {
49
  const maxRetries = _retryOptions?.maxRetries ?? 5
50
  const intervalIncrements = _retryOptions?.intervalIncrements ?? 1000
51
  const maxRetryInterval = _retryOptions?.maxRetryInterval ?? 15000
52
  const insistPolicy = _retryOptions?.insistPolicy ?? InsistPolicy.RETRY
53
  const alwaysRetryOn = _retryOptions?.alwaysRetryOn ?? []
54
  const alwaysThrowOn = _retryOptions?.alwaysThrowOn ?? []
55
56
  let retryAfter: null | number = null
57
  let retryInterval = _retryOptions?.retryInterval ?? 2000
58
  let e: Error | null
59
60
  const errorClassifier: SimpleErrorClassifier = getSimpleErrorClassifier(
61
    _retryOptions?.errorClassifier,
62
    (t: number) => {
63
      retryAfter = t !== null && !isNaN(t) && t >= 0 ? t : null
64
    }
65
  )
66
67
  try {
68
    return (await fn()) as PromiseInnerType<R>
69
  } catch (_e) {
70
    e = _e
71
  }
72
73
  for (let numIntents = 0; numIntents < maxRetries; numIntents++) {
74
    let retry = insistPolicy === InsistPolicy.RETRY
75
76
    // Check for special cases
77
    if (retry) {
78
      if (errorClassifier(e) === InsistPolicy.THROW) {
79
        retry = false
80
      } else {
81
        for (const errorType of alwaysThrowOn) {
82
          if (e !== null && e instanceof errorType) {
83
            retry = false
84
            break
85
          }
86
        }
87
      }
88
    } else {
89
      if (errorClassifier(e) === InsistPolicy.RETRY) {
90
        retry = true
91
      } else {
92
        for (const errorType of alwaysRetryOn) {
93
          if (e !== null && e instanceof errorType) {
94
            retry = true
95
            break
96
          }
97
        }
98
      }
99
    }
100
101
    // Throw or retry
102
    if (!retry) {
103
      throw e
104
    }
105
106
    await _sleep(retryAfter ?? retryInterval)
107
    retryInterval = Math.min(retryInterval + intervalIncrements, maxRetryInterval)
108
    retryAfter = null
109
110
    try {
111
      return (await fn()) as PromiseInnerType<R>
112
    } catch (_e) {
113
      e = _e
114
    }
115
  }
116
117
  throw e // It should be unreachable
118
}
119