<commit_before>package korolev

import java.io.{ByteArrayOutputStream, InputStream}
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}

import korolev.Async._
import korolev.internal.{ApplicationInstance, Connection}
import korolev.state._
import levsha.Id
import slogging.LazyLogging

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

package object server extends LazyLogging {

  type StateStorage[F[+_], S] = korolev.state.StateStorage[F, S]
  val StateStorage = korolev.state.StateStorage

  type MimeTypes = String => Option[String]
  type KorolevService[F[+_]] = PartialFunction[Request, F[Response]]

  def korolevService
      [
        F[+_]: Async,
        S: StateSerializer: StateDeserializer,
        M
      ] (
        mimeTypes: MimeTypes,
        config: KorolevServiceConfig[F, S, M]
      ): KorolevService[F] = {

    import misc._

    val sessions = TrieMap.empty[String, KorolevSession[F]]

    def getOrCreateStateManager(deviceId: DeviceId, sessionId: SessionId): F[(StateManager[F], Boolean)] =
      for {
        maybeStateManager <- config.stateStorage.get(deviceId, sessionId)
        stateManager <- maybeStateManager.fold(config.stateStorage.create(deviceId, sessionId))(Async[F].pure(_))
      } yield (stateManager, maybeStateManager.isEmpty)

    def renderStatic(request: Request): F[Response] =
      for {
        deviceId <- deviceFromRequest(request)
        sessionId <- config.idGenerator.generateSessionId()
        state <- config.router(deviceId, None)
          .toState
          .lift((None, request.path))
          .getOrElse(config.stateStorage.createTopLevelState(deviceId))
          .flatMap { state =>
            getOrCreateStateManager(deviceId, sessionId).map(_._1).flatMap { manager =>
              manager.write(Id.TopLevel, state).map(_ => state)
            }
          }
      } yield {
        val dsl = new levsha.TemplateDsl[Context.Effect[F, S, M]]()
        val textRenderContext = new HtmlRenderContext[F, S, M]()
        val rootPath = config.rootPath
        val clw = {
          val textRenderContext = new HtmlRenderContext[F, S, M]()
          config.connectionLostWidget(textRenderContext)
          textRenderContext.mkString
        }
        val heartbeatInterval = config.heartbeatInterval.toMillis

        import dsl._

        val kfg = s"window['kfg']={sid:'$sessionId',r:'$rootPath',clw:'$clw',heartbeatInterval:$heartbeatInterval}"
        val document = 'html(
          'head(
            'script('language /= "javascript", kfg),
            'script('src /= config.rootPath + "korolev-client.min.js"),
            config.head
          ),
          config.render(state)
        )

        // Render document to textRenderContext
        document(textRenderContext)

        Response.Http(
          status = Response.Status.Ok,
          headers = Seq(
            "content-type" -> htmlContentType,
            "set-cookie" -> s"${Cookies.DeviceId}=$deviceId"
          ),
          body = Some {
            val sb = mutable.StringBuilder.newBuilder
            val html = sb
              .append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">")
              .append('\n')
              .append(textRenderContext.builder)
              .mkString
            html.getBytes(StandardCharsets.UTF_8)
          }
        )
      }

    object matchStatic {

      /**
        * @return (resource bytes, ContentType)
        */
      def unapply(req: Request): Option[(Array[Byte], String)] =
        req.path match {
          case Root => None
          case path @ _ / fileName =>
            val fsPath = s"/static${path.toString}"
            val stream = getClass.getResourceAsStream(fsPath)
            Option(stream) map { stream =>
              val fileExtension = fileName.lastIndexOf('.') match {
                case -1 => binaryContentType
                case index => fileName.substring(index + 1)
              }
              (inputStreamToBytes(stream), fileExtension)
            }
        }

      private def inputStreamToBytes(stream: InputStream): Array[Byte] = {
        val bufferSize = 32
        val baos = new ByteArrayOutputStream(Math.max(bufferSize, stream.available()))
        val buffer = new Array[Byte](bufferSize)

        var lastBytesRead = 0
        while ({
          lastBytesRead = stream.read(buffer)
          lastBytesRead != -1
        }) {
          baos.write(buffer, 0, lastBytesRead)
        }

        baos.toByteArray
      }
    }

    def deviceFromRequest(request: Request): F[DeviceId] =
      request.cookie(Cookies.DeviceId) match {
        case None => config.idGenerator.generateDeviceId()
        case Some(deviceId) => Async[F].pure(deviceId)
      }

    def makeSessionKey(deviceId: DeviceId, sessionId: SessionId): String =
      s"$deviceId-$sessionId"

    def createSession(deviceId: DeviceId, sessionId: SessionId): F[KorolevSession[F]] = {

      val connection = new Connection[F]()

      for {
        stateManagerIsNew <- getOrCreateStateManager(deviceId, sessionId)
        (stateManager, isNew) = stateManagerIsNew
        initialState <- config.stateStorage.createTopLevelState(deviceId)

        // Create Korolev with dynamic router
        router = config.router(deviceId, Some(sessionId))
        qualifiedSessionId = QualifiedSessionId(deviceId, sessionId)
        korolev = new ApplicationInstance(
          qualifiedSessionId, connection,
          stateManager, initialState,
          config.render, router, fromScratch = isNew
        )
        env <- config.envConfigurator.configure(korolev.topLevelComponentInstance.browserAccess)
      } yield {

        // Subscribe to events to publish them to env
        korolev.topLevelComponentInstance.setEventsSubscription(env.onMessage)

        new KorolevSession[F] {

          val aliveRef = new AtomicBoolean(true)<commit_msg>Fix event subscription when using lazy async.<commit_after>package korolev

import java.io.{ByteArrayOutputStream, InputStream}
import java.nio.charset.StandardCharsets
import java.util.concurrent.atomic.{AtomicBoolean, AtomicReference}

import korolev.Async._
import korolev.internal.{ApplicationInstance, Connection}
import korolev.state._
import levsha.Id
import slogging.LazyLogging

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

package object server extends LazyLogging {

  type StateStorage[F[+_], S] = korolev.state.StateStorage[F, S]
  val StateStorage = korolev.state.StateStorage

  type MimeTypes = String => Option[String]
  type KorolevService[F[+_]] = PartialFunction[Request, F[Response]]

  def korolevService
      [
        F[+_]: Async,
        S: StateSerializer: StateDeserializer,
        M
      ] (
        mimeTypes: MimeTypes,
        config: KorolevServiceConfig[F, S, M]
      ): KorolevService[F] = {

    import misc._

    val sessions = TrieMap.empty[String, KorolevSession[F]]

    def getOrCreateStateManager(deviceId: DeviceId, sessionId: SessionId): F[(StateManager[F], Boolean)] =
      for {
        maybeStateManager <- config.stateStorage.get(deviceId, sessionId)
        stateManager <- maybeStateManager.fold(config.stateStorage.create(deviceId, sessionId))(Async[F].pure(_))
      } yield (stateManager, maybeStateManager.isEmpty)

    def renderStatic(request: Request): F[Response] =
      for {
        deviceId <- deviceFromRequest(request)
        sessionId <- config.idGenerator.generateSessionId()
        state <- config.router(deviceId, None)
          .toState
          .lift((None, request.path))
          .getOrElse(config.stateStorage.createTopLevelState(deviceId))
          .flatMap { state =>
            getOrCreateStateManager(deviceId, sessionId).map(_._1).flatMap { manager =>
              manager.write(Id.TopLevel, state).map(_ => state)
            }
          }
      } yield {
        val dsl = new levsha.TemplateDsl[Context.Effect[F, S, M]]()
        val textRenderContext = new HtmlRenderContext[F, S, M]()
        val rootPath = config.rootPath
        val clw = {
          val textRenderContext = new HtmlRenderContext[F, S, M]()
          config.connectionLostWidget(textRenderContext)
          textRenderContext.mkString
        }
        val heartbeatInterval = config.heartbeatInterval.toMillis

        import dsl._

        val kfg = s"window['kfg']={sid:'$sessionId',r:'$rootPath',clw:'$clw',heartbeatInterval:$heartbeatInterval}"
        val document = 'html(
          'head(
            'script('language /= "javascript", kfg),
            'script('src /= config.rootPath + "korolev-client.min.js"),
            config.head
          ),
          config.render(state)
        )

        // Render document to textRenderContext
        document(textRenderContext)

        Response.Http(
          status = Response.Status.Ok,
          headers = Seq(
            "content-type" -> htmlContentType,
            "set-cookie" -> s"${Cookies.DeviceId}=$deviceId"
          ),
          body = Some {
            val sb = mutable.StringBuilder.newBuilder
            val html = sb
              .append("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">")
              .append('\n')
              .append(textRenderContext.builder)
              .mkString
            html.getBytes(StandardCharsets.UTF_8)
          }
        )
      }

    object matchStatic {

      /**
        * @return (resource bytes, ContentType)
        */
      def unapply(req: Request): Option[(Array[Byte], String)] =
        req.path match {
          case Root => None
          case path @ _ / fileName =>
            val fsPath = s"/static${path.toString}"
            val stream = getClass.getResourceAsStream(fsPath)
            Option(stream) map { stream =>
              val fileExtension = fileName.lastIndexOf('.') match {
                case -1 => binaryContentType
                case index => fileName.substring(index + 1)
              }
              (inputStreamToBytes(stream), fileExtension)
            }
        }

      private def inputStreamToBytes(stream: InputStream): Array[Byte] = {
        val bufferSize = 32
        val baos = new ByteArrayOutputStream(Math.max(bufferSize, stream.available()))
        val buffer = new Array[Byte](bufferSize)

        var lastBytesRead = 0
        while ({
          lastBytesRead = stream.read(buffer)
          lastBytesRead != -1
        }) {
          baos.write(buffer, 0, lastBytesRead)
        }

        baos.toByteArray
      }
    }

    def deviceFromRequest(request: Request): F[DeviceId] =
      request.cookie(Cookies.DeviceId) match {
        case None => config.idGenerator.generateDeviceId()
        case Some(deviceId) => Async[F].pure(deviceId)
      }

    def makeSessionKey(deviceId: DeviceId, sessionId: SessionId): String =
      s"$deviceId-$sessionId"

    def createSession(deviceId: DeviceId, sessionId: SessionId): F[KorolevSession[F]] = {

      val connection = new Connection[F]()

      for {
        stateManagerIsNew <- getOrCreateStateManager(deviceId, sessionId)
        (stateManager, isNew) = stateManagerIsNew
        initialState <- config.stateStorage.createTopLevelState(deviceId)

        // Create Korolev with dynamic router
        router = config.router(deviceId, Some(sessionId))
        qualifiedSessionId = QualifiedSessionId(deviceId, sessionId)
        korolev = new ApplicationInstance(
          qualifiedSessionId, connection,
          stateManager, initialState,
          config.render, router, fromScratch = isNew
        )
        env <- config.envConfigurator.configure(korolev.topLevelComponentInstance.browserAccess)
      } yield {

        // Subscribe to events to publish them to env
        korolev.topLevelComponentInstance.setEventsSubscription { message: M =>
          env.onMessage(message).runIgnoreResult
        }

        new KorolevSession[F] {

          val aliveRef = new AtomicBoolean(true)
{'fraction_non_alphanumeric': 0.1069197896120973, 'fraction_numerical': 0.002794214332675871, 'mean_word_length': 2.4230661040787624, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7245956', 'n_tokens_mistral': 3499, 'n_tokens_neox': 3355, 'n_words': 686}