diff options
author | Li Haoyi <haoyi@dropbox.com> | 2014-09-05 14:17:29 -0700 |
---|---|---|
committer | Li Haoyi <haoyi@dropbox.com> | 2014-09-05 14:17:29 -0700 |
commit | b9624be0c341ad070869497d2d10db0bb6beb3ed (patch) | |
tree | ee6e5ee7b7c07248acbf9b6f97911d6541c72572 | |
parent | fa3b7505ee26c4db0abbe72a4139ea4dafaaad60 (diff) | |
download | workbench-b9624be0c341ad070869497d2d10db0bb6beb3ed.tar.gz workbench-b9624be0c341ad070869497d2d10db0bb6beb3ed.tar.bz2 workbench-b9624be0c341ad070869497d2d10db0bb6beb3ed.zip |
0.2.0
-rw-r--r-- | build.sbt | 2 | ||||
-rw-r--r-- | client/src/main/scala/workbench/WorkbenchClient.scala | 28 | ||||
-rw-r--r-- | example/src/main/scala/example/ScalaJSExample.scala | 2 | ||||
-rw-r--r-- | readme.md | 15 | ||||
-rw-r--r-- | shared/main/scala/workbench/Shared.scala | 30 | ||||
-rw-r--r-- | src/main/scala/workbench/Server.scala | 38 |
6 files changed, 87 insertions, 28 deletions
@@ -9,7 +9,7 @@ val defaultSettings = Seq( lazy val root = project.in(file(".")).settings(defaultSettings:_*).settings( name := "workbench", - version := "0.1.6", + version := "0.2.0", organization := "com.lihaoyi", scalaVersion := "2.10.4", sbtPlugin := true, diff --git a/client/src/main/scala/workbench/WorkbenchClient.scala b/client/src/main/scala/workbench/WorkbenchClient.scala index 662e797..9fc5b89 100644 --- a/client/src/main/scala/workbench/WorkbenchClient.scala +++ b/client/src/main/scala/workbench/WorkbenchClient.scala @@ -6,24 +6,24 @@ import upickle.{Reader, Writer, Js} import scala.scalajs.js import scala.scalajs.js.annotation.JSExport import scalajs.concurrent.JSExecutionContext.Implicits.runNow -object WireServer extends autowire.Server[Js.Value, upickle.Reader, upickle.Writer]{ - def write[Result: Writer](r: Result) = upickle.writeJs(r) - def read[Result: Reader](p: Js.Value) = upickle.readJs[Result](p) +/** + * The connection from workbench server to the client + */ +object Wire extends autowire.Server[Js.Value, upickle.Reader, upickle.Writer] with ReadWrite{ def wire(parsed: Js.Arr): Unit = { val Js.Arr(path, args: Js.Obj) = parsed - - val req = new Request( - upickle.readJs[Seq[String]](path), - args.value.toMap - ) - WireServer.route[Api](WorkbenchClient).apply(req) + val req = new Request(upickle.readJs[Seq[String]](path), args.value.toMap) + Wire.route[Api](WorkbenchClient).apply(req) } } @JSExport object WorkbenchClient extends Api{ + @JSExport val shadowBody = dom.document.body.cloneNode(deep = true) + @JSExport var interval = 1000 + @JSExport var success = false @JSExport def main(bootSnippet: String, host: String, port: Int): Unit = { @@ -35,7 +35,7 @@ object WorkbenchClient extends Api{ json.read(data.responseText) .asInstanceOf[Js.Arr] .value - .foreach(v => WireServer.wire(v.asInstanceOf[Js.Arr])) + .foreach(v => Wire.wire(v.asInstanceOf[Js.Arr])) main(bootSnippet, host, port) case util.Failure(e) => if (!success) println("Workbench disconnected " + e) @@ -44,7 +44,7 @@ object WorkbenchClient extends Api{ dom.setTimeout(() => main(bootSnippet, host, port), interval) } } - + @JSExport override def clear(): Unit = { dom.document.asInstanceOf[js.Dynamic].body = shadowBody.cloneNode(true) for(i <- 0 until 100000){ @@ -52,12 +52,12 @@ object WorkbenchClient extends Api{ dom.clearInterval(i) } } - + @JSExport override def reload(): Unit = { dom.console.log("Reloading page...") dom.location.reload() } - + @JSExport override def run(path: String, bootSnippet: Option[String]): Unit = { val tag = dom.document.createElement("script") var loaded = false @@ -75,7 +75,7 @@ object WorkbenchClient extends Api{ } dom.document.head.appendChild(tag) } - + @JSExport override def print(level: String, msg: String): Unit = { level match { case "error" => dom.console.error(msg) diff --git a/example/src/main/scala/example/ScalaJSExample.scala b/example/src/main/scala/example/ScalaJSExample.scala index bab193b..7a41c44 100644 --- a/example/src/main/scala/example/ScalaJSExample.scala +++ b/example/src/main/scala/example/ScalaJSExample.scala @@ -22,7 +22,7 @@ object ScalaJSExample { val corners = Seq(Point(255, 255), Point(0, 255), Point(128, 0)) def clear() = { - ctx.fillStyle = "black" + ctx.fillStyle = "white" ctx.fillRect(0, 0, 255, 255) } @@ -20,7 +20,7 @@ resolvers += "spray repo" at "http://repo.spray.io" resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" -addSbtPlugin("com.lihaoyi" % "workbench" % "0.1.5") +addSbtPlugin("com.lihaoyi" % "workbench" % "0.2.0") ``` - Add to your `build.sbt` ```scala @@ -83,6 +83,19 @@ You can force the clean-up-and-reboot to happen from the browser via the shortcu With this done, you should be receiving the SBT logspam (compilation, warnings, errors) in your browse console, and the page should be automatically refreshing/updating when the application gets recompiled. If you have problems setting this up, try starting from the [example app](https://github.com/lihaoyi/workbench-example-app) and working from there. + +# Development + +To develop, go into `example/` and run `sbt ~fastOptJS`. Then you can go to + +``` +http://localhost:12345/target/scala-2.11/classes/index-dev.html +``` + +and see a small sierpinski-triangle application. Editing the code within `example/` should cause the SBT log-spam to appear in the browser console, and changes (e.g. changing the color of the background fill) should cause a recompile and updating of the browser animation. + +To make changes to workbench, modify the workbench source code and stop/re-run `sbt ~fastOptJS`. When workbench finishes re-compiling, SBT re-starts and the page becomes accessible, your changes to workbench will take effect. You can replace `fullOptJs` in the `built.sbt` file with `fastOptJS`, and swapping the reference to `client-opt.js` to `client-fastopt.js`, if you want to speed up the development cycle. + Pull requests welcome! License diff --git a/shared/main/scala/workbench/Shared.scala b/shared/main/scala/workbench/Shared.scala index da202b4..f9a1434 100644 --- a/shared/main/scala/workbench/Shared.scala +++ b/shared/main/scala/workbench/Shared.scala @@ -1,8 +1,38 @@ package com.lihaoyi.workbench +import upickle.{Js, Reader, Writer} + +/** + * A standard way to read and write `Js.Value`s with autowire/upickle + */ +trait ReadWrite{ + def write[Result: Writer](r: Result) = upickle.writeJs(r) + def read[Result: Reader](p: Js.Value) = upickle.readJs[Result](p) +} + +/** + * Shared API between the workbench server and the workbench client, + * comprising methods the server can call on the client to make it do + * things + */ trait Api{ + /** + * Reset the HTML page to its initial state + */ def clear(): Unit + /** + * Reload the entire webpage + */ def reload(): Unit + + /** + * Print a `msg` with the given logging `level` + */ def print(level: String, msg: String): Unit + + /** + * Execute the javascript file available at the given `path`. Optionally, + * run a `bootSnippet` after the file has been executed. + */ def run(path: String, bootSnippet: Option[String]): Unit }
\ No newline at end of file diff --git a/src/main/scala/workbench/Server.scala b/src/main/scala/workbench/Server.scala index bec88b6..4d37073 100644 --- a/src/main/scala/workbench/Server.scala +++ b/src/main/scala/workbench/Server.scala @@ -18,17 +18,28 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA config = ConfigFactory.load(ActorSystem.getClass.getClassLoader), classLoader = ActorSystem.getClass.getClassLoader ) - object Wire extends autowire.Client[Js.Value, upickle.Reader, upickle.Writer]{ + + /** + * The connection from workbench server to the client + */ + object Wire extends autowire.Client[Js.Value, upickle.Reader, upickle.Writer] with ReadWrite{ def doCall(req: Request): Future[Js.Value] = { - pubSub ! Js.Arr(upickle.writeJs(req.path), Js.Obj(req.args.toSeq:_*)) + longPoll ! Js.Arr(upickle.writeJs(req.path), Js.Obj(req.args.toSeq:_*)) Future.successful(Js.Null) } - def write[Result: Writer](r: Result) = upickle.writeJs(r) - def read[Result: Reader](p: Js.Value) = upickle.readJs[Result](p) } - private val pubSub = actor(new Actor{ + + /** + * Actor meant to handle long polling, buffering messages or waiting actors + */ + private val longPoll = actor(new Actor{ var waitingActor: Option[ActorRef] = None var queuedMessages = List[Js.Value]() + + /** + * Flushes returns nothing to any waiting actor every so often, + * to prevent the connection from living too long. + */ case object Clear import system.dispatcher @@ -53,12 +64,19 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA case (msg: Js.Arr, Some(a), Nil) => respond(a, upickle.json.write(Js.Arr(msg))) waitingActor = None - case (Clear, Some(a), Nil) => - respond(a, upickle.json.write(Js.Arr())) + case (Clear, waiting, Nil) => + waiting.foreach(respond(_, upickle.json.write(Js.Arr()))) waitingActor = None } }) + /** + * Simple spray server: + * + * - /workbench.js is hardcoded to be the workbench javascript client + * - Any other GET request just pulls from the local filesystem + * - POSTs to /notifications get routed to the longPoll actor + */ startServer(url, port) { get { path("workbench.js") { @@ -79,11 +97,9 @@ class Server(url: String, port: Int, bootSnippet: String) extends SimpleRoutingA } ~ post { path("notifications") { ctx => - pubSub ! ctx.responder + longPoll ! ctx.responder } } } - def kill() = { - system.shutdown() - } + def kill() = system.shutdown() }
\ No newline at end of file |