<commit_before>package korolev

import java.io.{File, RandomAccessFile}
import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicInteger

import bridge.JSAccess
import korolev.ApplicationContext._
import korolev.Async.AsyncOps
import korolev.Component.{ComponentInstance, EventRegistry, Frontend}
import levsha.events.calculateEventPropagation
import levsha.impl.DiffRenderContext
import levsha.impl.DiffRenderContext.ChangesPerformer
import levsha.{Document, Id, XmlNs}
import slogging.LazyLogging

import scala.util.{Failure, Success, Try}
import korolev.util.Scheduler

abstract class Korolev[F[+ _]: Async, S, M] {
  def jsAccess: JSAccess[F]
  def resolveFormData(descriptor: String, formData: Try[FormData]): Unit
  def topLevelComponentInstance: ComponentInstance[F, S, M, S, Any, M]
}

object Korolev {

  def apply[F[+ _]: Async, S, M](identifier: String,
                                 ja: JSAccess[F],
                                 topLevelInitialState: S,
                                 stateReaderOpt: Option[StateReader],
                                 render: PartialFunction[S, Document.Node[ApplicationContext.Effect[F, S, M]]],
                                 router: Router[F, S, S],
                                 fromScratch: Boolean)(implicit scheduler: Scheduler[F]): Korolev[F, S, M] =
    new Korolev[F, S, M] with LazyLogging {

      val async = Async[F]
      val devMode = new RenderContextDevMode(identifier, fromScratch)
      val jsAccess = ja
      val client = {
        // Prepare frontend
        jsAccess.global.getAndSaveAs("Korolev", "@Korolev")
        jsAccess.obj("@Korolev")
      }
      val topLevelComponentInstance = {
        val renderer = render.lift
        val frontend = new Frontend[F] {
          def listenEvent(name: String, preventDefault: Boolean): F[Unit] =
            client.callAndFlush("ListenEvent", name, preventDefault)
          def uploadForm(id: Id, descriptor: String): F[Unit] =
            client.callAndFlush("UploadForm", id.mkString, descriptor)
          def focus(id: Id): F[Unit] =
            client.callAndFlush("Focus", id.mkString)
          def setAttr[T](id: Id, xmlNs: String, name: String, value: T, isProperty: Boolean): F[Unit] =
            client.callAndFlush("SetAttr", id.mkString, xmlNs, name, value, isProperty)
          def extractProperty[T](id: Id, name: String): F[T] =
            client.callAndFlush("ExtractProperty", id.mkString, name)
        }
        val eventRegistry = new EventRegistry[F](frontend)
        val initialState = stateReaderOpt.flatMap(_.read[S](Id.TopLevel)).getOrElse(topLevelInitialState)
        val component = new Component[F, S, Any, M](initialState, Component.TopLevelComponentId) {
          def render(parameters: Any, state: S): Document.Node[Effect[F, S, M]] = {
            renderer(state).getOrElse {
              Document.Node[Effect[F, S, M]] { rc =>
                // TODO better reporting
                // TODO should be safe!
                rc.openNode(XmlNs.html, "body")
                rc.addTextNode("Render didn't defined for this state")
                rc.closeNode("body")
              }
            }
          }
        }
        new ComponentInstance[F, S, M, S, Any, M](Id.TopLevel, frontend, eventRegistry, component)
      }
      val renderContext = DiffRenderContext[Effect[F, S, M]](savedBuffer = devMode.loadRenderContext())

      val changesPerformer = new ChangesPerformer {
        private def isProp(name: String) = name.charAt(0) == '^'
        private def escapeName(name: String, isProp: Boolean) =
          if (isProp) name.substring(1) else name

        def remove(id: Id): Unit =
          client.call("Remove", id.parent.get.mkString, id.mkString).runIgnoreResult()
        def createText(id: Id, text: String): Unit =
          client.call("CreateText", id.parent.get.mkString, id.mkString, text).runIgnoreResult()
        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
          client.call("Create", parent, id.mkString, pXmlns, tag).runIgnoreResult()
        }
        def setAttr(id: Id, xmlNs: String, name: String, value: String): Unit = {
          val p = isProp(name)
          val n = escapeName(name, p)
          val pXmlns = if (xmlNs eq levsha.XmlNs.html.uri) 0 else xmlNs
          client.call("SetAttr", id.mkString, pXmlns, n, value, p).runIgnoreResult()
        }
        def removeAttr(id: Id, xmlNs: String, name: String): Unit = {
          val p = isProp(name)
          val n = escapeName(name, p)
          val pXmlns = if (xmlNs eq levsha.XmlNs.html.uri) 0 else xmlNs
          client.call("RemoveAttr", id.mkString, pXmlns, n, p).runIgnoreResult()
        }
      }

      def resolveFormData(descriptor: String, formData: Try[FormData]): Unit = {
        topLevelComponentInstance.resolveFormData(descriptor, formData)
      }

      val currentRenderNum = new AtomicInteger(0)

      val initialization = async sequence {
        Seq(
          // History callback
          jsAccess.registerCallbackAndFlush[String] { pathString =>
            val path = Router.Path.fromString(pathString)
            val maybeState = router.toState.lift((topLevelComponentInstance.getState, path))
            maybeState foreach { asyncState =>
              asyncState run {
                case Success(newState) =>
                  topLevelComponentInstance.setState(newState)
                case Failure(e) =>
                  logger.error("Error occurred when updating state", e)
              }
            }
          } flatMap { historyCallback =>
            client.callAndFlush[Unit]("RegisterHistoryHandler", historyCallback)
          },
          // Event callback
          jsAccess.registerCallbackAndFlush[String] { targetAndType =>
            val Array(renderNum, target, tpe) = targetAndType.split(':')
            if (currentRenderNum.get == renderNum.toInt) {
              calculateEventPropagation(Id(target), tpe) forall { eventId =>
                topLevelComponentInstance.applyEvent(eventId)
              }
              ()
            }
          } flatMap { eventCallback =>
            client.callAndFlush[Unit]("RegisterGlobalEventHandler", eventCallback)
          },
          // FormData progress callback
          jsAccess.registerCallbackAndFlush[String] { descriptorLoadedTotal =>
            val Array(descriptor, loaded, total) = descriptorLoadedTotal.split(':')
            topLevelComponentInstance.handleFormDataProgress(descriptor, loaded.toInt, total.toInt)
          } flatMap { callback =>
            client.callAndFlush[Unit]("RegisterFormDataProgressHandler", callback)
          },
          client.callAndFlush[Unit]("SetRenderNum", 0),
          if (fromScratch) client.callAndFlush("CleanRoot") else async.unit,
          if (devMode.isActive) client.callAndFlush("ReloadCss") else async.unit
        )
      }

      async.run(initialization) {
        case Success(_) =>
          logger.trace("Korolev initialization complete")

          // Perform initial rendering
          if (fromScratch) {
            renderContext.openNode(levsha.XmlNs.html, "body")
            renderContext.closeNode("body")
            renderContext.diff(DiffRenderContext.DummyChangesPerformer)
          } else {
            if (devMode.hasSavedRenderContext) {
              renderContext.swap()
              topLevelComponentInstance.applyRenderContext((), renderContext, stateReaderOpt)
              renderContext.diff(changesPerformer)
              devMode.saveRenderContext(renderContext)
            } else {
              topLevelComponentInstance.applyRenderContext((), renderContext, stateReaderOpt)
              renderContext.diff(DiffRenderContext.DummyChangesPerformer)
              if (devMode.isActive) devMode.saveRenderContext(renderContext)
            }
          }

          def onState(giveStateReader: Boolean) = {
            // Set page url if router exists
            router.fromState
              .lift(topLevelComponentInstance.getState)
              .foreach(path => client.call("ChangePageUrl", path.toString))

            // Prepare render context
            renderContext.swap()

            // Reset all event handlers delays and elements
            topLevelComponentInstance.prepare()

            // Perform rendering
            topLevelComponentInstance.applyRenderContext(
              parameters = (), // Boxed unit as parameter. Top level component doesn't need parameters
              stateReaderOpt = if (giveStateReader) stateReaderOpt else None,
              rc = renderContext)

            // Infer changes
            renderContext.diff(changesPerformer)
            if (devMode.isActive) devMode.saveRenderContext(renderContext)

            // Make korolev ready to next render
            topLevelComponentInstance.dropObsoleteMisc()
            client.call("SetRenderNum", currentRenderNum.incrementAndGet()).runIgnoreResult()
            jsAccess.flush()
          }

          topLevelComponentInstance.subscribeStateChange((_, _) => onState(giveStateReader = false))
          if (fromScratch) onState(giveStateReader = true)
          else jsAccess.flush()
        case Failure(e) =>
          logger.error("Error occurred on event callback registration", e)
      }
    }

  class RenderContextDevMode(identifier: String, fromScratch: Boolean) {

    lazy val file = new File(DevMode.renderStateDirectory, identifier)

    lazy val hasSavedRenderContext = DevMode.isActive && file.exists && !fromScratch

    def isActive = DevMode.isActive

    def loadRenderContext() = if (hasSavedRenderContext) {
      val nioFile = new RandomAccessFile(file, "r")
      val channel = nioFile.getChannel
      try {
        val buffer = ByteBuffer.allocate(channel.size.toInt)
        channel.read(buffer)
        buffer.position(0)
        Some(buffer)
      } finally {
        nioFile.close()
        channel.close()
      }
    } else {
      None
    }

    def saveRenderContext(renderContext: DiffRenderContext[_]) = {
      val nioFile = new RandomAccessFile(file, "rw")
      val channel = nioFile.getChannel
      try {
        val buffer = renderContext.save()
        channel.write(buffer)
      } finally {
        nioFile.close()
        channel.close()
      }
    }
  }
}
<commit_msg>#162 Add render lock<commit_after>package korolev

import java.io.{File, RandomAccessFile}
import java.nio.ByteBuffer
import java.util.concurrent.atomic.AtomicInteger

import bridge.JSAccess
import korolev.ApplicationContext._
import korolev.Async.AsyncOps
import korolev.Component.{ComponentInstance, EventRegistry, Frontend}
import levsha.events.calculateEventPropagation
import levsha.impl.DiffRenderContext
import levsha.impl.DiffRenderContext.ChangesPerformer
import levsha.{Document, Id, XmlNs}
import slogging.LazyLogging

import scala.util.{Failure, Success, Try}
import korolev.util.Scheduler

abstract class Korolev[F[+ _]: Async, S, M] {
  def jsAccess: JSAccess[F]
  def resolveFormData(descriptor: String, formData: Try[FormData]): Unit
  def topLevelComponentInstance: ComponentInstance[F, S, M, S, Any, M]
}

object Korolev {

  def apply[F[+ _]: Async, S, M](identifier: String,
                                 ja: JSAccess[F],
                                 topLevelInitialState: S,
                                 stateReaderOpt: Option[StateReader],
                                 render: PartialFunction[S, Document.Node[ApplicationContext.Effect[F, S, M]]],
                                 router: Router[F, S, S],
                                 fromScratch: Boolean)(implicit scheduler: Scheduler[F]): Korolev[F, S, M] =
    new Korolev[F, S, M] with LazyLogging {

      val async = Async[F]
      val devMode = new RenderContextDevMode(identifier, fromScratch)
      val jsAccess = ja
      val client = {
        // Prepare frontend
        jsAccess.global.getAndSaveAs("Korolev", "@Korolev")
        jsAccess.obj("@Korolev")
      }
      val topLevelComponentInstance = {
        val renderer = render.lift
        val frontend = new Frontend[F] {
          def listenEvent(name: String, preventDefault: Boolean): F[Unit] =
            client.callAndFlush("ListenEvent", name, preventDefault)
          def uploadForm(id: Id, descriptor: String): F[Unit] =
            client.callAndFlush("UploadForm", id.mkString, descriptor)
          def focus(id: Id): F[Unit] =
            client.callAndFlush("Focus", id.mkString)
          def setAttr[T](id: Id, xmlNs: String, name: String, value: T, isProperty: Boolean): F[Unit] =
            client.callAndFlush("SetAttr", id.mkString, xmlNs, name, value, isProperty)
          def extractProperty[T](id: Id, name: String): F[T] =
            client.callAndFlush("ExtractProperty", id.mkString, name)
        }
        val eventRegistry = new EventRegistry[F](frontend)
        val initialState = stateReaderOpt.flatMap(_.read[S](Id.TopLevel)).getOrElse(topLevelInitialState)
        val component = new Component[F, S, Any, M](initialState, Component.TopLevelComponentId) {
          def render(parameters: Any, state: S): Document.Node[Effect[F, S, M]] = {
            renderer(state).getOrElse {
              Document.Node[Effect[F, S, M]] { rc =>
                // TODO better reporting
                // TODO should be safe!
                rc.openNode(XmlNs.html, "body")
                rc.addTextNode("Render didn't defined for this state")
                rc.closeNode("body")
              }
            }
          }
        }
        new ComponentInstance[F, S, M, S, Any, M](Id.TopLevel, frontend, eventRegistry, component)
      }
      val renderContext = DiffRenderContext[Effect[F, S, M]](savedBuffer = devMode.loadRenderContext())

      val changesPerformer = new ChangesPerformer {
        private def isProp(name: String) = name.charAt(0) == '^'
        private def escapeName(name: String, isProp: Boolean) =
          if (isProp) name.substring(1) else name

        def remove(id: Id): Unit =
          client.call("Remove", id.parent.get.mkString, id.mkString).runIgnoreResult()
        def createText(id: Id, text: String): Unit =
          client.call("CreateText", id.parent.get.mkString, id.mkString, text).runIgnoreResult()
        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
          client.call("Create", parent, id.mkString, pXmlns, tag).runIgnoreResult()
        }
        def setAttr(id: Id, xmlNs: String, name: String, value: String): Unit = {
          val p = isProp(name)
          val n = escapeName(name, p)
          val pXmlns = if (xmlNs eq levsha.XmlNs.html.uri) 0 else xmlNs
          client.call("SetAttr", id.mkString, pXmlns, n, value, p).runIgnoreResult()
        }
        def removeAttr(id: Id, xmlNs: String, name: String): Unit = {
          val p = isProp(name)
          val n = escapeName(name, p)
          val pXmlns = if (xmlNs eq levsha.XmlNs.html.uri) 0 else xmlNs
          client.call("RemoveAttr", id.mkString, pXmlns, n, p).runIgnoreResult()
        }
      }

      def resolveFormData(descriptor: String, formData: Try[FormData]): Unit = {
        topLevelComponentInstance.resolveFormData(descriptor, formData)
      }

      val currentRenderNum = new AtomicInteger(0)

      val initialization = async sequence {
        Seq(
          // History callback
          jsAccess.registerCallbackAndFlush[String] { pathString =>
            val path = Router.Path.fromString(pathString)
            val maybeState = router.toState.lift((topLevelComponentInstance.getState, path))
            maybeState foreach { asyncState =>
              asyncState run {
                case Success(newState) =>
                  topLevelComponentInstance.setState(newState)
                case Failure(e) =>
                  logger.error("Error occurred when updating state", e)
              }
            }
          } flatMap { historyCallback =>
            client.callAndFlush[Unit]("RegisterHistoryHandler", historyCallback)
          },
          // Event callback
          jsAccess.registerCallbackAndFlush[String] { targetAndType =>
            val Array(renderNum, target, tpe) = targetAndType.split(':')
            if (currentRenderNum.get == renderNum.toInt) {
              calculateEventPropagation(Id(target), tpe) forall { eventId =>
                topLevelComponentInstance.applyEvent(eventId)
              }
              ()
            }
          } flatMap { eventCallback =>
            client.callAndFlush[Unit]("RegisterGlobalEventHandler", eventCallback)
          },
          // FormData progress callback
          jsAccess.registerCallbackAndFlush[String] { descriptorLoadedTotal =>
            val Array(descriptor, loaded, total) = descriptorLoadedTotal.split(':')
            topLevelComponentInstance.handleFormDataProgress(descriptor, loaded.toInt, total.toInt)
          } flatMap { callback =>
            client.callAndFlush[Unit]("RegisterFormDataProgressHandler", callback)
          },
          client.callAndFlush[Unit]("SetRenderNum", 0),
          if (fromScratch) client.callAndFlush("CleanRoot") else async.unit,
          if (devMode.isActive) client.callAndFlush("ReloadCss") else async.unit
        )
      }

      async.run(initialization) {
        case Success(_) =>
          logger.trace("Korolev initialization complete")

          // Perform initial rendering
          if (fromScratch) {
            renderContext.openNode(levsha.XmlNs.html, "body")
            renderContext.closeNode("body")
            renderContext.diff(DiffRenderContext.DummyChangesPerformer)
          } else {
            if (devMode.hasSavedRenderContext) {
              renderContext.swap()
              topLevelComponentInstance.applyRenderContext((), renderContext, stateReaderOpt)
              renderContext.diff(changesPerformer)
              devMode.saveRenderContext(renderContext)
            } else {
              topLevelComponentInstance.applyRenderContext((), renderContext, stateReaderOpt)
              renderContext.diff(DiffRenderContext.DummyChangesPerformer)
              if (devMode.isActive) devMode.saveRenderContext(renderContext)
            }
          }

          def onState(giveStateReader: Boolean) = renderContext.synchronized {
            // Set page url if router exists
            router.fromState
              .lift(topLevelComponentInstance.getState)
              .foreach(path => client.call("ChangePageUrl", path.toString))

            // Prepare render context
            renderContext.swap()

            // Reset all event handlers delays and elements
            topLevelComponentInstance.prepare()

            // Perform rendering
            topLevelComponentInstance.applyRenderContext(
              parameters = (), // Boxed unit as parameter. Top level component doesn't need parameters
              stateReaderOpt = if (giveStateReader) stateReaderOpt else None,
              rc = renderContext)

            // Infer changes
            renderContext.diff(changesPerformer)
            if (devMode.isActive) devMode.saveRenderContext(renderContext)

            // Make korolev ready to next render
            topLevelComponentInstance.dropObsoleteMisc()
            client.call("SetRenderNum", currentRenderNum.incrementAndGet()).runIgnoreResult()
            jsAccess.flush()
          }

          topLevelComponentInstance.subscribeStateChange((_, _) => onState(giveStateReader = false))
          if (fromScratch) onState(giveStateReader = true)
          else jsAccess.flush()
        case Failure(e) =>
          logger.error("Error occurred on event callback registration", e)
      }
    }

  class RenderContextDevMode(identifier: String, fromScratch: Boolean) {

    lazy val file = new File(DevMode.renderStateDirectory, identifier)

    lazy val hasSavedRenderContext = DevMode.isActive && file.exists && !fromScratch

    def isActive = DevMode.isActive

    def loadRenderContext() = if (hasSavedRenderContext) {
      val nioFile = new RandomAccessFile(file, "r")
      val channel = nioFile.getChannel
      try {
        val buffer = ByteBuffer.allocate(channel.size.toInt)
        channel.read(buffer)
        buffer.position(0)
        Some(buffer)
      } finally {
        nioFile.close()
        channel.close()
      }
    } else {
      None
    }

    def saveRenderContext(renderContext: DiffRenderContext[_]) = {
      val nioFile = new RandomAccessFile(file, "rw")
      val channel = nioFile.getChannel
      try {
        val buffer = renderContext.save()
        channel.write(buffer)
      } finally {
        nioFile.close()
        channel.close()
      }
    }
  }
}

{'fraction_non_alphanumeric': 0.10093582887700535, 'fraction_numerical': 0.001002673796791444, 'mean_word_length': 2.3447780261897155, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 33, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 22}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1551399', 'n_tokens_mistral': 5767, 'n_tokens_neox': 5632, 'n_words': 1268}