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

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

There is a newer version: 1.16.0-M5
Show 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 korolev.data.Bytes
import korolev.effect.Effect
import korolev.effect.Queue
import korolev.effect.Reporter
import korolev.effect.Scheduler
import korolev.effect.Stream
import korolev.effect.syntax.*
import korolev.internal.Frontend.DomEventMessage
import korolev.state.StateDeserializer
import korolev.state.StateManager
import korolev.state.StateSerializer
import korolev.util.JsCode
import korolev.util.Lens
import korolev.web.FormData
import levsha.Id
import levsha.StatefulRenderContext
import levsha.events.EventId
import levsha.events.EventPhase

import scala.collection.AbstractMapView
import scala.collection.MapView
import scala.collection.concurrent.TrieMap
import scala.collection.mutable
import scala.concurrent.ExecutionContext
import scala.util.control.NonFatal

import Context.*

/**
  * 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[_]: Effect,
    AS: StateSerializer: StateDeserializer,
    M,
    CS: StateSerializer: StateDeserializer,
    P,
    E
](
    nodeId: Id,
    sessionId: Qsid,
    frontend: Frontend[F],
    eventRegistry: EventRegistry[F],
    stateManager: StateManager[F],
    val component: Component[F, CS, P, E],
    stateQueue: Queue[F, (Id, Any, Option[Effect.Promise[Unit]])],
    createMiscProxy: (StatefulRenderContext[Binding[F, AS, M]],
                      (StatefulRenderContext[Binding[F, CS, E]], Binding[F, CS, E]) => Unit) => StatefulRenderContext[
      Binding[F, CS, E]],
    scheduler: Scheduler[F],
    reporter: Reporter,
    recovery: PartialFunction[Throwable, F[Unit]],
) { self =>

  import ComponentInstance._
  import reporter.Implicit

  private val miscLock = new Object()

  private var lastParameters: P = _
  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, Id]
  private val events = mutable.Map.empty[EventId, Vector[Event[F, CS, E]]]
  private val nestedComponents = mutable.Map.empty[Id, ComponentInstance[F, CS, E, _, _, _]]

  // Why we use '() => F[Unit]'? Because should
  // support scala.concurrent.Future which is has
  // strict semantic (runs immediately).
  private val pendingEffects = Queue[F, () => F[Unit]]()

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

  private[korolev] case class BrowserAccess(dem: DomEventMessage) extends BaseAccessDefault[F, CS, E] {

    private def getId(elementId: ElementId): F[Id] = Effect[F].delay {
      unsafeGetId(elementId)
    }

    private def unsafeGetId(elementId: ElementId): Id = {
      // miscLock synchronization required
      // because prop handler methods can be
      // invoked during render.
      miscLock.synchronized {
        elements.get(elementId) match {
          case None =>
            elementId.name match {
              case Some(name) => throw new Exception(s"No element matched for accessor $name")
              case None       => throw new Exception(s"No element matched for accessor")
            }
          case Some(id) => id
        }
      }
    }

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

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

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

    def publish(message: E): F[Unit] =
      Effect[F].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[Qsid] = Effect[F].delay(self.sessionId)

    def transition(f: Transition[CS]): F[Unit] = applyTransition(x => Effect[F].pure(f(x)))
    def transitionForce(f: Transition[CS]): F[Unit] = applyTransitionForce(x => Effect[F].pure(f(x)))
    def transitionAsync(f: TransitionAsync[F, CS]): F[Unit] = applyTransition(f)
    def transitionForceAsync(f: TransitionAsync[F, CS]): F[Unit] = applyTransitionForce(f)

    def downloadFormData(element: ElementId): F[FormData] =
      for {
        id <- getId(element)
        formData <- frontend.uploadForm(id)
      } yield formData

    def downloadFiles(id: ElementId): F[List[(FileHandler, Bytes)]] = {
      downloadFilesAsStream(id).flatMap { streams =>
        Effect[F].sequence {
          streams.map {
            case (handler, data) =>
              data
                .fold(Bytes.empty)(_ ++ _)
                .map(b => (handler, b))
          }
        }
      }
    }

    def downloadFilesAsStream(id: ElementId): F[List[(FileHandler, Stream[F, Bytes])]] = {
      listFiles(id).flatMap { handlers =>
        Effect[F].sequence {
          handlers.map { handler =>
            downloadFileAsStream(handler).map(f => (handler, f))
          }
        }
      }
    }

    /**
      * Get selected file as a stream from input
      */
    def downloadFileAsStream(handler: FileHandler): F[Stream[F, Bytes]] = {
      for {
        id <- getId(handler.elementId)
        streams <- frontend.uploadFile(id, handler)
      } yield streams
    }

    def listFiles(elementId: ElementId): F[List[FileHandler]] =
      for {
        id <- getId(elementId)
        files <- frontend.listFiles(id)
      } yield {
        files.map {
          case (fileName, size) =>
            FileHandler(fileName, size)(elementId)
        }
      }

    def uploadFile(name: String, stream: Stream[F, Bytes], size: Option[Long], mimeType: String): F[Unit] =
      frontend.downloadFile(name, stream, size, mimeType)

    def resetForm(elementId: ElementId): F[Unit] =
      getId(elementId).flatMap { id =>
        frontend.resetForm(id)
      }

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

    def eventData: F[String] = frontend.extractEventData(dem)

    def registerCallback(name: String)(f: String => F[Unit]): F[Unit] =
      frontend.registerCustomCallback(name)(f)
  }

  private[korolev] val browserAccess = BrowserAccess(DomEventMessage(0, Id.TopLevel, "init"))

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

  def applyRenderContext(parameters: P,
                         rc: StatefulRenderContext[Binding[F, AS, M]],
                         snapshot: StateManager.Snapshot): Unit = miscLock.synchronized {
    // Reset all event handlers delays and elements
    prepare()
    val state = snapshot[CS](nodeId).map(Right(_)).getOrElse(component.initialState)
    val node = state match {
      case Right(value) =>
        if (lastParameters != parameters) {
          component.maybeUpdateState(parameters, value) match {
            case None => ()
            case Some(effect) =>
              effect.flatMap { newState =>
                stateManager.write(nodeId, newState) *>
                  stateQueue.enqueue(nodeId, newState, None)
              }.runAsyncForget // TODO Should be cancelable
          }
        }
        component.render(parameters, value)
      case Left(generateState) =>
        generateState(parameters).flatMap { newState =>
          stateManager.write(nodeId, newState) *>
            stateQueue.enqueue(nodeId, newState, None)
        }.runAsyncForget  // TODO Should be cancelable
        component.renderNoState(parameters)
    }

    lastParameters = parameters
    val proxy = createMiscProxy(
      rc, { (proxy, misc) =>
        misc match {
          case event: Event[F, CS, E] =>
            val id = rc.currentContainerId
            val eid = EventId(id, event.`type`, event.phase)
            val es = events.getOrElseUpdate(eid, Vector.empty)
            events.put(eid, es :+ event)
            eventRegistry.registerEventType(event.`type`)
          case element: ElementId =>
            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, scheduler, reporter)
              delays.put(id, delayInstance)
              delayInstance.start(browserAccess)
            }
          case entry: ComponentEntry[F, CS, E, Any, Any, Any] =>
            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).runAsyncForget)
                n.applyRenderContext(entry.parameters, proxy, snapshot)
              case _ =>
                val n = entry.createInstance(
                  id,
                  sessionId,
                  frontend,
                  eventRegistry,
                  stateManager,
                  stateQueue,
                  scheduler,
                  reporter,
                  recovery
                )
                markedComponentInstances += id
                nestedComponents.put(id, n)
                n.unsafeInitialize()
                n.setEventsSubscription((e: Any) => entry.eventHandler(browserAccess, e).runAsyncForget)
                n.applyRenderContext(entry.parameters, proxy, snapshot)
            }
        }
      }
    )
    node(proxy)
  }

  private def applyTransitionEffect(transition: TransitionAsync[F, CS]): F[CS] =
    for {
      maybeState <- stateManager.read[CS](nodeId)
      state <- maybeState
        .orElse(component.initialState.toOption)
        .fold(Effect[F].fail(new Exception("Uninitialized component state")): F[CS])(Effect[F].pure(_))
      newState <- transition(state)
      _ <- stateManager.write(nodeId, newState)
    } yield newState

  private def applyTransition(transition: TransitionAsync[F, CS]): F[Unit] = {
    val effect = () =>
      for {
        newState <- applyTransitionEffect(transition)
        _ <- stateQueue.enqueue(nodeId, newState, None)
      } yield ()
    pendingEffects.enqueue(effect)
  }

  private def applyTransitionForce(transition: TransitionAsync[F, CS]): F[Unit] = Effect[F].promiseF[Unit] { cb =>
    val effect = () =>
      for {
        newState <- applyTransitionEffect(transition).recoverF {
          case e =>
            cb(Left(e))
            Effect[F].fail[CS](e)
        }
        _ <- stateQueue.enqueue(nodeId, newState, Some(cb))
      } yield ()
    pendingEffects.enqueue(effect)
  }

  type EventHandlers =  Vector[DomEventMessage => F[Boolean]]
  type AllEventHandlers = MapView[EventId, EventHandlers]

  def allEventHandlers: AllEventHandlers =
    events.view.mapValues { handlers =>
      handlers.map { handler => (dem: DomEventMessage) =>
        handler
          .effect(BrowserAccess(dem))
          .as(handler.stopPropagation)        
      }
    } +++ nestedComponents
      .values
      .map(_.allEventHandlers)
      .foldLeft(MapView.empty: AllEventHandlers)(_ +++ _)

  def eventHandlersFor(eventId: EventId): EventHandlers =
    events
      .get(eventId).fold(Vector.empty[DomEventMessage => F[Boolean]]) { handlers =>
        handlers.map { handler => (dem: DomEventMessage) =>
          handler
            .effect(BrowserAccess(dem))
            .as(handler.stopPropagation)
        }
      } ++ nestedComponents
      .values
      .flatMap(_.eventHandlersFor(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)
          nested
            .destroy()
            .after(stateManager.delete(id))
            .runAsyncForget
        } 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)
    }
  }

  /**
    * Close 'pendingEffects' in this component and
    * all nested components.
    *
    * MUST be invoked after closing connection.
    */
  def destroy(): F[Unit] =
    for {
      _ <- pendingEffects.close()
      _ <- nestedComponents.values.toList
        .map(_.destroy())
        .sequence
        .unit
    } yield ()

  private def applyPendingEffect(f: () => F[Unit]): F[Unit] =
    f().recover { case e => reporter.error("Transition failed", e) }

  protected def unsafeInitialize(): Unit =
    pendingEffects.stream
      .foreach(applyPendingEffect)
      .runAsyncForget

  // Execute effects sequentially
  def initialize()(implicit ec: ExecutionContext): F[Effect.Fiber[F, Unit]] =
    Effect[F].start(pendingEffects.stream.foreach(applyPendingEffect))
}

private object ComponentInstance {

  import Context.Access
  import Context.Delay

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

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

    def isFinished: Boolean = finished

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

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy