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

korolev.server.internal.services.MessagingService.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.server.internal.services

import java.nio.{ByteBuffer, CharBuffer}
import java.nio.charset.StandardCharsets
import java.util.zip.{Deflater, Inflater}
import korolev.Qsid
import korolev.data.{Bytes, BytesLike}
import korolev.effect.{Effect, Queue, Reporter, Stream}
import korolev.effect.syntax.*
import korolev.internal.Frontend
import korolev.server.{HttpResponse, WebSocketResponse}
import korolev.server.DeflateCompressionService
import korolev.server.internal.HttpResponse
import korolev.web.Request.Head
import korolev.web.Response
import korolev.web.Response.Status
import scala.collection.concurrent.TrieMap
import scala.collection.mutable.ListBuffer

private[korolev] final class MessagingService[F[_]: Effect](
  reporter: Reporter,
  commonService: CommonService[F],
  sessionsService: SessionsService[F, _, _],
  compressionSupport: Option[DeflateCompressionService[F]]
) {

  import MessagingService._

  /**
   * Poll message from session's ongoing queue.
   */
  def longPollingSubscribe(qsid: Qsid, rh: Head): F[HttpResponse[F]] =
    for {
      _        <- sessionsService.createAppIfNeeded(qsid, rh, createTopic(qsid))
      maybeApp <- sessionsService.getApp(qsid)
      // See webSocketMessaging()
      maybeMessage <- maybeApp.fold(SomeReloadMessageF)(_.frontend.outgoingMessages.pull())
      response <- maybeMessage match {
                    case None => Effect[F].pure(commonGoneResponse)
                    case Some(message) =>
                      HttpResponse(
                        status = Response.Status.Ok,
                        message = message,
                        headers = commonResponseHeaders
                      )
                  }
    } yield {
      response
    }

  /**
   * Push message to session's incoming queue.
   */
  def longPollingPublish(qsid: Qsid, data: Stream[F, Bytes]): F[HttpResponse[F]] =
    for {
      topic   <- takeTopic(qsid)
      message <- data.fold(Bytes.empty)(_ ++ _).map(_.asUtf8String)
      _       <- topic.enqueue(message)
    } yield commonOkResponse

  private lazy val inflaters = ThreadLocal.withInitial(() => new Inflater(true))
  private lazy val deflaters = ThreadLocal.withInitial(() => new Deflater(Deflater.DEFAULT_COMPRESSION, true))

  private lazy val wsJsonDeflateDecoder = (bytes: Bytes) => {
    val inflater   = inflaters.get()
    val inputArray = bytes.asArray

    val chunkSize          = 1024
    val outputBlocks       = new ListBuffer[Array[Byte]]()
    var totalBytesInflated = 0

    inflater.reset()
    inflater.setInput(inputArray)

    while (!inflater.finished()) {
      val outputArray   = new Array[Byte](chunkSize)
      val bytesInflated = inflater.inflate(outputArray)
      totalBytesInflated += bytesInflated

      // Store the block even if partially filled
      outputBlocks += java.util.Arrays.copyOf(outputArray, bytesInflated)
    }

    // Concatenate all the blocks to form the final decompressed string
    val finalOutputArray = outputBlocks.flatten.toArray
    Effect[F].pure(new String(finalOutputArray, 0, totalBytesInflated, StandardCharsets.UTF_8))
  }

  private lazy val wsJsonDeflateEncoder = (message: String) => {
    val encoder  = StandardCharsets.UTF_8.newEncoder()
    val deflater = deflaters.get()

    // Initialize input as a byte array from the string
    val inputArray = message.getBytes(StandardCharsets.UTF_8)

    // Clear and reset deflater
    deflater.reset()

    // Set the input data for the deflater
    deflater.setInput(inputArray)
    deflater.finish()

    // Temporary buffer to hold deflation result
    val tempOutputArray = new Array[Byte](1024)

    // Initialize a ListBuffer to hold multiple output blocks
    var outputBlocks = ListBuffer[Array[Byte]]()

    // Deflate the input in chunks
    while (!deflater.finished()) {
      val bytesDeflated = deflater.deflate(tempOutputArray)
      outputBlocks += java.util.Arrays.copyOf(tempOutputArray, bytesDeflated)
    }

    // Concatenate all blocks to form the final array
    val compressedArray = outputBlocks.flatten.toArray

    // Convert it back to korolev.data.Bytes
    Effect[F].pure(Bytes.wrap(compressedArray))
  }

  private val wsJsonDecoder = (bytes: Bytes) => Effect[F].pure(bytes.asUtf8String)
  private val wsJsonEncoder = (message: String) => Effect[F].pure(BytesLike[Bytes].utf8(message))

  def webSocketMessaging(
    qsid: Qsid,
    rh: Head,
    incomingMessages: Stream[F, Bytes],
    protocols: Seq[String]
  ): F[WebSocketResponse[F]] = {
    val (selectedProtocol, decoder, encoder) = {
      // Support for protocol compression. A client can tell us
      // it can decompress the messages.
      if (protocols.contains(ProtocolJsonDeflate)) {
        compressionSupport match {
          case Some(DeflateCompressionService(decoder, encoder)) =>
            (ProtocolJsonDeflate, decoder, encoder)
          case None =>
            (ProtocolJsonDeflate, wsJsonDeflateDecoder, wsJsonDeflateEncoder)
        }
      } else {
        (ProtocolJson, wsJsonDecoder, wsJsonEncoder)
      }
    }
    sessionsService.createAppIfNeeded(qsid, rh, incomingMessages.mapAsync(decoder)) flatMap { _ =>
      sessionsService.getApp(qsid) flatMap {
        case Some(app) =>
          val httpResponse = Response(Status.Ok, app.frontend.outgoingMessages.mapAsync(encoder), Nil, None)
          Effect[F].pure(WebSocketResponse(httpResponse, selectedProtocol))
        case None =>
          // Respond with reload message because app was not found.
          // In this case it means that server had ben restarted and
          // do not have an information about the state which had been
          // applied to render of the page on a client side.
          Stream(Frontend.ReloadMessage).mat().map { messages =>
            val httpResponse = Response(Status.Ok, messages.mapAsync(encoder), Nil, None)
            WebSocketResponse(httpResponse, selectedProtocol)
          }
      }
    }
  }

  /**
   * Sessions created via long polling subscription takes messages from topics
   * stored in this table.
   */
  private val longPollingTopics = TrieMap.empty[Qsid, Queue[F, String]]

  /**
   * Same headers in all responses
   */
  private val commonResponseHeaders = Seq(
    "cache-control" -> "no-cache",
    "content-type"  -> "application/json"
  )

  /**
   * Same response for all 'publish' requests.
   */
  private val commonOkResponse = Response(
    status = Response.Status.Ok,
    body = Stream.empty[F, Bytes],
    headers = commonResponseHeaders,
    contentLength = Some(0L)
  )

  /**
   * Same response for all 'subscribe' requests where outgoing stream is
   * consumed.
   */
  private val commonGoneResponse = Response(
    status = Response.Status.Gone,
    body = Stream.empty[F, Bytes],
    headers = commonResponseHeaders,
    contentLength = Some(0L)
  )

  private def takeTopic(qsid: Qsid) =
    Effect[F].delay {
      if (longPollingTopics.contains(qsid)) longPollingTopics(qsid)
      else throw new Exception(s"There is no long-polling topic matching $qsid")
    }

  private def createTopic(qsid: Qsid) = {
    reporter.debug(s"Create long-polling topic for $qsid")
    val topic = Queue[F, String]()
    topic.cancelSignal.runAsync(_ => longPollingTopics.remove(qsid))
    longPollingTopics.putIfAbsent(qsid, topic)
    topic.stream
  }
}

private[korolev] object MessagingService {

  private val ProtocolJsonDeflate = "json-deflate"
  private val ProtocolJson        = "json"

  def SomeReloadMessageF[F[_]: Effect]: F[Option[String]] =
    Effect[F].pure(Option(Frontend.ReloadMessage))
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy