Permalink
Browse files

Nearly done.

  • Loading branch information...
darkfrog26 committed Jul 21, 2016
1 parent ef00246 commit 72fbb1bb3fa2e3ca5d7398e58b52c93d2f368f52
View
@@ -1,2 +1,3 @@
target
.idea/
.idea/
*fastopt*
@@ -0,0 +1,62 @@
package org.hyperscala
import com.outr.scribe.Logging
import org.scalajs.dom._
import org.scalajs.dom.raw.WebSocket
class ClientConnectionManager(app: WebApplication) extends ConnectionManager {
private lazy val _connection = new ClientConnection(app)
override def connections: Set[Connection] = Set(_connection)
override def connectionOption: Option[Connection] = Option(_connection)
override def init(): Unit = _connection.init()
}
class ClientConnection(val app: WebApplication) extends Connection with Logging {
private lazy val url = s"ws://${window.location.host}${app.communicationPath}"
private lazy val webSocket = new WebSocket(url)
private var connected = false
private var queue = List.empty[String]
override def init(): Unit = {
webSocket.onopen = (evt: Event) => {
logger.info(s"WebSocket connection open")
connected = true
if (queue.nonEmpty) {
queue.reverse.foreach { backlog =>
webSocket.send(backlog)
}
queue = Nil
}
}
webSocket.onerror = (evt: ErrorEvent) => {
logger.info(s"WebSocket error: ${evt.message}")
}
webSocket.onclose = (evt: CloseEvent) => {
logger.info(s"WebSocket connection closed")
}
webSocket.onmessage = (evt: MessageEvent) => {
val messageData = evt.data.toString
val index = messageData.indexOf(':')
if (index == -1) {
logger.error(s"Ignoring invalid message: $messageData")
} else {
val id = messageData.substring(0, index).toInt
val json = messageData.substring(index + 1)
receive(id, json)
}
}
}
override def send(id: Int, json: String): Unit = {
val message = s"$id:$json"
if (connected) {
webSocket.send(message)
} else {
queue = message :: queue
}
}
}
@@ -5,8 +5,6 @@ import org.scalajs.dom.ext._
import org.scalajs.dom.raw.HTMLElement
trait ClientScreen extends Screen {
override def app: ClientWebApplication
type URL = String
protected def init(): Unit
@@ -1,7 +0,0 @@
package org.hyperscala
import pl.metastack.metarx.Sub
trait ClientWebApplication extends WebApplication {
lazy val screen: Sub[Option[Screen]] = Sub[Option[Screen]](None)
}
@@ -0,0 +1,60 @@
package org.hyperscala
import com.outr.scribe.Logging
import io.undertow.websockets.WebSocketConnectionCallback
import io.undertow.websockets.core.{AbstractReceiveListener, BufferedTextMessage, WebSocketChannel, WebSockets}
import io.undertow.websockets.spi.WebSocketHttpExchange
class ServerConnectionManager(val app: WebApplication) extends WebSocketConnectionCallback with ConnectionManager {
private val currentConnection = new ThreadLocal[Option[Connection]] {
override def initialValue(): Option[Connection] = None
}
private var _connections = Set.empty[Connection]
override def connections: Set[Connection] = _connections
override def connectionOption: Option[Connection] = currentConnection.get()
override def init(): Unit = {}
override def onConnect(exchange: WebSocketHttpExchange, channel: WebSocketChannel): Unit = {
logger.info("WebSocket connected!")
val connection = new ServerConnection(this, channel)
synchronized {
_connections += connection
}
channel.getReceiveSetter.set(connection)
}
def using[R](connection: Connection)(f: => R): R = {
currentConnection.set(Option(connection))
try {
f
} finally {
currentConnection.remove()
}
}
}
class ServerConnection(manager: ServerConnectionManager, channel: WebSocketChannel) extends AbstractReceiveListener with Connection with Logging {
override def app: WebApplication = manager.app
override def init(): Unit = {}
override def send(id: Int, json: String): Unit = WebSockets.sendText(s"$id:$json", channel, None.orNull)
override def onFullTextMessage(channel: WebSocketChannel, message: BufferedTextMessage): Unit = {
val data = message.getData
val index = data.indexOf(':')
if (index == -1) {
logger.error(s"Ignoring invalid message: $data")
} else {
val id = data.substring(0, index).toInt
val json = data.substring(index + 1)
manager.using(this) {
receive(id, json)
}
}
}
}
@@ -11,7 +11,6 @@ trait Connection {
* @param id the id of the pickler used
* @param json the JSON data to send
*/
// TODO: Macro for sending
def send(id: Int, json: String): Unit
/**
@@ -0,0 +1,10 @@
package org.hyperscala
import com.outr.scribe.Logging
trait ConnectionManager extends Logging {
def connections: Set[Connection]
def connectionOption: Option[Connection]
def connection: Connection = connectionOption.getOrElse(throw new RuntimeException("No connection defined."))
def init(): Unit
}
@@ -1,6 +1,6 @@
package org.hyperscala
import pl.metastack.metarx.Channel
import pl.metastack.metarx.{Channel, StateChannel}
import scala.annotation.compileTimeOnly
import scala.language.experimental.macros
@@ -35,7 +35,7 @@ object Macros {
val typeString = s.tpe.toString
val (preType, postType) = if (typeString.indexOf('.') != -1) {
val index = typeString.indexOf('.')
(typeString.substring(0, index + 1) -> typeString.substring(index + 1))
typeString.substring(0, index + 1) -> typeString.substring(index + 1)
} else {
"" -> typeString
}
@@ -57,4 +57,23 @@ object Macros {
}
""")
}
def connectionManager(c: blackbox.Context)(): c.Expr[ConnectionManager] = {
import c.universe._
val isJS = try {
c.universe.rootMirror.staticClass("scala.scalajs.js.Any")
true
} catch {
case t: Throwable => false
}
val manager = if (isJS) {
q"""new org.hyperscala.ClientConnectionManager(${c.prefix.tree})"""
} else {
q"""new org.hyperscala.ServerConnectionManager(${c.prefix.tree})"""
}
c.Expr[ConnectionManager](manager)
}
}
@@ -1,27 +1,26 @@
package org.hyperscala
import pl.metastack.metarx.Sub
import pl.metastack.metarx.{StateChannel, Sub}
import scala.language.experimental.macros
abstract class WebApplication(host: String, port: Int) {
abstract class WebApplication(val host: String, val port: Int) {
protected[hyperscala] var picklers = Vector.empty[Pickler[_]]
protected[hyperscala] var screens = Vector.empty[Screen]
protected[hyperscala] var connections = Set.empty[Connection]
protected[hyperscala] val currentConnection = new ThreadLocal[Option[Connection]] {
override def initialValue(): Option[Connection] = None
}
protected[hyperscala] def connection: Connection = currentConnection.get().getOrElse(throw new RuntimeException(s"Connection not specified."))
protected def createConnectionManager(): ConnectionManager = macro Macros.connectionManager
protected val connectionManager: ConnectionManager
def create[S <: Screen]: S = macro Macros.screen[S]
def communicationPath: String = "/communication"
protected[hyperscala] def add[T](pickler: Pickler[T]): Unit = synchronized {
val position = picklers.length
picklers = picklers :+ pickler
pickler.channel.attach { t =>
if (!pickler.receiving.get()) {
val json = pickler.write(t)
connection.send(position, json)
connectionManager.connection.send(position, json)
}
}
}
@@ -0,0 +1,22 @@
package example
import com.outr.scribe.Logging
import org.hyperscala.ClientScreen
import org.scalajs.dom._
trait ClientDashboardScreen extends DashboardScreen with ClientScreen with Logging {
override protected def init(): Unit = {
logger.info("Dashboard init!")
}
override protected def activate(): URL = {
logger.info(s"Dashboard activate!")
"/dashboard.html"
}
override protected def deactivate(): Unit = {
logger.info("Dashboard deactivate!")
}
override def isActive: Boolean = document.location.pathname == "/dashboard.html"
}
@@ -2,17 +2,9 @@ package example
import com.outr.scribe.Logging
import org.hyperscala.ClientScreen
import org.scalajs.dom.{Event, html}
import org.scalajs.dom._
trait ClientLoginScreen extends LoginScreen with Logging with ClientScreen {
// Change screen upon successful login
response.attach { r =>
r.errorMessage match {
case Some(message) => logger.warn(s"Failed to authenticate: $message")
case None => app.screen := Some(ExampleApplication.dashboard)
}
}
// Configure form submit
// TODO: load 'form' if not already loaded
lazy val form = byId[html.Form]("form")
@@ -22,22 +14,34 @@ trait ClientLoginScreen extends LoginScreen with Logging with ClientScreen {
override def init(): Unit = {
logger.info(s"Login init!")
// Change screen upon successful login
response.attach { r =>
r.errorMessage match {
case Some(message) => logger.warn(s"Failed to authenticate: $message")
case None => logger.info("Should log in...") //app.screen := Some(ExampleApplication.dashboard) // TODO: implement
}
}
// Send authentication request to server
form.onsubmit = (evt: Event) => {
authenticate := Authentication(username.value, password.value)
false
}
}
override def activate(): Unit = {
override def activate(): URL = {
logger.info(s"Login Activated!")
form.style.display = "block"
"/login.html"
}
override def deactivate(): Unit = {
logger.info(s"Login Deactivated!")
form.style.display = "none"
}
override def isActive: Boolean = document.location.pathname == "/login.html"
}
@@ -0,0 +1,16 @@
<html>
<head>
<title>Example</title>
</head>
<body>
<div id="content">
<div id="example">
<h2>Example web application</h2>
</div>
</div>
<script type="application/javascript" src="app/hyperscala-example-fastopt.js"></script>
<script type="application/javascript">
org.hyperscala.example.ExampleApp().main();
</script>
</body>
</html>
@@ -0,0 +1,26 @@
<html>
<head>
<title>Login</title>
</head>
<body>
<div id="content">
<form id="login">
<div>
<label for="username">Username:</label>
<input type="text" id="username"/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password"/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</div>
<script type="application/javascript" src="app/hyperscala-example-fastopt.js"></script>
<script type="application/javascript">
org.hyperscala.example.ExampleApp().main();
</script>
</body>
</html>
@@ -1,15 +1,13 @@
package example
import org.hyperscala.{Screen, WebApplication}
import org.hyperscala.{ConnectionManager, Screen, WebApplication}
import pl.metastack.metarx.Channel
object ExampleApplication extends WebApplication("localhost", 8080) {
override protected val connectionManager: ConnectionManager = createConnectionManager()
val login = create[LoginScreen]
val dashboard = create[DashboardScreen]
override protected[hyperscala] def send(id: Int, json: String): Unit = {
// TODO: implement
}
}
trait LoginScreen extends Screen {
Oops, something went wrong.

0 comments on commit 72fbb1b

Please sign in to comment.