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

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

The newest version!
/*
 * Copyright 2017-2020 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 korolev.*
import java.util.concurrent.atomic.AtomicInteger
import korolev.*
import korolev.Context.*
import korolev.effect.{Effect, Hub, Queue, Reporter, Scheduler, Stream}
import korolev.effect.syntax.*
import korolev.internal.Frontend.DomEventMessage
import korolev.state.StateDeserializer
import korolev.state.StateManager
import korolev.state.StateSerializer
import korolev.web.Path
import korolev.web.PathAndQuery
import levsha.Document
import levsha.Id
import levsha.StatefulRenderContext
import levsha.XmlNs
import levsha.events.calculateEventPropagation
import levsha.impl.DiffRenderContext
import levsha.impl.DiffRenderContext.ChangesPerformer
import scala.collection.concurrent.TrieMap
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.*

final class ApplicationInstance[
  F[_]: Effect,
  S: StateSerializer: StateDeserializer,
  M
](
  sessionId: Qsid,
  val frontend: Frontend[F],
  stateManager: StateManager[F],
  initialState: S,
  render: S => Document.Node[Binding[F, S, M]],
  rootPath: Path,
  router: Router[F, S],
  createMiscProxy: (
    StatefulRenderContext[Binding[F, S, M]],
    (StatefulRenderContext[Binding[F, S, M]], Binding[F, S, M]) => Unit
  ) => StatefulRenderContext[Binding[F, S, M]],
  scheduler: Scheduler[F],
  reporter: Reporter,
  recovery: PartialFunction[Throwable, S => S],
  delayedRender: FiniteDuration
)(implicit ec: ExecutionContext) { application =>

  import reporter.Implicit

  private val devMode       = new DevMode.ForRenderContext(sessionId.toString)
  private val eventCounters = new TrieMap[(Id, String), Int]()
  private val stateQueue    = Queue[F, (Id, Any, Option[Effect.Promise[Unit]])]()
  private val stateHub      = Hub(stateQueue.stream)
  private val messagesQueue = Queue[F, M]()

  private val renderContext = {
    DiffRenderContext[Binding[F, S, M]](savedBuffer = devMode.loadRenderContext())
  }

  val topLevelComponentInstance: ComponentInstance[F, S, M, S, Any, M] = {
    val eventRegistry = new EventRegistry[F](frontend)
    val component = new Component[F, S, Any, M](initialState, Component.TopLevelComponentId) {
      def render(parameters: Any, state: S): Document.Node[Binding[F, S, M]] =
        try {
          application.render(state)
        } catch {
          case e: MatchError =>
            Document.Node[Binding[F, S, M]] { rc =>
              reporter.error(s"Render is not defined for $state")
              rc.openNode(XmlNs.html, "html")
              rc.openNode(XmlNs.html, "body")
              rc.addTextNode("Render is not defined for the state. ")
              rc.addTextNode(e.getMessage())
              rc.closeNode("body")
              rc.closeNode("html")
            }
        }
    }
    val componentInstance = new ComponentInstance[F, S, M, S, Any, M](
      Id.TopLevel,
      sessionId,
      frontend,
      eventRegistry,
      stateManager,
      component,
      stateQueue,
      createMiscProxy,
      scheduler,
      reporter,
      { case ex: Throwable =>
        topLevelComponentInstance.browserAccess.transition(recovery(ex))
      }
    )
    componentInstance.setEventsSubscription(messagesQueue.offerUnsafe)
    componentInstance
  }

  /**
   * If dev mode is enabled save render context
   */
  private def saveRenderContextIfNecessary(): F[Unit] =
    if (devMode.isActive) Effect[F].delay(devMode.saveRenderContext(renderContext))
    else Effect[F].unit

  private def onState(maybeRenderCallback: Seq[Effect.Promise[Unit]]): F[Unit] =
    for {
      snapshot <- stateManager.snapshot
      // Set page url if router exists
      _ <- router.fromState
             .lift(snapshot(Id.TopLevel).getOrElse(initialState))
             .fold(Effect[F].unit)(uri => frontend.changePageUrl(rootPath ++ uri))
      _ <- Effect[F].delay {
             // Prepare render context
             renderContext.swap()
             // Perform rendering
             topLevelComponentInstance.applyRenderContext(
               parameters = (), // Boxed unit as parameter. Top level component doesn't need parameters
               snapshot = snapshot,
               rc = renderContext
             )
           }
      // Infer and perform changes
      _ <- frontend.performDomChanges(renderContext.diff)
      _ <- saveRenderContextIfNecessary()
      // Make korolev ready to next render
      _ <- Effect[F].delay(topLevelComponentInstance.dropObsoleteMisc())
      _  = maybeRenderCallback.foreach(_(Right(())))
    } yield ()

  private def onHistory(pq: PathAndQuery): F[Unit] =
    stateManager
      .read[S](Id.TopLevel)
      .flatMap { maybeTopLevelState =>
        router.toState
          .lift(pq)
          .fold(Effect[F].delay(Option.empty[S]))(_(maybeTopLevelState.getOrElse(initialState)).map(Some(_)))
      }
      .flatMap {
        case Some(newState) =>
          stateManager
            .write(Id.TopLevel, newState)
            .after(stateQueue.enqueue(Id.TopLevel, newState, None))
        case None =>
          Effect[F].unit
      }

  private def onEvent(dem: DomEventMessage): F[Unit] = {
    def aux(effects: List[DomEventMessage => F[Boolean]]): F[Unit] =
      effects match {
        case Nil => Effect[F].unit
        case effect :: xs =>
          effect(dem).flatMap { stopPropagation =>
            if (stopPropagation) Effect[F].unit
            else aux(xs)
          }
      }
    val k = (dem.target, dem.eventType)
    Effect[F]
      .delay(eventCounters.getOrElse(k, 0)).flatMap { eventCounter => 
        if (eventCounter == dem.eventCounter) {
          val propagation = calculateEventPropagation(dem.target, dem.eventType)
          val allEffects = topLevelComponentInstance.eventHandlersFor(propagation.toVector)
          if (allEffects.nonEmpty) {
            for {
              _ <- aux(allEffects.toList)
              newEventConter = dem.eventCounter + 1
              _             <- Effect[F].delay(eventCounters.put(k, newEventConter))
              _             <- frontend.setEventCounter(dem.target, dem.eventType, newEventConter)
            } yield ()
          } else {
            Effect[F].unit
          }
        } else {
          Effect[F].unit
        }
      }
      .recover { case error => reporter.error(s"Unable to process event $dem", error) }
      .start
      .unit
  }

  private final val internalStateStream =
    stateHub.newStreamUnsafe()

  val stateStream: F[Stream[F, (Id, Any)]] =
    stateHub.newStream().map { stream =>
      stream.map { case (id, state, _) =>
        (id, state)
      }
    }

  val messagesStream: Stream[F, M] =
    messagesQueue.stream

  def destroy(): F[Unit] = for {
    _ <- stateQueue.close()
    _ <- messagesQueue.close()
    _ <- topLevelComponentInstance.destroy()
  } yield ()

  def initialize()(implicit ec: ExecutionContext): F[Unit] = {

    // If dev mode is enabled and active
    // CSS should be reloaded
    def reloadCssIfNecessary() =
      if (devMode.isActive) frontend.reloadCss()
      else Effect[F].unit

    // Render current state using 'performDiff'.
    def render(performDiff: (ChangesPerformer => Unit) => F[Unit]) =
      for {
        snapshot <- stateManager.snapshot
        _        <- Effect[F].delay(topLevelComponentInstance.applyRenderContext((), renderContext, snapshot))
        _        <- performDiff(renderContext.diff)
        _        <- saveRenderContextIfNecessary()
      } yield ()

    if (devMode.saved) {

      // Initialize with
      // 1. Old page in users browser
      // 2. Has saved render context
      for {
        _ <- frontend.resetEventCounters()
        _ <- reloadCssIfNecessary()
        _ <- Effect[F].delay(renderContext.swap())
        // Serialized render context exists.
        // It means that user is looking at page
        // generated by old code. The code may
        // consist changes in render, so we
        // should deliver them to the user.
        _ <- render(frontend.performDomChanges)
        _ <- topLevelComponentInstance.initialize()
      } yield ()

    } else {

      // Initialize with pre-rendered page
      for {
        _ <- frontend.resetEventCounters()
        _ <- reloadCssIfNecessary()
        _ <- render(f => Effect[F].delay(f(DiffRenderContext.DummyChangesPerformer)))
        // Start handlers
        _ <- frontend.browserHistoryMessages
               .foreach(onHistory)
               .start
        _ <- frontend.domEventMessages
               .foreach(onEvent)
               .start
        _ <- if (delayedRender.toMillis > 0) {
               internalStateStream
                 .buffer(delayedRender)
                 .foreach { xs =>
                   onState(xs.flatMap(_._3))
                 }
                 .start
             } else {
               internalStateStream.foreach { x =>
                 onState(x._3.toSeq)
               }.start
             }
        // Init component
        _ <- topLevelComponentInstance.initialize()
      } yield ()
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy