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

korolev.internal.ClientSideApi.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2018 Aleksey Fomkin
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package korolev.internal

import java.util.concurrent.atomic.AtomicInteger

import korolev.{Async, Reporter, Router}
import korolev.Async._
import korolev.Router.Path
import levsha.Id
import levsha.impl.DiffRenderContext.ChangesPerformer

import scala.annotation.switch
import scala.collection.concurrent.TrieMap
import scala.collection.mutable
import scala.util.{Failure, Success}

/**
  * Typed interface to client side
  */
final class ClientSideApi[F[_]: Async](connection: Connection[F], reporter: Reporter)
  extends ChangesPerformer {

  import ClientSideApi._

  private val lastDescriptor = new AtomicInteger(0)
  private val async = Async[F]
  private val promises = TrieMap.empty[String, Promise[F, String]]
  private val domChangesBuffer = mutable.ArrayBuffer.empty[Any]

  private var onHistory: HistoryCallback = _
  private var onFormDataProgress: FormDataProgressCallback = _
  private var onEvent: EventCallback = _

  private def isProperty(name: String) =
    name.charAt(0) == '^'

  private def isStyle(name: String) =
    name.charAt(0) == '*'

  private def escapeName(name: String, truncate: Boolean) =
    if (truncate) name.substring(1)
    else name

  /**
    * @param onEvent            (renderNum, target, eventType) => Unit
    * @param onFormDataProgress (descriptor, loaded, total)
    */
  def setHandlers(onHistory: HistoryCallback,
                  onEvent: EventCallback,
                  onFormDataProgress: FormDataProgressCallback): Unit = {
    this.onHistory = onHistory
    this.onEvent = onEvent
    this.onFormDataProgress = onFormDataProgress
  }

  def listenEvent(name: String, preventDefault: Boolean): Unit =
    connection.send(Procedure.ListenEvent.code, name, preventDefault)

  def uploadForm(id: Id, descriptor: String): Unit =
    connection.send(Procedure.UploadForm.code, id.mkString, descriptor)

  def uploadFiles(id: Id, descriptor: String): Unit =
    connection.send(Procedure.UploadFiles.code, id.mkString, descriptor)

  def focus(id: Id): Unit =
    connection.send(Procedure.Focus.code, id.mkString)

  def extractProperty(id: Id, name: String): F[String] = {
    val descriptor = lastDescriptor.getAndIncrement().toString
    val promise = async.promise[String]
    promises.put(descriptor, promise)
    connection.send(Procedure.ExtractProperty.code, descriptor, id.mkString, name)
    promise.async
  }

  def setProperty(id: Id, name: Symbol, value: Any): Unit = {
    // TODO setProperty should be dedicated
    connection.send(Procedure.ModifyDom.code, ModifyDomProcedure.SetAttr.code, id.mkString, 0, name.name, value, true)
  }

  def evalJs(code: String): F[String] = {
    val descriptor = lastDescriptor.getAndIncrement().toString
    val promise = async.promise[String]
    promises.put(descriptor, promise)
    connection.send(Procedure.EvalJs.code, descriptor, code)
    promise.async
  }

  def changePageUrl(path: Path): Unit =
    connection.send(Procedure.ChangePageUrl.code, path.toString)

  def setRenderNum(i: Int): Unit =
    connection.send(Procedure.SetRenderNum.code, i)

  def cleanRoot(): Unit =
    connection.send(Procedure.CleanRoot.code)

  def reloadCss(): Unit =
    connection.send(Procedure.ReloadCss.code)

  def extractEventData(renderNum: Int): F[String] = {
    val descriptor = lastDescriptor.getAndIncrement().toString
    val promise = async.promise[String]
    promises.put(descriptor, promise)
    connection.send(Procedure.ExtractEventData.code, descriptor, renderNum)
    promise.async
  }

  def startDomChanges(): Unit = {
    domChangesBuffer.append(Procedure.ModifyDom.code)
  }

  def flushDomChanges(): Unit = {
    connection.send(domChangesBuffer: _*)
    domChangesBuffer.clear()
  }

  def remove(id: Id): Unit =
    domChangesBuffer.append(ModifyDomProcedure.Remove.code, id.parent.get.mkString, id.mkString)

  def createText(id: Id, text: String): Unit =
    domChangesBuffer.append(ModifyDomProcedure.CreateText.code, id.parent.get.mkString, id.mkString, text)

  def create(id: Id, xmlNs: String, tag: String): Unit = {
    val parent = id.parent.fold("0")(_.mkString)
    val pXmlns =
      if (xmlNs eq levsha.XmlNs.html.uri) 0
      else xmlNs
    domChangesBuffer.append(ModifyDomProcedure.Create.code, parent, id.mkString, pXmlns, tag)
  }

  def setAttr(id: Id, xmlNs: String, name: String, value: String): Unit = {
    if (isStyle(name)) {
      val n = escapeName(name, truncate = true)
      domChangesBuffer.append(ModifyDomProcedure.SetStyle.code, id.mkString, n, value)
    } else {
      val p = isProperty(name)
      val n = escapeName(name, p)
      val pXmlns =
        if (xmlNs eq levsha.XmlNs.html.uri) 0
        else xmlNs
      domChangesBuffer.append(ModifyDomProcedure.SetAttr.code, id.mkString, pXmlns, n, value, p)
    }
  }

  def removeAttr(id: Id, xmlNs: String, name: String): Unit = {
    if (isStyle(name)) {
      val n = escapeName(name, truncate = true)
      domChangesBuffer.append(ModifyDomProcedure.RemoveStyle.code, id.mkString, n)
    } else {
      val p = isProperty(name)
      val n = escapeName(name, p)
      val pXmlns =
        if (xmlNs eq levsha.XmlNs.html.uri) 0
        else xmlNs
      domChangesBuffer.append(ModifyDomProcedure.RemoveAttr.code, id.mkString, pXmlns, n, p)
    }
  }

  private def unescapeJsonString(s: String): String = {
    val sb = StringBuilder.newBuilder
    var i = 1
    val len = s.length - 1
    while (i < len) {
      val c = s.charAt(i)
      var charsConsumed = 0
      if (c != '\\') {
        charsConsumed = 1
        sb.append(c)
      } else {
        charsConsumed = 2
        (s.charAt(i + 1): @switch) match {
          case '\\' => sb.append('\\')
          case '"' => sb.append('"')
          case 'b' => sb.append('\b')
          case 'f' => sb.append('\f')
          case 'n' => sb.append('\n')
          case 'r' => sb.append('\r')
          case 't' => sb.append('\t')
          case 'u' =>
            val code = s.substring(i + 2, i + 6)
            charsConsumed = 6
            sb.append(Integer.parseInt(code, 16).toChar)
        }
      }
      i += charsConsumed
    }
    sb.result()
  }

  private def onReceive(): Unit = connection.received.run {
    case Success(json) =>
      val tokens = json
        .substring(1, json.length - 1) // remove brackets
        .split(",", 2) // split to tokens
      val callbackType = tokens(0)
      val args =
        if (tokens.length > 1) unescapeJsonString(tokens(1))
        else ""

      callbackType.toInt match {
        case CallbackType.DomEvent.code =>
          val Array(renderNum, target, tpe) = args.split(':')
          onEvent(renderNum.toInt, Id(target), tpe)
        case CallbackType.FormDataProgress.code =>
          val Array(descriptor, loaded, total) = args.split(':')
          onFormDataProgress(descriptor, loaded.toInt, total.toInt)
        case CallbackType.ExtractPropertyResponse.code =>
          val Array(descriptor, propertyType, value) = args.split(":", 3)
          val result = propertyType.toInt match {
            case PropertyType.Error.code => Failure(ClientSideException(value))
            case _ => Success(value)
          }
          promises
            .remove(descriptor)
            .foreach(_.complete(result))
        case CallbackType.ExtractEventDataResponse.code =>
          val Array(descriptor, value) = args.split(":", 2)
          promises.remove(descriptor).foreach(_.complete(Success(value)))
        case CallbackType.History.code =>
          onHistory(Router.Path.fromString(args))
        case CallbackType.EvalJsResponse.code =>
          val Array(descriptor, status, json) = args.split(":", 3)
          promises
            .remove(descriptor)
            .foreach { promise =>
              status.toInt match {
                case EvalJsStatus.Success.code =>
                  promise.complete(Success(json))
                case EvalJsStatus.Failure.code =>
                  promise.complete(Failure(ClientSideException("JavaScript evaluation error")))
              }
            }
        case CallbackType.Heartbeat.code =>
          // ignore
      }
      onReceive()
    case Failure(e) =>
      reporter.error("Unable to receive message from client", e)
  }

  onReceive()
}

object ClientSideApi {

  type HistoryCallback = Path => Unit
  type EventCallback = (Int, Id, String) => Unit
  type FormDataProgressCallback = (String, Int, Int) => Unit

  sealed abstract class Procedure(final val code: Int)

  object Procedure {
    case object SetRenderNum extends Procedure(0) // (n)
    case object CleanRoot extends Procedure(1) // ()
    case object ListenEvent extends Procedure(2) // (type, preventDefault)
    case object ExtractProperty extends Procedure(3) // (id, propertyName, descriptor)
    case object ModifyDom extends Procedure(4) // (commands)
    case object Focus extends Procedure(5) // (id) {
    case object ChangePageUrl extends Procedure(6) // (path)
    case object UploadForm extends Procedure(7) // (id, descriptor)
    case object ReloadCss extends Procedure(8) // ()
    case object KeepAlive extends Procedure(9) // ()
    case object EvalJs extends Procedure(10) // (code)
    case object ExtractEventData extends Procedure(11) // (descriptor, renderNum)
    case object UploadFiles extends Procedure(12) // (id, descriptor)

    val All = Set(
      SetRenderNum, CleanRoot, ListenEvent, ExtractProperty,
      ModifyDom, Focus, ChangePageUrl, UploadForm, ReloadCss,
      KeepAlive, EvalJs, ExtractEventData
    )

    def apply(n: Int): Option[Procedure] =
      All.find(_.code == n)
  }

  sealed abstract class ModifyDomProcedure(final val code: Int)

  object ModifyDomProcedure {
    case object Create extends ModifyDomProcedure(0) // (id, childId, xmlNs, tag)
    case object CreateText extends ModifyDomProcedure(1) // (id, childId, text)
    case object Remove extends ModifyDomProcedure(2) // (id, childId)
    case object SetAttr extends ModifyDomProcedure(3) // (id, xmlNs, name, value, isProperty)
    case object RemoveAttr extends ModifyDomProcedure(4) // (id, xmlNs, name, isProperty)
    case object SetStyle extends ModifyDomProcedure(5) // (id, name, value)
    case object RemoveStyle extends ModifyDomProcedure(6) // (id, name)
  }

  sealed abstract class PropertyType(final val code: Int)

  object PropertyType {
    case object String extends PropertyType(0)
    case object Number extends PropertyType(1)
    case object Boolean extends PropertyType(2)
    case object Object extends PropertyType(3)
    case object Error extends PropertyType(4)
  }

  sealed abstract class EvalJsStatus(final val code: Int)

  object EvalJsStatus {
    case object Success extends EvalJsStatus(0)
    case object Failure extends EvalJsStatus(1)
  }

  sealed abstract class CallbackType(final val code: Int)

  object CallbackType {
    case object DomEvent extends CallbackType(0) // `$renderNum:$elementId:$eventType`
    case object FormDataProgress extends CallbackType(1) // `$descriptor:$loaded:$total`
    case object ExtractPropertyResponse extends CallbackType(2) // `$descriptor:$value`
    case object History extends CallbackType(3) // URL
    case object EvalJsResponse extends CallbackType(4) // `$descriptor:$status:$value`
    case object ExtractEventDataResponse extends CallbackType(5) // `$descriptor:$dataJson`
    case object Heartbeat extends CallbackType(6) // `$descriptor:$anyvalue`

    final val All = Set(DomEvent, FormDataProgress, ExtractPropertyResponse, History, EvalJsResponse, ExtractEventDataResponse, Heartbeat)

    def apply(n: Int): Option[CallbackType] =
      All.find(_.code == n)
  }

  case class ClientSideException(message: String) extends Exception(message)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy