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

ethabi.protocol.ws.Client.scala Maven / Gradle / Ivy

The newest version!
package ethabi.protocol.ws

import akka.NotUsed
import akka.actor.{Actor, ActorRef, ActorSystem, PoisonPill, Props, Status}
import akka.http.scaladsl.Http
import akka.stream.{CompletionStrategy, Materializer, OverflowStrategy}
import akka.stream.scaladsl.{Keep, Sink, Source}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest}
import io.circe.syntax._
import io.circe.generic.auto._
import io.circe.{Decoder, jawn}
import ethabi.protocol.Notifier.{StartSubscribe, SubscribeSucceed, Unsubscribe}
import ethabi.protocol.Subscription._
import ethabi.protocol.Response._
import ethabi.protocol._
import ethabi.protocol.ws.Client.NewRequest
import scala.concurrent.{Future, Promise}
import scala.collection.mutable
import scala.util.{Failure, Success}

final class Client(url: String)(implicit system: ActorSystem, materializer: Materializer) extends Service with Subscription {
  import system.dispatcher

  private val listener = system.actorOf(Props(new Listener))
  // TODO: configurable
  private val (requestReceiver, upgradeResponse) =
    Source.actorRef[Message](PartialFunction.empty, PartialFunction.empty, 1024, OverflowStrategy.dropTail)
      .viaMat(Http().webSocketClientFlow(WebSocketRequest(url)))(Keep.both)
      .to(Sink.actorRef(listener, UpstreamStopped, exp => throw exp))
      .run()

  upgradeResponse onComplete {
    case Success(upgrade) =>
      if (upgrade.response.status != StatusCodes.SwitchingProtocols)
        throw new RuntimeException(s"connect failed: ${upgrade.response.status}")
    case Failure(exception) => throw new RuntimeException(s"connect failed: $exception")
  }

  // default supervisor strategy is ok
  final class Listener extends Actor {
    private val requests = mutable.Map.empty[Long, Promise[Response]]
    private val subscribers = mutable.Map.empty[SubscriptionId, ActorRef]

    override def receive: Receive = {
      case message: TextMessage.Strict => onMessage(message)
      case UpstreamStopped =>
        subscribers.values.foreach(_ ! UpstreamStopped)
        self ! PoisonPill
      case failure: Status.Failure =>
        subscribers.values.foreach(_ ! failure)
        self ! PoisonPill
      case StartSubscribe(target, request) => onSubscribe(target, request)
      case NewRequest(request, promise) => onNewRequest(request, promise)
      case Unsubscribe(id) => onUnsubscribe(id)
      case _ => // log
    }

    private def onNewRequest(request: Request, promise: Promise[Response]): Unit = {
      requests(request.id) = promise
      val message = TextMessage(request.asJson.toString)
      requestReceiver ! message
    }

    private def onMessage(message: TextMessage.Strict): Unit = {
      val json = jawn.parse(message.text) match {
          case Left(parsingFailure) => throw parsingFailure
          case Right(result) => result
        }
        Decoder[Response].either(Decoder[Notification]).decodeJson(json) match {
          case Left(decodingFailure) => throw decodingFailure
          case Right(result) => result match {
            case Left(response) => onResponse(response)
            case Right(notification) => onNotification(notification)
          }
        }
    }

    private def onSubscribe(target: ActorRef, request: Request): Unit = {
      val promise = Promise[Response]
      self ! NewRequest(request, promise)
      promise.future onComplete {
        case Success(response) => response.as[String] match {
          case Right(Some(id)) => subscribers(id) = target; target ! SubscribeSucceed(id)
          case _ => throw new RuntimeException(s"subscribe failed")
        }
        case Failure(exception) => throw exception
      }
    }

    private def onUnsubscribe(id: String): Unit = {
      subscribers.remove(id)
      val promise = Promise[Response]
      self ! NewRequest(Request.unsubscribe(id), promise)
      promise.future onComplete {
        case Success(_) =>
        case Failure(_) => onUnsubscribe(id)   // try again when failed
      }
    }

    private def onResponse(response: Response): Unit = {
      requests(response.id).trySuccess(response)
      requests.remove(response.id)
    }

    private def onNotification(notification: Notification): Unit = {
      subscribers.get(notification.params.subscription).foreach(_ ! notification)
    }
  }

  override def allowSubscribe: Boolean = true
  override def doRequest(req: Request): Future[Response] = {
    val promise = Promise[Response]
    listener ! NewRequest(req, promise)
    promise.future
  }

  override def subscribeNewHeaders(includeTransactions: Boolean = false): Source[Header, NotUsed] = {
    val request = Request.subscribeNewHeader(includeTransactions)
    Source.fromGraph(new Notifier[Header](listener, request))
  }

  override def subscribeLogs(logQuery: Request.LogQuery): Source[Log, NotUsed] = {
    val request = Request.subscribeLogs(logQuery)
    Source.fromGraph(new Notifier[Log](listener, request))
  }

  override def subscribeNewPendingTransaction(): Source[String, NotUsed] = {
    val request = Request.subscribeNewPendingTransactions()
    Source.fromGraph(new Notifier[String](listener, request))
  }

  override def subscribeSyncStatus(): Source[SyncStatus, NotUsed] = {
    val request = Request.subscribeSyncStatus()
    Source.fromGraph(new Notifier[SyncStatus](listener, request))
  }
}

object Client {
  private case class NewRequest(request: Request, promise: Promise[Response])

  def apply(url: String)(implicit system: ActorSystem, materialzier: Materializer) = new Client(url)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy