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

korolev.internal.ComponentInstance.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.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger

import korolev._
import Context._
import Async._
import korolev.execution.Scheduler
import korolev.state.{StateDeserializer, StateManager, StateSerializer}
import levsha.Document.Node
import levsha.{Id, StatefulRenderContext, XmlNs}
import levsha.events.EventId

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

/**
  * Component state holder and effects performer
  *
  * Performing cycle:
  *
  * 1. prepare()
  * 2. Optionally setState()
  * 3. applyRenderContext()
  * 4. dropObsoleteMisc()
  *
  * @tparam AS Type of top level state (application state)
  * @tparam CS Type of component state
  */
final class ComponentInstance
  [
    F[_]: Async: Scheduler,
    AS: StateSerializer: StateDeserializer, M,
    CS: StateSerializer: StateDeserializer, P, E
  ](
    nodeId: Id,
    sessionId: QualifiedSessionId,
    frontend: ClientSideApi[F],
    eventRegistry: EventRegistry[F],
    stateManager: StateManager[F, CS],
    getRenderNum: () => Int,
    val component: Component[F, CS, P, E],
    reporter: Reporter
  ) { self =>

  import ComponentInstance._
  import reporter.Implicit

  private val async = Async[F]
  private val miscLock = new Object()
  private val subscriptionsLock = new Object()
  private val lastPostDescriptor = new AtomicInteger(0)

  private val markedDelays = mutable.Set.empty[Id] // Set of the delays which are should survive
  private val markedComponentInstances = mutable.Set.empty[Id]
  private val delays = mutable.Map.empty[Id, DelayInstance[F, CS, E]]
  private val elements = mutable.Map.empty[ElementId[F, CS, E], Id]
  private val events = mutable.Map.empty[EventId, Event[F, CS, E]]
  private val nestedComponents = mutable.Map.empty[Id, ComponentInstance[F, CS, E, _, _, _]]
  private val formDataPromises = mutable.Map.empty[String, Promise[F, FormData]]
  private val downloadFilePromises = mutable.Map.empty[String, Promise[F, List[File[LazyBytes[F]]]]]
  private val formDataProgressTransitions = mutable.Map.empty[String, (Int, Int) => Transition[CS]]

  private val stateChangeSubscribers = mutable.ArrayBuffer.empty[(Id, Any) => Unit]
  private val pendingTransitions = new ConcurrentLinkedQueue[(Transition[CS], Promise[F, Unit])]()

  @volatile private var eventSubscription = Option.empty[E => _]
  @volatile private var transitionInProgress = false

  private[korolev] object browserAccess extends Access[F, CS, E] {

    private def noElementException[T]: F[T] = {
      val exception = new Exception("No element matched for accessor")
      async.fromTry(Failure(exception))
    }

    private def getId(elementId: ElementId[F, CS, E]): F[Id] = {
      // miscLock synchronization required
      // because prop handler methods can be
      // invoked during render.
      miscLock.synchronized {
        elements
          .get(elementId)
          .fold(noElementException[Id])(id => async.delay(id))
      }
    }

    def property(elementId: ElementId[F, CS, E]): PropertyHandler[F] = {
      val idF = getId(elementId)
      new PropertyHandler[F] {
        def get(propName: Symbol): F[String] = idF.flatMap { id =>
          frontend.extractProperty(id, propName.name)
        }

        def set(propName: Symbol, value: Any): F[Unit] = idF.flatMap { id =>
          // XmlNs argument is empty cause it will be ignored
          async.delay(frontend.setProperty(id, propName, value))
        }
      }
    }

    def focus(element: ElementId[F, CS, E]): F[Unit] =
      getId(element).flatMap { id =>
        async.delay(frontend.focus(id))
      }

    def publish(message: E): F[Unit] =
      async.delay(eventSubscription.foreach(f => f(message)))

    def state: F[CS] = {
      val state = stateManager.read[CS](nodeId)

      state.map(_.getOrElse(throw new RuntimeException("State is empty")))
    }

    def sessionId: F[QualifiedSessionId] = async.delay(self.sessionId)

    def transition(f: Transition[CS]): F[Unit] = applyTransition(f)

    def downloadFormData(element: ElementId[F, CS, E]): FormDataDownloader[F, CS] = new FormDataDownloader[F, CS] {

      private val descriptor = nodeId.mkString + lastPostDescriptor.getAndIncrement()

      def start(): F[FormData] = getId(element) flatMap { id =>
        val promise = async.promise[FormData]
        frontend.uploadForm(id, descriptor)
        formDataPromises.put(descriptor, promise)
        promise.async
      }

      def onProgress(f: (Int, Int) => Transition[CS]): this.type = {
        formDataProgressTransitions.put(descriptor, f)
        this
      }
    }

    def downloadFiles(id: ElementId[F, CS, E]): F[List[File[Array[Byte]]]] = {
      downloadFilesAsStream(id).flatMap { lazyFileList =>
        async.sequence {
          lazyFileList.map { lazyFile =>
            lazyFile.data.toStrict.map(x => File(lazyFile.name, x))
          }
        }
      }
    }

    def downloadFilesAsStream(elementId: ElementId[F, CS, E]): F[List[File[LazyBytes[F]]]] = {
      val promise = async.promise[List[File[LazyBytes[F]]]]
      val descriptor = nodeId.mkString + lastPostDescriptor.getAndIncrement()
      frontend.uploadFiles(elements(elementId), descriptor)
      downloadFilePromises.put(descriptor, promise)
      promise.async
    }

    def evalJs(code: String): F[String] = frontend.evalJs(code)

    def eventData: F[String] = {
      frontend.extractEventData(getRenderNum())
    }
  }

  private def applyEventResult(effect: F[Unit]): Unit = {
    // Run effect
    effect.run {
      case Failure(e) => reporter.error("Exception during applying transition", e)
      case Success(_) => ()
    }
  }

  private def createUnsubscribe[T](from: mutable.Buffer[T], that: T) = { () =>
    subscriptionsLock.synchronized { from -= that; () }
  }

  /**
    * Subscribe to component instance state changes.
    * Callback will be invoked for every state change.
    */
  def subscribeStateChange(callback: (Id, Any) => Unit): () => Unit = {
    subscriptionsLock.synchronized {
      stateChangeSubscribers += callback
      createUnsubscribe(stateChangeSubscribers, callback)
    }
  }

  /**
    * Subscribes to component instance events.
    * Callback will be invoked on call of `access.publish()` in the
    * component instance context.
    */
  def setEventsSubscription(callback: E => _): Unit = {
    subscriptionsLock.synchronized {
      eventSubscription = Some(callback)
    }
  }

  def applyRenderContext(parameters: P,
                         rc: StatefulRenderContext[Effect[F, AS, M]],
                         snapshot: StateManager.Snapshot): Unit = miscLock.synchronized {
    // Reset all event handlers delays and elements
    prepare()
    val state = snapshot[CS](nodeId).getOrElse(component.initialState)
    val node =
      try {
        component.render(parameters, state)
      } catch {
        case e: MatchError =>
          Node[Effect[F, CS, E]] { rc =>
            reporter.error(s"Render is not defined for $state", e)
            rc.openNode(XmlNs.html, "span")
            rc.addTextNode("Render is not defined for the state")
            rc.closeNode("span")
          }
      }
    val proxy = new StatefulRenderContext[Effect[F, CS, E]] { proxy =>
      def subsequentId: Id = rc.subsequentId
      def currentId: Id = rc.currentId
      def currentContainerId: Id = rc.currentContainerId
      def openNode(xmlNs: XmlNs, name: String): Unit = rc.openNode(xmlNs, name)
      def closeNode(name: String): Unit = rc.closeNode(name)
      def setAttr(xmlNs: XmlNs, name: String, value: String): Unit = rc.setAttr(xmlNs, name, value)
      def addTextNode(text: String): Unit = rc.addTextNode(text)
      def addMisc(misc: Effect[F, CS, E]): Unit = {
        misc match {
          case event @ Event(eventType, phase, _) =>
            val id = rc.currentContainerId
            events.put(EventId(id, eventType.name, phase), event)
            eventRegistry.registerEventType(event.`type`)
          case element: ElementId[F, CS, E] =>
            val id = rc.currentContainerId
            elements.put(element, id)
            ()
          case delay: Delay[F, CS, E] =>
            val id = rc.currentContainerId
            markedDelays += id
            if (!delays.contains(id)) {
              val delayInstance = new DelayInstance(delay, reporter)
              delays.put(id, delayInstance)
              delayInstance.start(browserAccess)
            }
          case entry @ ComponentEntry(_, _: Any, _: ((Access[F, CS, E], Any) => F[Unit])) =>
            val id = rc.subsequentId
            nestedComponents.get(id) match {
              case Some(n: ComponentInstance[F, CS, E, Any, Any, Any]) if n.component.id == entry.component.id =>
                // Use nested component instance
                markedComponentInstances += id
                n.setEventsSubscription((e: Any) => entry.eventHandler(browserAccess, e).runIgnoreResult)
                n.applyRenderContext(entry.parameters, proxy, snapshot)
              case _ =>
                // Create new nested component instance
                val sm = stateManager.withDefault(entry.component.initialState)
                val n = entry.createInstance(id, sessionId, frontend, eventRegistry, sm, getRenderNum, reporter)
                markedComponentInstances += id
                nestedComponents.put(id, n)
                n.subscribeStateChange { (id, state) =>
                  // Propagate nested component instance state change event
                  // to high-level component instance
                  stateChangeSubscribers.foreach { f =>
                    f(id, state)
                  }
                }
                n.setEventsSubscription((e: Any) => entry.eventHandler(browserAccess, e).runIgnoreResult)
                n.applyRenderContext(entry.parameters, proxy, snapshot)
            }
        }
      }
    }
    node(proxy)
  }

  def applyTransition(transition: Transition[CS]): F[Unit] = {
    def runTransition(transition: Transition[CS], promise: Promise[F, Unit]): Unit = {
      transitionInProgress = true
      stateManager.read[CS](nodeId) flatMap { maybeState =>
        val state = maybeState.getOrElse(component.initialState)
        try {
          val newState = transition(state)
          stateManager.write(nodeId, newState).map { _ =>
            stateChangeSubscribers.foreach(_.apply(nodeId, state))
          }
        } catch {
          case e: MatchError =>
            async.delay {
              reporter.warning("Transition doesn't fit the state", e)
            }
          case e: Throwable =>
            async.delay {
              reporter.error("Exception happened when applying transition", e)
            }
        }
      } runOrReport { _ =>
        promise.complete(Success(()))
        Option(pendingTransitions.poll()) match {
          case Some((t, p)) => runTransition(t, p)
          case None => transitionInProgress = false
        }
      }
    }

    val promise = async.promise[Unit]
    if (transitionInProgress) pendingTransitions.offer((transition, promise))
    else runTransition(transition, promise)
    promise.async
  }

  def applyEvent(eventId: EventId): Boolean = {
    events.get(eventId) match {
      case Some(event: Event[F, CS, E]) =>
        applyEventResult(event.effect(browserAccess))
        false
      case None =>
        nestedComponents.values.forall { nested =>
          nested.applyEvent(eventId)
        }
    }
  }

  /**
    * Remove all delays and nested component instances
    * which were not marked during applying render context.
    */
  def dropObsoleteMisc(): Unit = miscLock.synchronized {
    delays foreach {
      case (id, delay) =>
        if (!markedDelays.contains(id)) {
          delays.remove(id)
          delay.cancel()
        }
    }
    nestedComponents foreach {
      case (id, nested) =>
        if (!markedComponentInstances.contains(id)) {
          nestedComponents.remove(id)
          stateManager.delete(id).runIgnoreResult
        }
        else nested.dropObsoleteMisc()
    }
  }

  /**
    * Prepares component instance to applying render context.
    * Removes all temporary and obsolete misc.
    * All nested components also will be prepared.
    */
  private def prepare(): Unit = {
    markedComponentInstances.clear()
    markedDelays.clear()
    elements.clear()
    events.clear()
    // Remove only finished delays
    delays foreach {
      case (id, delay) =>
        if (delay.isFinished)
          delays.remove(id)
    }
  }

  def resolveFormData(descriptor: String, formData: Try[FormData]): Unit = miscLock.synchronized {
    formDataPromises.get(descriptor) match {
      case Some(promise) =>
        promise.complete(formData)
        // Remove promise and onProgress handler
        // when formData loading is complete
        formDataProgressTransitions.remove(descriptor)
        formDataPromises.remove(descriptor)
        ()
      case None =>
        nestedComponents.values.foreach { nested =>
          nested.resolveFormData(descriptor, formData)
        }
    }
  }

  def resolveFile(descriptor: String, files: List[File[LazyBytes[F]]]): Unit = miscLock.synchronized {
    downloadFilePromises.get(descriptor) match {
      case Some(promise) =>
        downloadFilePromises.remove(descriptor)
        promise.complete(Success(files))
      case None =>
        nestedComponents.values.foreach { nested =>
          nested.resolveFile(descriptor, files)
        }
    }
  }

  def handleFormDataProgress(descriptor: String, loaded: Int, total: Int): Unit = miscLock.synchronized {
    formDataProgressTransitions.get(descriptor) match {
      case None =>
        nestedComponents.values.foreach { nested =>
          nested.handleFormDataProgress(descriptor, loaded, total)
        }
      case Some(f) =>
        applyTransition(f(loaded.toInt, total.toInt))
          .runIgnoreResult
    }
  }
}

private object ComponentInstance {

  import Context.Access
  import Context.Delay

  final class DelayInstance[F[_]: Async: Scheduler, S, M](delay: Delay[F, S, M], reporter: Reporter) {

    import reporter.Implicit

    @volatile private var handler = Option.empty[Scheduler.JobHandler[F, _]]
    @volatile private var finished = false

    def isFinished: Boolean = finished

    def cancel(): Unit = {
      handler.foreach(_.cancel())
    }

    def start(access: Access[F, S, M]): Unit = {
      handler = Some {
        Scheduler[F].scheduleOnce(delay.duration) {
          finished = true
          delay.effect(access).runIgnoreResult
        }
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy