I have the following code. It returns a flow where...
# arrow
j
I have the following code. It returns a flow where I emit a payment status while it’s being processed. • I start by requesting a qrCode, customers use it to open a payment app preconfigured with payment recipient and sum. • I then need to poll an api that will answer “not approved” until the customer has pay. • Once the customer has pay, the process is done I need to constraint the polling to an arbitrary timeout value, is there a better/cleaner way to do this?
Copy code
fun processPayment(price: Int) = flow {
    emit(PaymentStatus.Working)
    either {
        val qrCode = getQrCode(price).bind()
        emit(PaymentStatus.ShowQrCode(qrCode.url))
        try {
            withTimeout(twoMinutes()) {
                var paymentIsApproved = false
                while (!paymentIsApproved) {
                    val paymentStatus = getPaymentFromApi(qrCode.reference).bind()
                    paymentIsApproved = paymentStatus.aggregate.authorizedAmount.value == price.toLong()
                    delay(2000)
                }
                emit(PaymentStatus.Success)
            }
        } catch (e: Exception) {
            emit(paymentError(PaymentError.CouldNotVerifyPaymentBeforeTimeout))
        }
    }.mapLeft(::paymentError)
}
a
Copy code
fun processPayment(price: Int) = flow {
    emit(PaymentStatus.Working)
    either {
        val qrCode = getQrCode(price).bind()
        emit(PaymentStatus.ShowQrCode(qrCode.url))
        Schedule.spaced(2.seconds).whileOutput { it < 2.minutes }.retryOrElse {
          fa = {
             val paymentStatus = getPaymentFromApi(qrCode.reference).bind()
             val paymentIsApproved = paymentStatus.aggregate.authorizedAmount.value == price.toLong()
             if (paymentIsApproved) emit(PaymentStatus.Success)
          },
          orElse = {
             emit(paymentError(PaymentError.CouldNotVerifyPaymentBeforeTimeout))
          }
        }
    }.mapLeft(::paymentError)
}
the code above does not compile, it’s just a sketch; but the idea is that using Schedule you can retry something following a policy (like “every 2 seconds, up to 2 minutes”)
f
a bit offtopic, but the
flow
lambda argument type returns
Unit
so I don't think the Either value is returned. Makes me wonder what is the best way to capture the getQrCode logic failure. Maybe define processPayment with Raise context or extension?
j
@simon.vergauwen do you have an opinion on this?
s
I think the code using
Schedule
is indeed nicer instead of manually implementing the
Schedule
, but Francis is also correct in that the `Eitherem`` value is never returned or rather emitted.
j
Shouldn’t I use a
else
branch after
if(paymentApproved)
to throw a
Throwable
? Otherwise it would not retry, right?
And the return type after
retryOrElse
is
Any
not
Either<PaymentError, PaymentStatus>
s
I am a bit confused by the snippet. do
PaymentError
and
PaymentStatus
share a common parent?
j
I guess my naming here wasn't optimal hehe.
PaymentStatus
is a sealed class with
Working
,
Success
and
Error
as sub-elements.
Error
takes a
PaymentError
as parameter. It can be
Timeout
or whatever comes from the
bind()
s
So the return type is
Flow<PaymentStatus>
?
j
Well I only care about the final result. So success or whatever was the reason for it to fail. And I would emit that value int
flow
{ ... } That contains the schedule logic
s
So you only care about the final value? 🤔 So why use
Flow
?
j
I use the flow to emit the status of the payment :
Copy code
flow {
    emit(PaymentStatus.Working) // the process start, I emit "Working", the ui can show a spinner for example
    val result = Schedule... // Here I want to poll my endpoint until I get the expected response, then I emit that final "Success" or "Error" if the IO requests return 4xx, 5xx or if I get a timeout
    emit(result)
}
This does what I want I think: 1. emit “Working” 2. emit “ShowQrCode” 3. emit “Success” when the payment is authorized OR “Error” if anything went wrong or timeout happened
s
I think you can use something like this to not rely on
Throwable
there. So basically repeat polling until you reach
Either.Right
with the desired value.
Copy code
Schedule
  .spaced<Either<VippsError, PaymentStatus>>(twoSeconds)
  .whileOutput { it < twoMinutes }
  .whileInput {
    it.map { it.aggregate.authorizedAmount.value != price.toLong() }
      .getOrElse { true }
  }
  .zipRight(Schedule.identity())
  .repeatOrElseEither(
    fa = { getPayment(qrCode)) },
    orElse = { VippsError.from(throwable).left() }
  ).fold(
    { emit(PaymentStatus.Success) },
    { emit(it) }
  )
j
Nice, thanks for the help! 👏🏻