All Downloads are FREE. Search and download functionalities are using the official Maven repository.

japgolly.scalajs.react.extra.internal.AjaxF.scala Maven / Gradle / Ivy

package japgolly.scalajs.react.extra.internal

import japgolly.scalajs.react.util.Effect
import org.scalajs.dom.{ProgressEvent, XMLHttpRequest}
import scala.scalajs.js
import scala.util.{Failure, Success}

/** Purely-functional AJAX.
  *
  * For a demo, see
  *   - https://japgolly.github.io/scalajs-react/#examples/ajax-1
  *   - https://japgolly.github.io/scalajs-react/#examples/ajax-2
  */
class AjaxF[F[_], Async[_]](implicit F: Effect.Sync[F], Async: Effect.Async[Async]) {

  def deriveErrorMessage(xhr: XMLHttpRequest): String = {
//    var err = Option(xhr.statusText).map(_.trim).filter(_.nonEmpty) getOrElse xhr.status.toString
//    Option(xhr.responseText).map(_.trim).filter(_.nonEmpty).foreach(r => err = s"$err. Response: $r")
//    err
//    val resp = Option(xhr.responseText).map(_.trim).filter(_.nonEmpty).map("Response: " + _)
    s"[${xhr.status}] Response: ${xhr.responseText}"
  }

  /** Generic HTTP code validation */
  def isStatusSuccessful(status: Int): Boolean =
    (status >= 200 && status < 300) || status == 304

  // ===================================================================================================================
  // Step 1

  def apply(method: String, url: String): Step1 =
    new Step1(xhr => F.delay(xhr.open(method, url, true)))

  def apply(method: String, url: String, user: String, password: String): Step1 =
    new Step1(xhr => F.delay(xhr.open(method, url, true, user, password)))

  def get(url: String): Step1 =
    apply("GET", url)

  def post(url: String): Step1 =
    apply("POST", url)

  private type Ajax[A] = XMLHttpRequest => F[A]

  final class Step1(init: Ajax[Unit]) {
    def and(f: XMLHttpRequest => Unit): Step1 =
      new Step1(xhr => F.chain(init(xhr), F.delay(f(xhr))))

    def setRequestHeader(header: String, value: String): Step1 =
      and(_.setRequestHeader(header, value))

    def setRequestContentTypeJson: Step1 =
      setRequestHeader("Content-Type", "application/json")

    def setRequestContentTypeJson(charset: String): Step1 =
      setRequestHeader("Content-Type", "application/json;charset=" + charset)

    def setRequestContentTypeJsonUtf8: Step1 =
      setRequestContentTypeJson("UTF-8")

    def send: Step2 =
      new Step2(
        xhr => F.chain(init(xhr), F.delay(xhr.send())),
        None, None, None, None)

    def send(requestBody: js.Any): Step2 =
      new Step2(
        xhr => F.chain(init(xhr), F.delay(xhr.send(requestBody))),
        None, None, None, None)
  }

  // ===================================================================================================================
  // Step 2

  private type OnProgress = (XMLHttpRequest, ProgressEvent) => F[Unit]

  final class Step2(begin             : Ajax[Unit],
                    onreadystatechange: Option[Ajax[Unit]],
                    ontimeout         : Option[Ajax[Unit]],
                    onprogress        : Option[OnProgress],
                    onuploadprogress  : Option[OnProgress]) {

    private def copy(begin             : Ajax[Unit]         = begin,
                     onreadystatechange: Option[Ajax[Unit]] = onreadystatechange,
                     ontimeout         : Option[Ajax[Unit]] = ontimeout,
                     onprogress        : Option[OnProgress] = onprogress,
                     onuploadprogress  : Option[OnProgress] = onuploadprogress): Step2 =
      new Step2(
        begin              = begin,
        onreadystatechange = onreadystatechange,
        ontimeout          = ontimeout,
        onprogress         = onprogress,
        onuploadprogress   = onuploadprogress)

    private def optionalBefore[A, B, C](before: Option[A => F[B]], last: A => F[C]): A => F[C] =
      before.fold(last)(b => (a: A) => F.chain(b(a), last(a)))

    private def optionalBefore[A1, A2, B, C](before: Option[(A1, A2) => F[B]], last: (A1, A2) => F[C]): (A1, A2) => F[C] =
      before.fold(last)(b => (a1: A1, a2: A2) => F.chain(b(a1, a2), last(a1, a2)))

    def withTimeout(millis: Double, f: XMLHttpRequest => F[Unit]): Step2 =
      copy(
        begin     = xhr => F.chain(F.delay(xhr.timeout = millis), begin(xhr)),
        ontimeout = Some(optionalBefore(ontimeout, f)),
      )

    // TODO Prevent before withTimeout
    def onTimeout(f: XMLHttpRequest => F[Unit]): Step2 =
      copy(ontimeout = Some(optionalBefore(ontimeout, f)))

    def onDownloadProgress(f: OnProgress): Step2 =
      copy(onprogress = Some(optionalBefore(onprogress, f)))

    def onUploadProgress(f: OnProgress): Step2 =
      copy(onuploadprogress = Some(optionalBefore(onuploadprogress, f)))

    private def onReadyStateChange(f: Ajax[Unit]): Step2 =
      copy(onreadystatechange = Some(optionalBefore(onreadystatechange, f)))

    private def onCompleteKleisli(f: Ajax[Unit]): Ajax[Unit] =
      xhr => F.when_(xhr.readyState == XMLHttpRequest.DONE)(f(xhr))

    def onComplete(f: XMLHttpRequest => F[Unit]): Step2 =
      onReadyStateChange(onCompleteKleisli(f))

    def validateResponse(isValid: XMLHttpRequest => Boolean): (AjaxException => F[Unit]) => Step2 =
      onFailure => onComplete(xhr =>
        F.unless_(isValid(xhr))(onFailure(AjaxException(xhr))))

    def validateStatus(isValidStatus: Int => Boolean): (AjaxException => F[Unit]) => Step2 =
      validateResponse(xhr => isValidStatus(xhr.status))

    def validateStatusIs(expectedStatus: Int): (AjaxException => F[Unit]) => Step2 =
      validateStatus(_ == expectedStatus)

    def validateStatusIsSuccessful: (AjaxException => F[Unit]) => Step2 =
      validateStatus(isStatusSuccessful)

    private def registerU(k: Ajax[Unit])(set: (XMLHttpRequest, js.Function1[Any, Unit]) => Unit): Ajax[Unit] =
      xhr => F.delay(set(xhr, _ => F.runSync(F.suspend(k(xhr)))))

    private def register_(cb: Option[Ajax[Unit]])(set: (XMLHttpRequest, js.Function1[Any, Unit]) => Unit): Ajax[Unit] =
      cb match {
        case Some(k) => registerU(k)(set)
        case None    => _ => F.empty
      }

    private def registerE[E](cb: Option[(XMLHttpRequest, E) => F[Unit]])
                            (set: (XMLHttpRequest, js.Function1[E, Unit]) => Unit): Ajax[Unit] =
      cb match {
        case Some(k) => xhr => F.delay(set(xhr, e => F.runSync(k(xhr, e))))
        case None    => _ => F.empty
      }

    private def registerSecondaryCallbacks: Ajax[Unit] =
      xhr =>
        F.chain(
          register_(ontimeout)(_.ontimeout = _)(xhr),
          registerE(onprogress)(_.onprogress = _)(xhr),
          registerE(onuploadprogress)(_.upload.onprogress = _)(xhr),
        )

    def asCallback: F[Unit] =
      F.flatMap(newXHR)(xhr =>
        F.chain(
          register_(onreadystatechange)(_.onreadystatechange = _)(xhr),
          registerSecondaryCallbacks(xhr),
          begin(xhr),
        )
      )

    def asAsyncCallback: Async[XMLHttpRequest] =
      Async.first[XMLHttpRequest] { cc =>

        val fail: Throwable => F[Unit] =
          t => F.fromJsFn0(cc(Failure(t)))

        val onreadystatechange: Ajax[Unit] = {
          val complete = onCompleteKleisli(xhr => F.fromJsFn0(cc(Success(xhr))))
          xhr => {
            val main = F.suspend(F.chain(
              this.onreadystatechange.fold(F.empty)(_(xhr)),
              complete(xhr),
            ))
            F.handleError(main)(fail)
          }
        }

        val onerror: Ajax[Unit] =
          xhr => fail(AjaxException(xhr))

        val start: Ajax[Unit] =
          xhr => F.chain(
            registerU(onreadystatechange)(_.onreadystatechange = _)(xhr),
            registerU(onerror)(_.onerror = _)(xhr),
            registerSecondaryCallbacks(xhr),
            begin(xhr),
          )

        val main1 = F.flatMap(newXHR)(start)
        val main2 = F.handleError(main1)(fail)
        F.toJsFn(main2)
      }
  }

  private val newXHR = F.delay(new XMLHttpRequest())
}

final case class AjaxException(xhr: XMLHttpRequest) extends Exception {
  def isTimeout = xhr.status == 0 && xhr.readyState == 4
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy