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

org.hyperscala.connect.Connect.scala Maven / Gradle / Ivy

package org.hyperscala.connect

import akka.actor.{Actor, ActorSystem, Props}
import argonaut.Argonaut._
import argonaut._
import com.outr.net.http.{HttpApplication, HttpHandler}
import com.outr.net.http.content.StringContent
import com.outr.net.http.request.HttpRequest
import com.outr.net.http.response.{HttpResponse, HttpResponseStatus}
import com.outr.net.http.session.Session
import org.hyperscala.html._
import org.hyperscala.javascript.JavaScriptContent
import org.hyperscala.jquery.jQuery
import org.hyperscala.module.Module
import org.hyperscala.web.{Webpage, Website}
import org.powerscala.concurrent.Time
import org.powerscala.event.Listenable
import org.powerscala.event.processor.UnitProcessor
import org.powerscala.log.Logging
import org.powerscala.{Unique, Version}

/**
 * Connect provides communication support to Hyperscala to allow the client to communicate to the server and the server
 * to communicate back to the client.
 *
 * @author Matt Hicks 
 */
object Connect extends Module with Logging {
  val name = "connect"
  val version = Version(1)

  System.setProperty("akka.daemonic", "on")
  private val system = ActorSystem("RealtimePageActorSystem")
  private[connect] def newActor() = system.actorOf(Props[AsynchronousFunctionActor])

  object Error {
    val InvalidRequest = 1
    val PageNotFound = 2
    val ConnectionNotFound = 3
    val NoContent = 4
  }

  override def dependencies = List(jQuery)

  override def init[S <: Session](website: Website[S]) = {
    website.register("/js/hyperscala.connect.js", "hyperscala.connect.js")
    website.register("/css/hyperscala.connect.css", "hyperscala.connect.css")
    val handler = new ConnectHandler(website)(website.manifest)
    website.addHandler(handler, "/hyperscala.connect/send")
    website.addHandler(handler, "/hyperscala.connect/receive")
  }

  override def load[S <: Session](webpage: Webpage[S]) = {
    webpage.head.contents += new tag.Link(href = "/css/hyperscala.connect.css")
    webpage.head.contents += new tag.Script(src = "/js/hyperscala.connect.js")
    webpage.head.contents += new tag.Script {
      contents += ConnectSession(webpage)
    }
    if (!webpage.store.getOrElse("hyperscala.connect.disableErrorDisplay", false)) {
      webpage.body.contents += new tag.Div(id = "hyperscala_connect_error", clazz = List("hyperscala_connect_error_hidden")) {
        contents += new tag.Div {
          contents += new tag.A(id = "hyperscala_connect_error_close", titleText = "Close", clazz = List("close"))
          contents += new tag.Div(id = "hyperscala_connect_error_message")
        }
      }
    }
  }

  def disableErrorDisplay[S <: Session](webpage: Webpage[S]) = webpage.store.update("hyperscala.connect.disableErrorDisplay", true)

  def event[S <: Session](webpage: Webpage[S])(f: ((Connection[S], Message)) => Unit)(implicit manifest: Manifest[S]) = {
    connections(webpage).on(f)
  }

  def on[S <: Session](webpage: Webpage[S], event: String)(f: Json => Unit)(implicit manifest: Manifest[S]) = {
    connections(webpage).on(event)(f)
  }

  def send[S <: Session](webpage: Webpage[S], event: String, data: Json, sendWhenConnected: Boolean = true)(implicit manifest: Manifest[S]) = {
    connections(webpage).send2Client(event, data, sendWhenConnected)
  }

  def connections[S <: Session](page: Webpage[S])(implicit manifest: Manifest[S]) = {
    page.store.getOrSet("hyperscala.connect", new Connections(page))
  }

  def createError(response: HttpResponse, code: Int, message: String) = {
    warn(s"Connect.createError: $code - $message")
    response.copy(status = HttpResponseStatus.BadRequest(s"$code:$message"))
  }
}

class ConnectHandler[S <: Session](website: Website[S])(implicit manifest: Manifest[S]) extends HttpHandler {
  def onReceive(request: HttpRequest, response: HttpResponse) = request.contentString match {
    case Some(content) => {
      val jsonOption = request.url.filename match {
        case "receive" => content.decodeOption[ReceiveRequest]
        case "send" => content.decodeOption[SendRequest]
      }
      jsonOption match {
        case Some(json) => {
          website.pages.byId[Webpage[S]](json.pageId) match {
            case Some(page) => {
              page.checkIn() // Let the page know it's still in-use
              val conns = Connect.connections(page)
              conns.byId(json.connectionId) match {
                case Some(connection) => request.url.filename match {
                  case "receive" => {
                    val receive = json.asInstanceOf[ReceiveRequest]
                    connection.receive(response, receive.resend)
                  }
                  case "send" => {
                    val send = json.asInstanceOf[SendRequest]
                    send.messages.foreach {
                      case message => connection.send2Server(message)
                    }
                    val r = SendResponse("OK")
                    response.copy(content = StringContent(r.asJson.spaces2), status = HttpResponseStatus.OK)
                  }
                }
                case None => Connect.createError(response, Connect.Error.ConnectionNotFound, "Connection not found")
              }
            }
            case None => Connect.createError(response, Connect.Error.PageNotFound, s"Page not found (id: ${json.pageId})")
          }
        }
        case None => Connect.createError(response, Connect.Error.InvalidRequest, s"JSON ${request.url.filename} data was invalid. Actual content: [$content]")
      }
    }
    case None => Connect.createError(response, Connect.Error.NoContent, "No content was sent in the request.")
  }
}

class Connections[S <: Session](val webpage: Webpage[S])(implicit manifest: Manifest[S]) extends Listenable with Logging {
  private var map = Map.empty[String, Connection[S]]

  private var _backlog = List.empty[(String, Json)]
  def backlog = _backlog

  val created = new UnitProcessor[Connection[S]]("created")

  webpage.html.onAfterRender {
    if (_backlog.nonEmpty) {     // Send backlog after render
      _backlog.reverse.foreach {
        case (event, data) => map.values.foreach(c => c.send2Client(event, data))
      }
      _backlog = Nil
    }
  }

  def isEmpty = map.isEmpty

  lazy val actor = Connect.newActor()
  val messageEvent = new UnitProcessor[(Connection[S], Message)]("messageEvent")

  def on(f: ((Connection[S], Message)) => Unit) = messageEvent.on(f)

  def on(event: String)(f: Json => Unit) = {
    messageEvent.on {
      case (connection, message) => if (message.event == event) {
        f(message.data)
      }
    }
  }

  def create() = synchronized {
    val connection = new Connection(this)
    map += connection.id -> connection
    created.fire(connection)
    connection
  }

  def send2Client(event: String, data: Json, sendWhenConnected: Boolean) = synchronized {
    if (webpage.rendered) {
      map.values.foreach(c => c.send2Client(event, data))
    } else if (sendWhenConnected) {
      _backlog = event -> data :: _backlog
    }
  }

  def byId(id: String) = map.get(id)

  def update() = map.values.foreach(c => c.update())
}

class AsynchronousFunctionActor extends Actor {
  def receive = {
    case f: Function0[_] => f()
  }
}

class Connection[S <: Session](connections: Connections[S]) extends Logging {
  val id = Unique()

  private var server2ClientId = 0
  private var client2ServerId = 0

  def nextServer2ClientId() = synchronized {
    server2ClientId += 1
    server2ClientId
  }

  def webpage = connections.webpage

  private var queue = List.empty[Message]
  private var sentQueue = List.empty[Message]

  def update() = {}

  def send2Client(event: String, data: Json) = synchronized {
    val m = Message(nextServer2ClientId(), event, data)
    queue = m :: queue
  }

  def send2Server(message: Message) = synchronized {
    val expectedId = client2ServerId + 1
    if (message.id == expectedId) {
      client2ServerId = expectedId
      val website = webpage.website
      val request = website.request          // Get the request for the current thread
      val f = () => {
          HttpApplication.around(request) {
            connections.messageEvent.fire(this -> message)
          }
        }
      connections.actor ! f     // Process receives one at a time via actor
    } else if (message.id < expectedId) {           // We've already seen this one, ignore it
      warn(s"ignoring already received message id: ${message.id} (next expected: $expectedId)")
    } else {
      error(s"Lost messages. Expected: $expectedId but received ${message.id}.")
    }
  }

  def receive(response: HttpResponse, resend: Boolean) = {
    if (!resend) {
      Time.waitFor(30.0, precision = 0.05) {      // Wait up to 30 seconds for an entry in the queue
        webpage.checkIn()       // Keep the page alive
        queue.nonEmpty
      }

      synchronized {
        sentQueue = queue.reverse
        queue = List.empty[Message]
      }
    }
    val q = sentQueue
    val r = ReceiveResponse("OK", q)
    val json = r.asJson.spaces2
    response.copy(content = StringContent(json), status = HttpResponseStatus.OK)
  }
}

trait Request {
  def pageId: String
  def connectionId: String
  def timestamp: Long
}

case class ReceiveRequest(pageId: String, connectionId: String, timestamp: Long, resend: Boolean) extends Request

object ReceiveRequest {
  implicit def ReceiveRequestCodecJson: CodecJson[ReceiveRequest] = casecodec4(ReceiveRequest.apply, ReceiveRequest.unapply)("pageId", "connectionId", "timestamp", "resend")
}

case class SendRequest(pageId: String, connectionId: String, timestamp: Long, messages: List[Message]) extends Request

object SendRequest {
  implicit def SendRequestCodecJson: CodecJson[SendRequest] = casecodec4(SendRequest.apply, SendRequest.unapply)("pageId", "connectionId", "timestamp", "messages")
}

case class SendResponse(status: String)

object SendResponse {
  implicit def SendResponseCodecJson: CodecJson[SendResponse] = casecodec1(SendResponse.apply, SendResponse.unapply)("status")
}

case class ReceiveResponse(status: String, messages: List[Message])

object ReceiveResponse {
  implicit def ReceiveResonseCodecJson: CodecJson[ReceiveResponse] = casecodec2(ReceiveResponse.apply, ReceiveResponse.unapply)("status", "messages")
}

case class Message(id: Int, event: String, data: Json)

object Message {
  implicit def MessageCodecJson: CodecJson[Message] = casecodec3(Message.apply, Message.unapply)("id", "event", "data")
}

case class ConnectSession[S <: Session](page: Webpage[S]) extends JavaScriptContent {
  def content = {   // Generate a new connection each rendering of the content
    val connection = page.website.request.store.getOrSet("hyperscala.connect.session", Connect.connections(page)(page.website.manifest).create())
    s"""
      |$$(document).ready(function() {
      | HyperscalaConnect.init('${page.pageId}', '${connection.id}');
      |});
    """.stripMargin
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy