summaryrefslogtreecommitdiff
path: root/src/main/scala/workbench/Server.scala
blob: 5953fcb9a6720f9268f5d612f2c24dcd634f7b00 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package com.lihaoyi.workbench

import akka.actor.{ActorRef, Actor, ActorSystem}
import akka.util.ByteString
import com.typesafe.config.ConfigFactory
import sbt.{Logger, IO}
import spray.httpx.encoding.Gzip
import spray.routing.SimpleRoutingApp
import akka.actor.ActorDSL._

import upickle.Js
import upickle.default.{Reader, Writer}
import spray.http.{HttpEntity, AllOrigins, HttpResponse}
import spray.http.HttpHeaders.`Access-Control-Allow-Origin`
import concurrent.duration._
import scala.concurrent.Future
import scala.io.Source
import org.scalajs.core.tools.io._
import org.scalajs.core.tools.logging.Level
import scala.tools.nsc
import scala.tools.nsc.Settings

import scala.tools.nsc.backend.JavaPlatform
import scala.tools.nsc.util.ClassPath.JavaContext
import scala.collection.mutable
import scala.tools.nsc.typechecker.Analyzer
import scala.tools.nsc.util.{JavaClassPath, DirectoryClassPath}
import spray.http.HttpHeaders._
import spray.http.HttpMethods._

class Server(url: String, port: Int) extends SimpleRoutingApp{
  val corsHeaders: List[ModeledHeader] =
    List(
      `Access-Control-Allow-Methods`(OPTIONS, GET, POST),
      `Access-Control-Allow-Origin`(AllOrigins),
      `Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent"),
      `Access-Control-Max-Age`(1728000)
    )

  implicit val system = ActorSystem(
    "Workbench-System",
    config = ConfigFactory.load(ActorSystem.getClass.getClassLoader),
    classLoader = ActorSystem.getClass.getClassLoader
  )

  /**
   * The connection from workbench server to the client
   */
  object Wire extends autowire.Client[Js.Value, Reader, Writer] with ReadWrite{
    def doCall(req: Request): Future[Js.Value] = {
      longPoll ! Js.Arr(upickle.default.writeJs(req.path), Js.Obj(req.args.toSeq:_*))
      Future.successful(Js.Null)
    }
  }

  /**
   * 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

    system.scheduler.schedule(0.seconds, 10.seconds, self, Clear)
    def respond(a: ActorRef, s: String) = {
      a ! HttpResponse(
        entity = s,
        headers = corsHeaders
      )
    }
    def receive = (x: Any) => (x, waitingActor, queuedMessages) match {
      case (a: ActorRef, _, Nil) =>
        // Even if there's someone already waiting,
        // a new actor waiting replaces the old one
        waitingActor = Some(a)

      case (a: ActorRef, None, msgs) =>
        respond(a, upickle.json.write(Js.Arr(msgs:_*)))
        queuedMessages = Nil

      case (msg: Js.Arr, None, msgs) =>
        queuedMessages = msg :: msgs

      case (msg: Js.Arr, Some(a), Nil) =>
        respond(a, upickle.json.write(Js.Arr(msg)))
        waitingActor = None

      case (Clear, waitingOpt, msgs) =>
        waitingOpt.foreach(respond(_, upickle.json.write(Js.Arr(msgs :_*))))
        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") {
        complete {
          val body = IO.readStream(
            getClass.getClassLoader.getResourceAsStream("client-opt.js")
          )
          s"""
          (function(){
            $body

            com.lihaoyi.workbench.WorkbenchClient().main(${upickle.default.write(url)}, ${upickle.default.write(port)})
          }).call(this)
          """
        }
      } ~
      getFromDirectory(".")

    } ~
    post {
      path("notifications") { ctx =>
        longPoll ! ctx.responder
      }
    }
  }
  def kill() = system.shutdown()

}