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

io.youi.communication.Connection.scala Maven / Gradle / Ivy

The newest version!
package io.youi.communication

import fabric.parse.Json
import fabric.rw._
import io.youi.http.{BinaryData, ByteBufferData, ConnectionStatus, WebSocket}
import reactify.reaction.Reaction
import reactify.{Val, Var}
import scribe.Execution.global

import scala.concurrent.{ExecutionContext, Future}
import scala.language.experimental.macros

trait Connection {
  val webSocket: Var[Option[WebSocket]] = Var(None)
  val queue: HookupQueue = new HookupQueue
  val status: Val[ConnectionStatus] = Val(webSocket().map(_.status()).getOrElse(ConnectionStatus.Closed))
  val lastActive: Val[Long] = Var[Long](0L)

  // Created for binary output and removed upon completion
  private var writer: Option[ByteBufferWriter] = None
  private var uploads: Map[Long, Upload] = Map.empty

  object hookups {
    private var map = Map.empty[String, Hookup[Any]]

    def register[Interface](hookup: Hookup[Interface]): Unit = synchronized {
      map += hookup.name -> hookup.asInstanceOf[Hookup[Any]]
    }

    def byName(name: String): Hookup[Any] = map.getOrElse(name, throw new RuntimeException(s"Unable to find hookup by name: $name (names: ${map.keySet.mkString(", ")})"))
  }

  def receive(message: Message): Future[Message] = {
    val hookup = hookups.byName(message.name.get)
    hookup.receive(message)
  }

  def upload(fileName: String, bytes: Long): Upload = synchronized {
    val message = Message.uploadStart(fileName, bytes)
    val future = queue.enqueue(message).map { response =>
      response.name.get
    }
    val upload = Upload(fileName, bytes, future)
    uploads += message.id -> upload
    upload
  }

  protected def interface[Interface]()(implicit ec: ExecutionContext): Interface with Hookup[Interface] = macro HookupMacros.interface[Interface]
  protected def implementation[Interface, Implementation <: Interface]()(implicit ec: ExecutionContext): Implementation with Hookup[Interface] = macro HookupMacros.implementation[Interface, Implementation]

  private val receiveText: Reaction[String] = Reaction[String] { text =>
    lastActive.asInstanceOf[Var[Long]] @= System.currentTimeMillis()
    text match {
      case "PING" => webSocket.foreach(_.send.text @= "PONG")
      case "PONG" => // Ignore keep-alive
      case _ if text.startsWith("{") && text.endsWith("}") => {
        val message = Json.parse(text).as[Message]
        message.`type` match {
          case MessageType.Invoke => receive(message).foreach(queue.enqueue)
          case MessageType.Response => if (queue.success(message)) {
            // Success
          } else {
            scribe.warn(s"No id found for ${message.id}. Cannot apply: $message")
          }
          case MessageType.UploadStart => {
            val w = CommunicationPlatform.createWriter(message.name.get, message.bytes.get)
            writer = Some(w)
            w.promise.future.foreach { _ =>
              queue.enqueue(Message.uploadComplete(message.id, w.actualFileName))
            }
          }
          case MessageType.UploadComplete => if (queue.success(message)) {
            // Success
          } else {
            scribe.warn(s"No id found for ${message.id}. Cannot apply: $message")
          }
          case MessageType.Error => if (queue.failure(message.id, new RuntimeException(message.toString))) {
            // Success
          } else {
            scribe.warn(s"No id found for ${message.id}. Cannot fail: $message")
          }
        }
      }
      case _ => scribe.warn(s"Unhandled: $text")
    }
  }

  private val receiveBinary: Reaction[BinaryData] = Reaction[BinaryData] { message =>
    lastActive.asInstanceOf[Var[Long]] @= System.currentTimeMillis()

    writer match {
      case Some(w) => message match {
        case ByteBufferData(m) => {
          w.write(m)
          // TODO: support MessageType.UploadStatus
          if (w.remaining == 0L) {
            w.close()
            w.promise.success(())
          }
        }
      }
      case None => scribe.info("No writer assigned!")
    }
  }

  webSocket.changes {
    case (oldClient, newClient) => {
      oldClient.foreach { c =>
        c.receive.text.reactions -= receiveText
        c.receive.binary.reactions -= receiveBinary

        c.disconnect()
      }
      newClient.foreach { c =>
        c.receive.text.reactions += receiveText
        c.receive.binary.reactions += receiveBinary
      }
    }
  }
  queue.hasNext.attach(_ => checkQueue())
  status.changes {
    case (oldStatus, newStatus) => if (oldStatus == ConnectionStatus.Open) {
      lastActive.asInstanceOf[Var[Long]] @= System.currentTimeMillis()
    } else if (newStatus == ConnectionStatus.Open) {
      checkQueue()
    }
  }

  def disconnect(): Unit = webSocket := None

  def dispose(): Unit = {
    disconnect()
    queue.dispose()
  }

  /**
    * Checks for queue entries if connected
    */
  private def checkQueue(): Unit = synchronized {
    try {
      if (webSocket().exists(_.status() == ConnectionStatus.Open)) {
        val ws = webSocket().get
        queue.next() match {
          case Some(request) => {
            ws.send.text @= Json.format(request.request.toValue)
            checkQueue()
          }
          case None => // Nothing in the queue
        }
      }
    } catch {
      case t: Throwable => scribe.error(s"Error while checking queue", t)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy