summaryrefslogblamecommitdiff
path: root/src/main/scala/workbench/Server.scala
blob: 5953fcb9a6720f9268f5d612f2c24dcd634f7b00 (plain) (tree)
1
2
3
4
5
6
7
8
9


                                                
                           
                                        
                       
                                

                                     
 

                                       
                                                        

                                                           
                              
                      

                                           



                                           


                                                 
                                                               

                               
 
                                                              







                                                                                                                                                    
                                    
                       


                                                                     



                                                       
                                                                               
                                                  
                                                                                     

                                
   




                                                                             

                                             




                                                                   


                            
                                                                 


                                           
                             






                                                                       
 
                                       
                                                       
                            
 
                                       
                                    
 

                                                   
                           
 

                                                                            



                           






                                                                       



                            






                                                                        
                                                                                                                       

                       

         
                           
 
       

                                    
                                
       
     
   
                                
 
 
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()

}