├── .scalafmt.conf ├── project ├── build.properties ├── plugins.sbt └── JsUtils.scala ├── examples ├── routing │ └── src │ │ └── main │ │ ├── resources │ │ ├── application.conf │ │ └── static │ │ │ ├── favicon.ico │ │ │ └── main.css │ │ └── scala │ │ └── PathAndQueryRoutingExample.scala ├── file-streaming │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── OneByOneFileStreamingExample.scala ├── event-data │ └── src │ │ └── main │ │ └── scala │ │ └── EventDataExample.scala ├── akka-http │ └── src │ │ └── main │ │ └── scala │ │ └── AkkaHttpExample.scala ├── zio │ └── src │ │ ├── test │ │ └── scala │ │ │ └── ZioExampleSpec.scala │ │ └── main │ │ └── scala │ │ └── ZioExample.scala ├── focus │ └── src │ │ └── main │ │ └── scala │ │ └── FocusExample.scala ├── monix │ └── src │ │ └── main │ │ └── scala │ │ └── MonixExample.scala ├── evalJs │ └── src │ │ └── main │ │ └── scala │ │ └── EvalJsExample.scala ├── delay │ └── src │ │ └── main │ │ └── scala │ │ └── DelayExample.scala ├── web-component │ └── src │ │ └── main │ │ └── scala │ │ └── WebComponentExample.scala ├── context-scope │ └── src │ │ └── main │ │ └── scala │ │ ├── BlogView.scala │ │ ├── ContextScopeExample.scala │ │ └── ViewState.scala ├── zio-http │ └── src │ │ └── main │ │ └── scala │ │ └── ZioHttpExample.scala └── extension │ └── src │ └── main │ └── scala │ └── ExtensionExample.scala ├── docs ├── deploy.sh └── build.sh ├── misc ├── integration-tests │ └── src │ │ └── main │ │ ├── resources │ │ ├── simplelogger.properties │ │ └── static │ │ │ ├── main.css │ │ │ └── debug-console.js │ │ └── scala │ │ ├── gp │ │ └── package.scala │ │ └── tools │ │ ├── Step.scala │ │ ├── StepResult.scala │ │ ├── Caps.scala │ │ ├── SauceLabsClient.scala │ │ └── package.scala └── performance-benchmark │ └── src │ └── main │ ├── scala │ └── korolev │ │ ├── BenchmarkConfiguration.scala │ │ ├── data │ │ ├── Scenario.scala │ │ ├── ScenarioStep.scala │ │ ├── Error.scala │ │ ├── ScenarioState.scala │ │ ├── Report.scala │ │ ├── ToServer.scala │ │ └── FromServer.scala │ │ └── ScenarioLoader.scala │ └── resources │ └── application.conf ├── modules ├── web-dsl │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── web │ │ └── dsl │ │ ├── JsonCodec.scala │ │ ├── dsl.scala │ │ └── BodyFactory.scala ├── effect │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── effect │ │ │ ├── Close.scala │ │ │ ├── io │ │ │ ├── JavaIO.scala │ │ │ └── FileIO.scala │ │ │ ├── Var.scala │ │ │ ├── syntax.scala │ │ │ └── Reporter.scala │ │ └── test │ │ └── scala │ │ └── korolev │ │ └── effect │ │ ├── io │ │ └── JavaIOSpec.scala │ │ ├── AsyncTableSpec.scala │ │ ├── QueueSpec.scala │ │ └── StreamSpec.scala ├── http │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ ├── effect │ │ └── io │ │ │ ├── package.scala │ │ │ └── DataSocket.scala │ │ └── http │ │ └── HttpServer.scala ├── korolev │ └── src │ │ ├── test │ │ └── scala │ │ │ └── korolev │ │ │ ├── testExecution.scala │ │ │ ├── JsCodeSpec.scala │ │ │ └── FormDataCodecSpec.scala │ │ └── main │ │ ├── scala │ │ └── korolev │ │ │ ├── Metrics.scala │ │ │ ├── state │ │ │ ├── StateSerializer.scala │ │ │ ├── package.scala │ │ │ ├── StateDeserializer.scala │ │ │ ├── StateManager.scala │ │ │ ├── javaSerialization.scala │ │ │ └── IdGenerator.scala │ │ │ ├── server │ │ │ ├── internal │ │ │ │ ├── Cookies.scala │ │ │ │ ├── BadRequestException.scala │ │ │ │ ├── package.scala │ │ │ │ ├── services │ │ │ │ │ ├── CommonService.scala │ │ │ │ │ ├── ServerSideRenderingService.scala │ │ │ │ │ └── FilesService.scala │ │ │ │ └── Html5RenderContext.scala │ │ │ ├── KorolevService.scala │ │ │ ├── StateLoader.scala │ │ │ ├── KorolevServiceConfig.scala │ │ │ └── package.scala │ │ │ ├── internal │ │ │ ├── package.scala │ │ │ ├── EventRegistry.scala │ │ │ └── DevMode.scala │ │ │ ├── Qsid.scala │ │ │ ├── util │ │ │ ├── Lens.scala │ │ │ ├── HtmlUtil.scala │ │ │ └── JsCode.scala │ │ │ ├── package.scala │ │ │ ├── Router.scala │ │ │ ├── Component.scala │ │ │ └── Extension.scala │ │ ├── es6 │ │ ├── utils.js │ │ ├── launcher.js │ │ └── bridge.js │ │ └── protocol.md ├── bytes │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── data │ │ ├── BytesReader.scala │ │ └── syntax.scala ├── testkit │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── testkit │ │ │ └── Action.scala │ │ └── test │ │ └── scala │ │ └── PseudoHtmlSpec.scala ├── web │ └── src │ │ ├── test │ │ └── scala │ │ │ └── korolev │ │ │ └── web │ │ │ └── RequestSpec.scala │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── web │ │ ├── FormData.scala │ │ ├── Response.scala │ │ └── Headers.scala └── standalone │ └── src │ └── main │ └── scala │ └── korolev │ └── server │ ├── KorolevApp.scala │ └── standalone.scala ├── .gitignore ├── interop ├── zio-http │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── zio │ │ └── http │ │ └── HttpStatusConverter.scala ├── ce3 │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── cats │ │ ├── package.scala │ │ └── IO3Effect.scala ├── monix │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── monix │ │ └── package.scala ├── akka │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── akka │ │ │ ├── SimpleAkkaHttpKorolevApp.scala │ │ │ ├── AkkaHttpServerConfig.scala │ │ │ ├── util │ │ │ ├── LoggingReporter.scala │ │ │ ├── Countdown.scala │ │ │ ├── KorolevStreamSubscriber.scala │ │ │ ├── AkkaByteStringBytesLike.scala │ │ │ └── KorolevStreamPublisher.scala │ │ │ └── instances.scala │ │ └── test │ │ └── scala │ │ └── CountdownSpec.scala ├── fs2-ce3 │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── fs2.scala │ │ └── test │ │ └── scala │ │ └── Fs2InteropSpec.scala ├── zio-streams │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── zio │ │ │ └── streams │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── korolev │ │ └── zio │ │ └── streams │ │ └── ZIOStreamsInteropTest.scala ├── fs2-ce2 │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── fs2.scala │ │ └── test │ │ └── scala │ │ └── Fs2InteropSpec.scala ├── slf4j │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── slf4j │ │ └── Slf4jReporter.scala ├── zio2-streams │ └── src │ │ ├── main │ │ └── scala │ │ │ └── korolev │ │ │ └── zio │ │ │ └── streams │ │ │ └── package.scala │ │ └── test │ │ └── scala │ │ └── korolev │ │ └── zio │ │ └── streams │ │ └── ZIOStreamsInteropTest.scala ├── zio │ └── src │ │ └── main │ │ └── scala │ │ └── korolev │ │ └── zio │ │ └── package.scala └── ce2 │ └── src │ └── main │ └── scala │ └── korolev │ └── cats │ └── package.scala ├── .github └── workflows │ └── scala.yml └── README.md /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | maxColumn = 120 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /examples/routing/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | loglevel = DEBUG 3 | } -------------------------------------------------------------------------------- /docs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | ./build.sh 4 | scp target/* fomkin.org:/var/www/html/korolev/ 5 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.defaultLogLevel=error 2 | -------------------------------------------------------------------------------- /examples/file-streaming/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.http.server.parsing.max-content-length = 1G 2 | akka.loglevel = "DEBUG" -------------------------------------------------------------------------------- /examples/routing/src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fomkin/korolev/HEAD/examples/routing/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /docs/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p target 4 | asciidoctor -a toc=left user-guide.adoc -o target/user-guide.html 5 | asciidoctor-pdf user-guide.adoc -o target/user-guide.pdf 6 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/gp/package.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | 3 | package object gp { 4 | 5 | implicit val actorSystem: ActorSystem = ActorSystem() 6 | 7 | } 8 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/tools/Step.scala: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import org.openqa.selenium.WebDriver 4 | 5 | case class Step(caption: String, lambda: WebDriver => StepResult) 6 | -------------------------------------------------------------------------------- /modules/web-dsl/src/main/scala/korolev/web/dsl/JsonCodec.scala: -------------------------------------------------------------------------------- 1 | package korolev.web.dsl 2 | 3 | trait JsonCodec[J] { 4 | def encode(json: J): String 5 | def decode(source: String): J 6 | } 7 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/BenchmarkConfiguration.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | case class BenchmarkConfiguration(host: String, port: Int, path: String, ssl: Boolean, testers: Int) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea* 3 | target/ 4 | .cache 5 | .classpath 6 | .project 7 | .settings/ 8 | *.iml 9 | 10 | # vscode 11 | /.vscode 12 | 13 | # metals 14 | .metals 15 | .bloop 16 | project/**/metals.sbt 17 | 18 | .bsp 19 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/Scenario.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | case class Scenario(name: String, steps: Vector[ScenarioStep]) { 4 | def newState: ScenarioState = ScenarioState(this, 0) 5 | } 6 | 7 | object Scenario 8 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/tools/StepResult.scala: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | sealed trait StepResult 4 | 5 | object StepResult { 6 | case class CowardlySkipped(reason: String) extends StepResult 7 | case class Error(cause: Throwable) extends StepResult 8 | case object Ok extends StepResult 9 | } 10 | -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/Close.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect 2 | 3 | trait Close[F[_], -T] { 4 | def onClose(that: T): F[Unit] 5 | def close(that: T): F[Unit] 6 | } 7 | 8 | object Close { 9 | def apply[F[_], T](implicit ev: Close[F, T]): Close[F, T] = 10 | implicitly[Close[F, T]] 11 | } 12 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/ScenarioStep.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | sealed trait ScenarioStep 4 | 5 | object ScenarioStep { 6 | case class Send(name: Option[String], value: ToServer) extends ScenarioStep 7 | case class Expect(name: Option[String], value: FromServer) extends ScenarioStep 8 | } 9 | -------------------------------------------------------------------------------- /interop/zio-http/src/main/scala/korolev/zio/http/HttpStatusConverter.scala: -------------------------------------------------------------------------------- 1 | package korolev.zio.http 2 | 3 | import korolev.web.Response.{Status => KStatus} 4 | import zio.http.Status 5 | 6 | object HttpStatusConverter { 7 | 8 | def fromKorolevStatus(kStatus: KStatus): Status = 9 | Status.fromInt(kStatus.code).orNull 10 | } 11 | -------------------------------------------------------------------------------- /examples/routing/src/main/resources/static/main.css: -------------------------------------------------------------------------------- 1 | .todos { 2 | height: 250px; 3 | overflow-y: scroll; 4 | } 5 | 6 | .checkbox { 7 | display: inline-block; 8 | width: 10px; 9 | height: 10px; 10 | border: 1px solid green; 11 | border-radius: 10px; 12 | margin-right: 5px; 13 | } 14 | 15 | .checkbox__checked { 16 | background-color: green; 17 | } 18 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.http { 2 | host-connection-pool { 3 | max-connections = 1024 4 | max-open-requests = 1024 5 | } 6 | client { 7 | connecting-timeout = 60 seconds 8 | idle-timeout = 60 seconds 9 | } 10 | } 11 | 12 | benchmark { 13 | host = "localhost" 14 | port = "8000" 15 | path = "/" 16 | ssl = false 17 | testers = 128 18 | } -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") 2 | 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8") 4 | 5 | addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.1") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-git" % "1.0.0") 8 | 9 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.0.0") 10 | 11 | libraryDependencies += "com.google.javascript" % "closure-compiler" % "v20190106" 12 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/Error.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | sealed trait Error 4 | 5 | object Error { 6 | case class ArbitraryThrowable(e: Throwable) extends Error 7 | case class InvalidHttpStatusCodeForPage(code: Int) extends Error 8 | case class InvalidHttpStatusCodeForWS(code: Int) extends Error 9 | case object SessionIdNotDefined extends Error 10 | case object DeviceIdNotDefined extends Error 11 | } 12 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/resources/static/main.css: -------------------------------------------------------------------------------- 1 | .todos { 2 | height: 250px; 3 | overflow-y: scroll; 4 | } 5 | 6 | .todo_checkbox { 7 | display: inline-block; 8 | width: 10px; 9 | height: 10px; 10 | border: 1px solid green; 11 | border-radius: 10px; 12 | margin-right: 5px; 13 | } 14 | 15 | .todo_checkbox__checked { 16 | background-color: green; 17 | } 18 | 19 | .todo__finished { 20 | text-decoration: line-through; 21 | } 22 | -------------------------------------------------------------------------------- /modules/http/src/main/scala/korolev/effect/io/package.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect 2 | 3 | import java.nio.channels.CompletionHandler 4 | 5 | package object io { 6 | 7 | private[io] def completionHandler[T](cb: Either[Throwable, T] => Unit): CompletionHandler[T, Unit] = 8 | new CompletionHandler[T, Unit] { 9 | def completed(v: T, a: Unit): Unit = cb(Right(v)) 10 | def failed(error: Throwable, a: Unit): Unit = cb(Left(error)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /modules/korolev/src/test/scala/korolev/testExecution.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import scala.concurrent.ExecutionContext 4 | 5 | object testExecution { 6 | 7 | private class RunNowExecutionContext extends ExecutionContext { 8 | def execute(runnable: Runnable): Unit = runnable.run() 9 | def reportFailure(cause: Throwable): Unit = cause.printStackTrace() 10 | } 11 | 12 | implicit val defaultExecutor: ExecutionContext = new RunNowExecutionContext() 13 | } 14 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/ScenarioState.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | case class ScenarioState(scenario: Scenario, step: Int) { 4 | 5 | def current: Option[ScenarioStep] = 6 | if (endReached) None 7 | else Some(scenario.steps(step)) 8 | 9 | def next: ScenarioState = 10 | if (endReached) this 11 | else copy(step = step + 1) 12 | 13 | def endReached: Boolean = 14 | step == scenario.steps.length 15 | } 16 | 17 | object ScenarioState 18 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/tools/Caps.scala: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import org.openqa.selenium.remote.DesiredCapabilities 4 | 5 | case class Caps(desiredCapabilities: DesiredCapabilities) 6 | 7 | object Caps { 8 | 9 | def apply(builder: () => DesiredCapabilities)(capabilities: (String, Any)*): Caps = { 10 | val caps = builder() 11 | capabilities foreach { 12 | case (k, v) => 13 | caps.setCapability(k, v) 14 | } 15 | new Caps(caps) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/Report.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | sealed trait Report 4 | 5 | object Report { 6 | case class Unexpected(state: ScenarioState, expected: FromServer, gotten: FromServer) extends Report 7 | case class Success(scenario: Scenario, metrics: Map[Int, Long]) extends Report 8 | case class CantRunScenario(scenario: Scenario) extends Report 9 | case class MessagesFromClosedConnection(message: FromServer) extends Report 10 | case object SuddenlyClosed extends Report 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 11 17 | uses: actions/setup-java@v2 18 | with: 19 | java-version: '11' 20 | distribution: 'temurin' 21 | - name: coursier-cache-action 22 | uses: coursier/cache-action@v6.3 23 | - name: Run tests 24 | run: sbt +test 25 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/Metrics.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import java.util.concurrent.atomic.AtomicLong 4 | 5 | object Metrics { 6 | 7 | sealed trait Metric[T] 8 | 9 | final class LongMetric(name: String) extends Metric[Long] { 10 | private val ref = new AtomicLong(0) 11 | def update(f: Long => Long): Unit = ref.updateAndGet(x => f(x)) 12 | def get: Long = ref.get() 13 | } 14 | 15 | // Diff time includes changes inference and preparation of the message for a browser. 16 | final val MinDiffNanos = new LongMetric("min-diff-nanos") 17 | final val MaxDiffNanos = new LongMetric("max-diff-nanos") 18 | } -------------------------------------------------------------------------------- /modules/bytes/src/main/scala/korolev/data/BytesReader.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | object BytesReader { 4 | 5 | def readByte[T: BytesLike](bytes: T, i: Long): Byte = 6 | BytesLike[T].get(bytes, i) 7 | 8 | def readShort[T: BytesLike](bytes: T, i: Long): Short = 9 | ((readByte(bytes, i) & 0xFF) << 8 | (readByte(bytes, i + 1) & 0xFF)).toShort 10 | 11 | def readInt[T: BytesLike](bytes: T, i: Long): Int = 12 | ((readShort(bytes, i) & 0xFFFF) << 16) | (readShort(bytes, i + 2) & 0xFFFF) 13 | 14 | def readLong[T: BytesLike](bytes: T, i: Long): Long = 15 | ((readInt(bytes, i) & 0xFFFFFFFFL) << 32) | (readInt(bytes, i + 4) & 0xFFFFFFFFL) 16 | } 17 | -------------------------------------------------------------------------------- /modules/effect/src/test/scala/korolev/effect/io/JavaIOSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect.io 2 | 3 | import java.io.ByteArrayInputStream 4 | 5 | import org.scalatest.flatspec.AsyncFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class JavaIOSpec extends AsyncFlatSpec with Matchers { 9 | 10 | final val inputStream1Length = 239978 11 | final val inputStream1 = new ByteArrayInputStream(Array.fill[Byte](inputStream1Length)(1)) 12 | 13 | "JavaIO.fromInputStream" should "return exactly same bytes as contains in InputStream" in { 14 | JavaIO.fromInputStream(inputStream1).flatMap(_.fold(Array.empty[Byte])(_ ++ _)).map { bytes => 15 | bytes.length shouldEqual inputStream1Length 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/StateSerializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.state 18 | 19 | trait StateSerializer[T] { 20 | def serialize(value: T): Array[Byte] 21 | } 22 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | package object state { 20 | 21 | type DeviceId = String 22 | type SessionId = String 23 | } 24 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/Cookies.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal 18 | 19 | private[korolev] object Cookies { 20 | val DeviceId = "deviceId" 21 | } 22 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/StateDeserializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.state 18 | 19 | trait StateDeserializer[T] { 20 | def deserialize(data: Array[Byte]): Option[T] 21 | } 22 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/BadRequestException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal 18 | 19 | private[korolev] case class BadRequestException(message: String) 20 | extends Throwable(message) 21 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/internal/package.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import scala.annotation.switch 4 | import scala.collection.mutable 5 | 6 | package object internal { 7 | 8 | private[korolev] def jsonEscape(sb: mutable.StringBuilder, s: String, unicode: Boolean): Unit = { 9 | var i = 0 10 | val len = s.length 11 | while (i < len) { 12 | (s.charAt(i): @switch) match { 13 | case '"' => sb.append("\\\"") 14 | case '\\' => sb.append("\\\\") 15 | case '\b' => sb.append("\\b") 16 | case '\f' => sb.append("\\f") 17 | case '\n' => sb.append("\\n") 18 | case '\r' => sb.append("\\r") 19 | case '\t' => sb.append("\\t") 20 | case c => 21 | if (c < ' ' || (c > '~' && unicode)) sb.append("\\u%04x" format c.toInt) 22 | else sb.append(c) 23 | } 24 | i += 1 25 | } 26 | () 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /interop/ce3/src/main/scala/korolev/cats/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import _root_.cats.effect.unsafe.IORuntime 20 | 21 | package object cats { 22 | 23 | implicit final val withDefaultRuntime: IO3Effect = 24 | new IO3Effect(IORuntime.global) 25 | } 26 | -------------------------------------------------------------------------------- /modules/http/src/main/scala/korolev/effect/io/DataSocket.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect.io 2 | 3 | import korolev.effect.io.DataSocket.CloseReason 4 | import korolev.effect.{Close, Effect, Stream} 5 | import korolev.effect.syntax._ 6 | 7 | trait DataSocket[F[_], B] { 8 | def stream: Stream[F, B] 9 | def onClose(): F[CloseReason] 10 | def write(bytes: B): F[Unit] 11 | } 12 | 13 | object DataSocket { 14 | 15 | sealed trait CloseReason 16 | 17 | object CloseReason { 18 | case object ByPeer extends CloseReason 19 | case object StreamCanceled extends CloseReason 20 | case class Error(e: Throwable) extends CloseReason 21 | } 22 | 23 | implicit def dataSocketClose[F[_]: Effect, B]: Close[F, DataSocket[F, B]] = 24 | new Close[F, DataSocket[F, B]] { 25 | def onClose(that: DataSocket[F, B]): F[Unit] = 26 | that.onClose().unit 27 | def close(that: DataSocket[F, B]): F[Unit] = 28 | that.stream.cancel() 29 | } 30 | } -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/Qsid.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import korolev.state.{DeviceId, SessionId} 20 | 21 | /** 22 | * Qualified Session Identifier 23 | */ 24 | case class Qsid(deviceId: DeviceId, sessionId: SessionId) { 25 | override lazy val toString: String = 26 | s"$deviceId-$sessionId" 27 | } 28 | -------------------------------------------------------------------------------- /modules/testkit/src/main/scala/korolev/testkit/Action.scala: -------------------------------------------------------------------------------- 1 | package korolev.testkit 2 | 3 | import korolev.Context.ElementId 4 | 5 | sealed trait Action[+F[_], +S, +M] { 6 | def transition(f: S => Boolean): Boolean = this match { 7 | case Action.Transition(newState) => f(newState) 8 | case _ => false 9 | } 10 | } 11 | 12 | object Action { 13 | 14 | case class Transition[T](newState: T) extends Action[Nothing, T, Nothing] 15 | case class PropertySet(element: ElementId, name: String, value: String) extends Action[Nothing, Nothing, Nothing] 16 | case class Focus(element: ElementId) extends Action[Nothing, Nothing, Nothing] 17 | case class ResetForm(element: ElementId) extends Action[Nothing, Nothing, Nothing] 18 | case class Publish[T](message: T) extends Action[Nothing, Nothing, T] 19 | case class RegisterCallback[F[_]](name: String, f: String => F[Unit]) extends Action[F, Nothing, Nothing] 20 | case class EvalJs(result: Either[Throwable, String]) extends Action[Nothing, Nothing, Nothing] 21 | } 22 | -------------------------------------------------------------------------------- /interop/monix/src/main/scala/korolev/monix/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import _root_.monix.execution.Scheduler 20 | import _root_.monix.eval.Task 21 | 22 | import korolev.effect.Effect 23 | 24 | package object monix { 25 | 26 | implicit final def monixTaskEffect(implicit scheduler: Scheduler): Effect[Task] = 27 | new MonixTaskEffect() 28 | } 29 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/package.scala: -------------------------------------------------------------------------------- 1 | package korolev.server 2 | 3 | import korolev.data.Bytes 4 | 5 | import java.nio.charset.StandardCharsets 6 | import korolev.effect.{Effect, Stream} 7 | import korolev.effect.syntax._ 8 | 9 | import korolev.web.Response 10 | import korolev.web.Response.Status 11 | 12 | package object internal { 13 | 14 | def HttpResponse[F[_]: Effect](status: Status): HttpResponse[F] = { 15 | new Response(status, Stream.empty, Nil, Some(0L)) 16 | } 17 | 18 | def HttpResponse[F[_]: Effect](status: Status, 19 | body: Array[Byte], headers: Seq[(String, String)]): F[HttpResponse[F]] = 20 | Stream(Bytes.wrap(body)) 21 | .mat[F]() 22 | .map(lb => new Response(status, lb, headers, Some(body.length.toLong))) 23 | 24 | def HttpResponse[F[_]: Effect](status: Status, message: String, headers: Seq[(String, String)]): F[HttpResponse[F]] = 25 | HttpResponse(status, message.getBytes(StandardCharsets.UTF_8), headers) 26 | } 27 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/KorolevService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server 18 | 19 | trait KorolevService[F[_]] { 20 | 21 | /** 22 | * Process HTTP request 23 | */ 24 | def http(request: HttpRequest[F]): F[HttpResponse[F]] 25 | 26 | /** 27 | * Process WebSocket requests 28 | */ 29 | def ws(request: WebSocketRequest[F]): F[WebSocketResponse[F]] 30 | } -------------------------------------------------------------------------------- /examples/event-data/src/main/scala/EventDataExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.server._ 3 | import korolev.akka._ 4 | import korolev.state.javaSerialization._ 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | 8 | object EventDataExample extends SimpleAkkaHttpKorolevApp { 9 | 10 | val globalContext = Context[Future, String, Any] 11 | 12 | import globalContext._ 13 | import levsha.dsl._ 14 | import html._ 15 | 16 | val service = akkaHttpService { 17 | KorolevServiceConfig [Future, String, Any] ( 18 | stateLoader = StateLoader.default("nothing"), 19 | document = json => optimize { 20 | Html( 21 | body( 22 | input( 23 | `type` := "text", 24 | event("keydown") { access => 25 | access.eventData.flatMap { eventData => 26 | access.transition(_ => eventData) 27 | } 28 | } 29 | ), 30 | pre(json) 31 | ) 32 | ) 33 | } 34 | ) 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /modules/web/src/test/scala/korolev/web/RequestSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev.web 2 | 3 | import korolev.web.Request.Method 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class RequestSpec extends AnyFlatSpec with Matchers { 8 | 9 | private val defaultRequest = Request(Method.Get, PathAndQuery.Root, Nil, None, ()) 10 | 11 | "parsedCookies" should "parse cookies in correct format" in { 12 | val request = defaultRequest.copy(renderedCookie = "a=1;b=2") 13 | request.cookie("a") shouldEqual Some("1") 14 | request.cookie("b") shouldEqual Some("2") 15 | } 16 | 17 | it should "parse cookies without values" in { 18 | val request = defaultRequest.copy(renderedCookie = "a;b=2") 19 | request.cookie("a") shouldEqual Some("") 20 | request.cookie("b") shouldEqual Some("2") 21 | } 22 | 23 | it should "parse cookies in invalid format" in { 24 | val request = defaultRequest.copy(renderedCookie = "a=1=2;b=2") 25 | request.cookie("a") shouldEqual None 26 | request.cookie("b") shouldEqual Some("2") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/util/Lens.scala: -------------------------------------------------------------------------------- 1 | package korolev.util 2 | 3 | /** 4 | * Very simple all-in-one optic. 5 | */ 6 | case class Lens[S, S2](read: PartialFunction[S, S2], 7 | write: PartialFunction[(S, S2), S]) { 8 | 9 | def get(that: S): Option[S2] = 10 | read.lift(that) 11 | 12 | def modify(that: S)(f: S2 => S2): S = 13 | read.lift(that).fold(that) { s2 => 14 | write(that, f(s2)) 15 | } 16 | 17 | def update(that: S, value: S2): S = 18 | write.lift(that, value).getOrElse(that) 19 | 20 | def focus[S3](read: PartialFunction[S2, S3], 21 | write: PartialFunction[(S2, S3), S2]): Lens[S, S3] = { 22 | val composedWrite: (S, S3) => Option[S] = { (s, s3) => 23 | for { 24 | origS2 <- this.read.lift(s) 25 | updatedS2 <- write.lift(origS2, s3) 26 | updatedS <- this.write.lift(s, updatedS2) 27 | } yield updatedS 28 | } 29 | Lens(this.read.andThen(read), Function.unlift(composedWrite.tupled)) 30 | } 31 | 32 | def ++[S3](lens: Lens[S2, S3]): Lens[S, S3] = focus(lens.read, lens.write) 33 | } 34 | -------------------------------------------------------------------------------- /modules/korolev/src/test/scala/korolev/JsCodeSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import korolev.Context.ElementId 4 | import korolev.util.JsCode 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class JsCodeSpec extends AnyFlatSpec with Matchers { 9 | 10 | import JsCode._ 11 | 12 | "JsCode.apply" should "construct correct list" in { 13 | val el1 = new ElementId(Some("el1")) 14 | val el2 = new ElementId(Some("el2")) 15 | val jsCode = JsCode(List("--", "++", "//"), List(el1, el2)) 16 | 17 | jsCode should equal(Part("--", Element(el1, Part("++", Element(el2, Part("//", End)))))) 18 | } 19 | 20 | "jsCode.mkString" should "construct correct string" in { 21 | val el1 = new ElementId(Some("el1")) 22 | val el2 = new ElementId(Some("el2")) 23 | val el2id: ElementId => levsha.Id = { 24 | case `el1` => levsha.Id("1_1") 25 | case `el2` => levsha.Id("1_2") 26 | } 27 | val jsCode = "swapElements(" :: el1 :: ", " :: el2 :: ");" :: End 28 | 29 | jsCode.mkString(el2id) should equal("swapElements(Korolev.element('1_1'), Korolev.element('1_2'));") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/akka-http/src/main/scala/AkkaHttpExample.scala: -------------------------------------------------------------------------------- 1 | import akka.actor.ActorSystem 2 | import akka.http.scaladsl.Http 3 | import korolev._ 4 | import korolev.akka._ 5 | import korolev.server._ 6 | import korolev.state.javaSerialization._ 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.Future 10 | 11 | object AkkaHttpExample extends App { 12 | 13 | private implicit val actorSystem: ActorSystem = ActorSystem() 14 | 15 | val applicationContext = Context[Future, Boolean, Any] 16 | 17 | import applicationContext._ 18 | import levsha.dsl._ 19 | import html.{body, button, Html} 20 | 21 | private val config = KorolevServiceConfig[Future, Boolean, Any]( 22 | stateLoader = StateLoader.default(false), 23 | document = s => optimize { 24 | Html( 25 | body( 26 | s"Hello akka-http: $s", 27 | button("Click me!", 28 | event("click")(_.transition(!_)) 29 | ) 30 | ) 31 | ) 32 | } 33 | ) 34 | 35 | private val route = akkaHttpService(config).apply(AkkaHttpServerConfig()) 36 | 37 | Http().newServerAt("0.0.0.0", 8080).bindFlow(route) 38 | } 39 | -------------------------------------------------------------------------------- /examples/zio/src/test/scala/ZioExampleSpec.scala: -------------------------------------------------------------------------------- 1 | import korolev.effect.Effect 2 | import org.scalatest.flatspec.AsyncFlatSpec 3 | import org.scalatest.matchers.should.Matchers 4 | import korolev.testkit.* 5 | import zio.Task 6 | 7 | class ZioExampleSpec extends AsyncFlatSpec with Matchers { 8 | 9 | import ZioExample._ 10 | 11 | private implicit val taskEffectInstance: Effect[Task] = 12 | korolev.zio.taskEffectInstance(zio.Runtime.default) 13 | 14 | private val browser = Browser() 15 | .value(aInput, "2") 16 | .value(bInput, "3") 17 | 18 | "onChange" should "read inputs and put calculation result to the view state" in Effect[Task].toFuture { 19 | browser 20 | .access[Task, Option[Int], Any](Option.empty[Int], onChange) 21 | .map { actions => 22 | actions shouldEqual List( 23 | Action.Transition(Some(5)) 24 | ) 25 | } 26 | } 27 | 28 | it should "be handled" in Effect[Task].toFuture { 29 | browser.event(Option.empty[Int], 30 | renderForm(None), 31 | "input", 32 | _.byName("a-input").headOption.map(_.id)) map { actions => 33 | actions shouldEqual List( 34 | Action.Transition(Some(5)) 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/StateManager.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.state 18 | 19 | import korolev.effect.Effect 20 | import levsha.Id 21 | 22 | abstract class StateManager[F[_]: Effect] { self => 23 | def snapshot: F[StateManager.Snapshot] 24 | def read[T: StateDeserializer](nodeId: Id): F[Option[T]] 25 | def delete(nodeId: Id): F[Unit] 26 | def write[T: StateSerializer](nodeId: Id, value: T): F[Unit] 27 | } 28 | 29 | object StateManager { 30 | 31 | trait Snapshot { 32 | def apply[T: StateDeserializer](nodeId: Id): Option[T] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/io/JavaIO.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect.io 2 | 3 | import korolev.data.BytesLike 4 | import korolev.effect.{Effect, Stream} 5 | 6 | import java.io.InputStream 7 | import scala.annotation.tailrec 8 | 9 | object JavaIO { 10 | 11 | def fromInputStream[F[_] : Effect, B: BytesLike](inputStream: InputStream, chunkSize: Int = 8192 * 2): F[Stream[F, B]] = { 12 | @tailrec 13 | def readStream(chunk: Array[Byte], offset: Int, len: Int): (Unit, Option[B]) = { 14 | val read = inputStream.read(chunk, offset, len) 15 | if (read == len) { 16 | ((), Some(BytesLike[B].wrapArray(chunk))) 17 | } else { 18 | readStream(chunk, offset + read, len - read) 19 | } 20 | } 21 | 22 | Stream.unfoldResource[F, InputStream, Unit, B]( 23 | default = (), 24 | create = Effect[F].pure(inputStream), 25 | loop = (inputStream, _) => Effect[F].delay { 26 | if (inputStream.available() > 0) { 27 | val len = Math.min(inputStream.available(), chunkSize) 28 | val chunk = new Array[Byte](len) 29 | readStream(chunk, 0, len) 30 | } else { 31 | ((), None) 32 | } 33 | } 34 | ) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/SimpleAkkaHttpKorolevApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka 18 | 19 | import akka.actor.ActorSystem 20 | import akka.http.scaladsl.Http 21 | 22 | abstract class SimpleAkkaHttpKorolevApp(config: AkkaHttpServerConfig = null) { 23 | 24 | implicit val actorSystem: ActorSystem = ActorSystem() 25 | 26 | def service: AkkaHttpService 27 | 28 | def main(args: Array[String]): Unit = { 29 | val escapedConfig = 30 | if (config == null) AkkaHttpServerConfig() 31 | else config 32 | val route = service(escapedConfig) 33 | Http().newServerAt("0.0.0.0", 8080).bindFlow(route) 34 | () 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /interop/fs2-ce3/src/main/scala/korolev/fs2.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import _root_.cats.effect.kernel.Concurrent 4 | import _root_.fs2.{Stream => Fs2Stream} 5 | import korolev.effect.syntax._ 6 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | object fs2 { 11 | 12 | implicit class Fs2StreamOps[F[_] : KorolevEffect : Concurrent, O](stream: Fs2Stream[F, O]) { 13 | 14 | def toKorolev(bufferSize: Int = 1)(implicit ec: ExecutionContext): F[KorolevStream[F, O]] = { 15 | val queue = new Queue[F, O](bufferSize) 16 | val cancelToken: Either[Throwable, Unit] = Right(()) 17 | 18 | KorolevEffect[F] 19 | .start( 20 | stream 21 | .interruptWhen(queue.cancelSignal.as(cancelToken)) 22 | .evalMap(queue.enqueue) 23 | .compile 24 | .drain 25 | .flatMap(_ => queue.stop()) 26 | ) 27 | .as(queue.stream) 28 | } 29 | } 30 | 31 | implicit class KorolevStreamOps[F[_] : KorolevEffect, O](stream: KorolevStream[F, O]) { 32 | def toFs2: Fs2Stream[F, O] = 33 | Fs2Stream.unfoldEval(()) { _ => 34 | stream 35 | .pull() 36 | .map(mv => mv.map(v => (v, ()))) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /interop/zio-streams/src/main/scala/korolev/zio/streams/package.scala: -------------------------------------------------------------------------------- 1 | package korolev.zio 2 | 3 | import korolev.effect.{Effect as KorolevEffect, Stream as KorolevStream} 4 | import zio.stream.ZStream 5 | import zio.{Chunk, RIO, ZIO, ZManaged} 6 | 7 | package object streams { 8 | 9 | 10 | implicit class KorolevSreamOps[R, O](stream: KorolevStream[RIO[R, *], O]) { 11 | 12 | def toZStream: ZStream[R, Throwable, O] = { 13 | ZStream.unfoldM(()) { _ => 14 | stream 15 | .pull() 16 | .map(mv => mv.map(v => (v, ()))) 17 | } 18 | } 19 | } 20 | 21 | implicit class ZStreamOps[R, O](stream: ZStream[R, Throwable, O]) { 22 | 23 | type F[A] = RIO[R, A] 24 | 25 | def toKorolev(implicit eff: KorolevEffect[F]): ZManaged[R, Throwable, KorolevStream[F, Seq[O]]] = 26 | stream.process.map { zPull => 27 | new ZKorolevStream(zPull) 28 | } 29 | } 30 | 31 | private[streams] class ZKorolevStream[R, O] 32 | ( 33 | zPull: ZIO[R, Option[Throwable], Chunk[O]] 34 | )(implicit eff: KorolevEffect[RIO[R, *]]) extends KorolevStream[RIO[R, *], Seq[O]] { 35 | 36 | def pull(): RIO[R, Option[Seq[O]]] = 37 | zPull.option 38 | 39 | def cancel(): RIO[R, Unit] = 40 | ZIO.dieMessage("Can't cancel ZStream from Korolev") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/Var.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.effect 18 | 19 | import java.util.concurrent.atomic.AtomicReference 20 | 21 | final class Var[F[_]: Effect, T](initialValue: T) { 22 | private val casRef = new AtomicReference[T](initialValue) 23 | def set(value: T): F[Unit] = 24 | Effect[F].delay(casRef.set(value)) 25 | def get: F[T] = 26 | Effect[F].delay(casRef.get) 27 | def compareAndSet(expected: T, value: T): F[Boolean] = 28 | Effect[F].delay(casRef.compareAndSet(expected, value)) 29 | } 30 | 31 | object Var { 32 | def apply[F[_]: Effect, T](initialValue: T): Var[F, T] = 33 | new Var(initialValue) 34 | } 35 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/tools/SauceLabsClient.scala: -------------------------------------------------------------------------------- 1 | package tools 2 | 3 | import java.util.Base64 4 | 5 | import akka.actor.ActorSystem 6 | import akka.http.scaladsl.Http 7 | import akka.http.scaladsl.model._ 8 | import akka.http.scaladsl.model.headers._ 9 | 10 | import scala.concurrent.Await 11 | import scala.concurrent.duration._ 12 | 13 | class SauceLabsClient(userName: String, accessKey: String, jobId: String)(implicit actorSystem: ActorSystem) { 14 | 15 | def setName(name: String): Unit = { 16 | putToJob(s"""{"name": "$name"}""") 17 | } 18 | 19 | def setPassed(passed: Boolean): Unit = { 20 | putToJob(s"""{"passed": $passed}""") 21 | } 22 | 23 | def putToJob(data: String): Unit = { 24 | val authorization = { 25 | val s = s"$userName:$accessKey" 26 | Base64.getEncoder.encodeToString(s.getBytes) 27 | } 28 | 29 | val request = HttpRequest( 30 | uri = s"https://saucelabs.com/rest/v1/$userName/jobs/$jobId", 31 | method = HttpMethods.PUT, 32 | headers = List(Authorization(BasicHttpCredentials(authorization))), 33 | entity = HttpEntity(ContentTypes.`application/json`, data.getBytes) 34 | ) 35 | 36 | val response = Http() 37 | .singleRequest(request) 38 | 39 | Await.result(response, 10 seconds) 40 | () 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/focus/src/main/scala/FocusExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.akka._ 3 | import scala.concurrent.ExecutionContext.Implicits.global 4 | import korolev.server._ 5 | 6 | import scala.concurrent.Future 7 | import korolev.state.javaSerialization._ 8 | 9 | object FocusExample extends SimpleAkkaHttpKorolevApp { 10 | 11 | val globalContext = Context[Future, Boolean, Any] 12 | 13 | import globalContext._ 14 | import levsha.dsl._ 15 | import html._ 16 | 17 | // Handler to input 18 | val inputId = elementId() 19 | 20 | val service: AkkaHttpService = akkaHttpService { 21 | KorolevServiceConfig[Future, Boolean, Any]( 22 | stateLoader = StateLoader.default(false), 23 | document = _ => optimize { 24 | Html( 25 | body( 26 | div("Focus example"), 27 | div( 28 | input( 29 | inputId, 30 | `type` := "text", 31 | placeholder := "Wanna get some focus?" 32 | ) 33 | ), 34 | div( 35 | button( 36 | event("click") { access => 37 | access.focus(inputId) 38 | }, 39 | "Click to focus" 40 | ) 41 | ) 42 | ) 43 | ) 44 | } 45 | ) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/resources/static/debug-console.js: -------------------------------------------------------------------------------- 1 | window.document.addEventListener("DOMContentLoaded", function() { 2 | Korolev.setProtocolDebugEnabled(true); 3 | var display = document.createElement("pre"); 4 | display.setAttribute('style', 'height: 300px; overflow-y: scroll') 5 | display.innerHTML = '
Client log
'; 6 | document.body.appendChild(display); 7 | console.log = function() { 8 | var line = document.createElement("div"); 9 | for (var i = 0; i < arguments.length; i++) { 10 | if (arguments[i].indexOf("[0,0 ]") > -1) { 11 | document.getElementById('debug-log-label').textContent = 'Client log (connected)'; 12 | } 13 | line.textContent += arguments[i]; 14 | } 15 | display.appendChild(line); 16 | display.scrollTop = display.scrollHeight; 17 | }; 18 | console.error = function() { 19 | var line = document.createElement("div"); 20 | line.setAttribute("style", "color: red"); 21 | for (var i = 0; i < arguments.length; i++) 22 | line.textContent = arguments[i]; 23 | display.appendChild(line); 24 | }; 25 | document.body.addEventListener('click', function(e) { 26 | console.log('! click on ' + e.target); 27 | }); 28 | window.onerror = function(e) { 29 | console.error(e); 30 | }; 31 | }); 32 | -------------------------------------------------------------------------------- /interop/fs2-ce2/src/main/scala/korolev/fs2.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import _root_.cats.effect.{ConcurrentEffect => CatsConcurrentEffect} 4 | import _root_.fs2.{Stream => Fs2Stream} 5 | import korolev.effect.syntax._ 6 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 7 | 8 | import scala.concurrent.ExecutionContext 9 | 10 | object fs2 { 11 | 12 | implicit class Fs2StreamOps[F[_] : KorolevEffect : CatsConcurrentEffect, O](stream: Fs2Stream[F, O]) { 13 | 14 | def toKorolev(bufferSize: Int = 1)(implicit ec: ExecutionContext): F[KorolevStream[F, O]] = { 15 | val queue = new Queue[F, O](bufferSize) 16 | val cancelToken: Either[Throwable, Unit] = Right(()) 17 | 18 | KorolevEffect[F] 19 | .start( 20 | stream 21 | .interruptWhen(queue.cancelSignal.as(cancelToken)) 22 | .evalMap(queue.enqueue) 23 | .compile 24 | .drain 25 | .flatMap(_ => queue.stop()) 26 | ) 27 | .as(queue.stream) 28 | } 29 | } 30 | 31 | implicit class KorolevStreamOps[F[_] : KorolevEffect, O](stream: KorolevStream[F, O]) { 32 | def toFs2: Fs2Stream[F, O] = 33 | Fs2Stream.unfoldEval(()) { _ => 34 | stream 35 | .pull() 36 | .map(mv => mv.map(v => (v, ()))) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/package.scala: -------------------------------------------------------------------------------- 1 | import korolev.web.PathAndQuery 2 | 3 | /* 4 | * Copyright 2017-2020 Aleksey Fomkin 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package object korolev { 20 | 21 | // Routing API 22 | @inline val Root: PathAndQuery.Root.type = PathAndQuery.Root 23 | @inline val / : PathAndQuery./.type = PathAndQuery./ 24 | @inline val :? : PathAndQuery.:?.type = PathAndQuery.:? 25 | //@inline val :?? : PathAndQuery.:??.type = PathAndQuery.:?? 26 | @inline val :?* : PathAndQuery.:?*.type = PathAndQuery.:?* 27 | @inline val *& : PathAndQuery.*&.type = PathAndQuery.*& 28 | @inline val :& : PathAndQuery.:&.type = PathAndQuery.:& 29 | 30 | type Transition[S] = S => S 31 | type TransitionAsync[F[_], S] = S => F[S] 32 | } 33 | -------------------------------------------------------------------------------- /misc/integration-tests/src/main/scala/tools/package.scala: -------------------------------------------------------------------------------- 1 | 2 | import akka.actor.ActorSystem 3 | import org.openqa.selenium.{JavascriptExecutor, WebDriver, WebElement} 4 | import org.slf4j.LoggerFactory 5 | 6 | import scala.concurrent.duration._ 7 | 8 | package object tools { 9 | 10 | val logger = LoggerFactory.getLogger("tools") 11 | 12 | def step(caption: String)(lambda: WebDriver => StepResult) = 13 | Step(caption, lambda) 14 | 15 | def scenario(name: String)(steps: Step*)(implicit as: ActorSystem): Scenario = 16 | Scenario(name, steps) 17 | 18 | def assert(message: String, f: => Boolean) = { 19 | if (!f) { 20 | val exception = new AssertionError(message) 21 | throw exception 22 | } else { 23 | StepResult.Ok 24 | } 25 | } 26 | 27 | def fail(message: String): Unit = { 28 | throw new AssertionError(message) 29 | } 30 | 31 | def sleep(duration: FiniteDuration): Unit = { 32 | Thread.sleep(duration.toMillis) 33 | } 34 | 35 | implicit final class WebElementOps(val el: WebElement) extends AnyVal { 36 | def scrollTo()(implicit webDriver: WebDriver): Unit = webDriver match { 37 | case executor: JavascriptExecutor => 38 | executor.executeScript("arguments[0].scrollIntoView(true);", el) 39 | () 40 | case _ => 41 | throw new UnsupportedOperationException("WebDriver is not javascript executor") 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/ToServer.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | import korolev.internal.Frontend.CallbackType 4 | import ujson._ 5 | 6 | sealed trait ToServer 7 | 8 | object ToServer { 9 | 10 | case object Close extends ToServer 11 | 12 | case class Callback(tpe: CallbackType, data: Option[String]) extends ToServer 13 | 14 | object Callback { 15 | 16 | def apply(code: Int, data: String): Callback = { 17 | new Callback(CallbackType(code).get, Some(data)) 18 | } 19 | 20 | def fromJson(ast: Value): Either[String, Callback] = { 21 | ast match { 22 | case arr: Arr => 23 | arr.value.toList match { 24 | case Num(codeString) :: Nil => 25 | val code = codeString.toInt 26 | CallbackType(code) match { 27 | case None => Left(s"unknown callback #$code") 28 | case Some(callback) => Right(Callback(callback, None)) 29 | } 30 | case Num(codeString) :: Str(data) :: Nil => 31 | val code = codeString.toInt 32 | CallbackType(code) match { 33 | case None => Left(s"unknown callback #$code") 34 | case Some(callback) => Right(Callback(callback, Some(data))) 35 | } 36 | } 37 | case other => 38 | Left(s"Unexpected JSON #$other") 39 | } 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /modules/korolev/src/main/es6/utils.js: -------------------------------------------------------------------------------- 1 | export class ConnectionLostWidget { 2 | 3 | /** @param {string} template */ 4 | constructor(template) { 5 | /** @type {?Element} */ 6 | this._element = null; 7 | this._template = template; 8 | } 9 | 10 | show() { 11 | 12 | if (this._element !== null) 13 | return; 14 | 15 | // Parse template 16 | var element = document.createElement('div'); 17 | element.innerHTML = this._template 18 | .replace(/&/g, "&") 19 | .replace(/</g, "<") 20 | .replace(/>/g, ">") 21 | .replace(/"/g, "\"") 22 | .replace(/'/g, "'"); 23 | element = element.children[0]; 24 | 25 | // Append to document body 26 | document.body.appendChild(element); 27 | this._element = element; 28 | } 29 | 30 | hide() { 31 | if (this._element !== null) { 32 | document.body.removeChild(this._element); 33 | this._element = null; 34 | } 35 | } 36 | } 37 | 38 | export function encodeRFC5987ValueChars(str) { 39 | return encodeURIComponent(str). 40 | // Note that although RFC3986 reserves "!", RFC5987 does not, 41 | // so we do not need to escape it 42 | replace(/['()]/g, escape). // i.e., %27 %28 %29 43 | replace(/\*/g, '%2A'). 44 | // The following are not required for percent-encoding per RFC5987, 45 | // so we can allow for a little better readability over the wire: |`^ 46 | replace(/%(?:7C|60|5E)/g, unescape); 47 | } 48 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/services/CommonService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal.services 18 | 19 | import korolev.effect.{Stream, Effect} 20 | import korolev.server.HttpResponse 21 | import korolev.server.internal.HttpResponse 22 | import korolev.web.Response 23 | 24 | private[korolev] final class CommonService[F[_]: Effect] { 25 | 26 | val notFoundResponseF: F[HttpResponse[F]] = 27 | HttpResponse(Response.Status.NotFound, "Not found", Nil) 28 | 29 | def badRequest(message: String): F[HttpResponse[F]] = 30 | HttpResponse(Response.Status.BadRequest, s"Bad request. $message", Nil) 31 | 32 | val simpleOkResponse: HttpResponse[F] = Response( 33 | status = Response.Status.Ok, 34 | body = Stream.empty, 35 | headers = Nil, 36 | contentLength = Some(0L) 37 | ) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /modules/bytes/src/main/scala/korolev/data/syntax.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | import java.nio.ByteBuffer 4 | 5 | object syntax { 6 | 7 | implicit final class BytesLikeOps[T](bytes: T)(implicit instance: BytesLike[T]) { 8 | def apply(i: Int): Int = apply(i.toLong) 9 | def apply(i: Long): Int = instance.get(bytes, i) 10 | def length: Long = instance.length(bytes) 11 | def isEmpty: Boolean = length == 0 12 | def concat(right: T): T = BytesLike[T].concat(bytes, right) 13 | def ++(right: T): T = BytesLike[T].concat(bytes, right) 14 | def slice(from: Long, until: Long): T = BytesLike[T].slice(bytes, from, until) 15 | def slice(from: Long): T = BytesLike[T].slice(bytes, from, length) 16 | def mapI(f: (Byte, Long) => Byte): T = BytesLike[T].mapWithIndex(bytes, f) 17 | def indexOf(elem: Byte): Long = BytesLike[T].indexOf(bytes, elem) 18 | def indexOf(elem: Byte, from: Long): Long = BytesLike[T].indexOf(bytes, elem, from) 19 | def lastIndexOf(elem: Byte): Long = BytesLike[T].lastIndexOf(bytes, elem) 20 | def indexOfSlice(elem: T): Long = BytesLike[T].indexOfSlice(bytes, elem) 21 | def lastIndexOfSlice(elem: T): Long = BytesLike[T].lastIndexOfSlice(bytes, elem) 22 | def asUtf8String: String = instance.asUtf8String(bytes) 23 | def asAsciiString: String = instance.asAsciiString(bytes) 24 | def asArray: Array[Byte] = instance.asArray(bytes) 25 | def asBuffer: ByteBuffer = instance.asBuffer(bytes) 26 | def as[T2: BytesLike]: T2 = instance.as[T2](bytes) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/monix/src/main/scala/MonixExample.scala: -------------------------------------------------------------------------------- 1 | import korolev.Context 2 | import korolev.akka._ 3 | import korolev.server.{KorolevServiceConfig, StateLoader} 4 | import korolev.state.javaSerialization._ 5 | import korolev.monix._ 6 | 7 | import monix.eval.Task 8 | import monix.execution.Scheduler.Implicits.global 9 | 10 | object MonixExample extends SimpleAkkaHttpKorolevApp { 11 | 12 | val ctx = Context[Task, Option[String], Any] 13 | 14 | import ctx._ 15 | import levsha.dsl._ 16 | import html._ 17 | 18 | private val aInput = elementId() 19 | private val bInput = elementId() 20 | 21 | def service: AkkaHttpService = akkaHttpService { 22 | KorolevServiceConfig[Task, Option[String], Any]( 23 | stateLoader = StateLoader.default(None), 24 | document = maybeResult => optimize { 25 | Html( 26 | body( 27 | form( 28 | input(aInput, `type` := "number", event("input")(onChange)), 29 | span("+"), 30 | input(bInput, `type` := "number", event("input")(onChange)), 31 | span("="), 32 | maybeResult.map(result => span(result)), 33 | ) 34 | ) 35 | ) 36 | } 37 | ) 38 | } 39 | 40 | private def onChange(access: Access) = 41 | for { 42 | a <- access.valueOf(aInput) 43 | b <- access.valueOf(bInput) 44 | _ <- 45 | if (a.trim.isEmpty || b.trim.isEmpty) Task.unit 46 | else access.transition(_ => Some((a.toInt + b.toInt).toString)) 47 | } yield () 48 | } -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/AkkaHttpServerConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka 18 | 19 | import scala.concurrent.duration._ 20 | 21 | case class AkkaHttpServerConfig(maxRequestBodySize: Int = AkkaHttpServerConfig.DefaultMaxRequestBodySize, 22 | outputBufferSize: Int = AkkaHttpServerConfig.DefaultOutputBufferSize, 23 | wsStreamedCompletionTimeout: FiniteDuration = AkkaHttpServerConfig.DefaultWsStreamedCompletionTimeout, 24 | wsStreamedParallelism: Int = AkkaHttpServerConfig.DefaultWsStreamedParallelism) 25 | 26 | object AkkaHttpServerConfig { 27 | val DefaultMaxRequestBodySize: Int = 8 * 1024 * 1024 28 | val DefaultOutputBufferSize: Int = 1000 29 | val DefaultWsStreamedCompletionTimeout: FiniteDuration = 30.seconds 30 | val DefaultWsStreamedParallelism: Int = 2 31 | } 32 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/util/HtmlUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.util 18 | 19 | object HtmlUtil { 20 | 21 | def camelCaseToSnakeCase(value: String, startIndex: Int): String = { 22 | val sb = new StringBuilder() 23 | camelCaseToSnakeCase(sb, value, startIndex) 24 | sb.mkString 25 | } 26 | 27 | def camelCaseToSnakeCase(value: String, prefix: Char, startIndex: Int): String = { 28 | val sb = new StringBuilder() 29 | sb.append(prefix) 30 | camelCaseToSnakeCase(sb, value, startIndex) 31 | sb.mkString 32 | } 33 | 34 | def camelCaseToSnakeCase(sb: StringBuilder, value: String, startIndex: Int): Unit = { 35 | var i = startIndex 36 | while (i < value.length) { 37 | val char = value(i) 38 | if (Character.isUpperCase(char)) { 39 | sb.append('-') 40 | sb.append(Character.toLowerCase(char)) 41 | } else { 42 | sb.append(char) 43 | } 44 | i += 1 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/Html5RenderContext.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal 18 | 19 | import korolev.Context 20 | import korolev.Context._ 21 | import korolev.effect.Effect 22 | import levsha.RenderContext 23 | import levsha.impl.TextPrettyPrintingConfig 24 | 25 | private[korolev] final class Html5RenderContext[F[_]: Effect, S, M] 26 | extends levsha.impl.Html5RenderContext[Binding[F, S, M]](TextPrettyPrintingConfig.noPrettyPrinting) { 27 | 28 | override def addMisc(misc: Binding[F, S, M]): Unit = misc match { 29 | case ComponentEntry(component, parameters, _) => 30 | val rc = this.asInstanceOf[RenderContext[Context.Binding[F, Any, Any]]] 31 | // Static pages always made from scratch 32 | component.initialState match { 33 | case Right(state) => component.render(parameters, state).apply(rc) 34 | case Left(_) => component.renderNoState(parameters).apply(rc) 35 | } 36 | case _ => () 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /examples/evalJs/src/main/scala/EvalJsExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.server._ 3 | import korolev.akka._ 4 | import korolev.state.javaSerialization._ 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | import scala.concurrent.Future 7 | 8 | object EvalJsExample extends SimpleAkkaHttpKorolevApp { 9 | 10 | val globalContext = Context[Future, String, Any] 11 | 12 | import globalContext._ 13 | import levsha.dsl._ 14 | import html._ 15 | 16 | private def onClick(access: Access) = 17 | for { 18 | result <- access.evalJs("window.confirm('Do you have cow superpower?')") 19 | _ <- access.transition(s => result.toString) 20 | } yield () 21 | 22 | val service = akkaHttpService { 23 | KorolevServiceConfig [Future, String, Any] ( 24 | stateLoader = StateLoader.default("nothing"), 25 | document = { s => 26 | optimize { 27 | Html( 28 | head( 29 | script( 30 | """var x = 0; 31 | |setInterval(() => { 32 | | x++; 33 | | Korolev.invokeCallback('myCallback', x.toString()); 34 | |}, 1000); 35 | |""".stripMargin 36 | ) 37 | ), 38 | body( 39 | button("Click me", event("click")(onClick)), 40 | div(s) 41 | ) 42 | ) 43 | } 44 | }, 45 | extensions = List( 46 | Extension { access => 47 | for (_ <- access.registerCallback("myCallback")(arg => Future(println(arg)))) 48 | yield Extension.Handlers() 49 | } 50 | ) 51 | ) 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /examples/delay/src/main/scala/DelayExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.akka._ 3 | import korolev.server._ 4 | import korolev.state.javaSerialization._ 5 | 6 | import scala.concurrent.Future 7 | import scala.concurrent.duration._ 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | 10 | object DelayExample extends SimpleAkkaHttpKorolevApp { 11 | 12 | val globalContext = Context[Future, Option[Int], Any] 13 | 14 | import globalContext._ 15 | import levsha.dsl._ 16 | import html._ 17 | 18 | val service = akkaHttpService { 19 | KorolevServiceConfig[Future, Option[Int], Any]( 20 | stateLoader = StateLoader.default(Option.empty[Int]), 21 | document = { 22 | case Some(n) => optimize { 23 | Html( 24 | body( 25 | delay(3.seconds) { access => 26 | access.transition { 27 | case _ => None 28 | } 29 | }, 30 | button( 31 | "Push the button " + n, 32 | event("click") { access => 33 | access.transition { 34 | case s => s.map(_ + 1) 35 | } 36 | } 37 | ), 38 | "Wait 3 seconds!" 39 | ) 40 | ) 41 | } 42 | case None => optimize { 43 | Html( 44 | body( 45 | button( 46 | event("click") { access => 47 | access.transition { _ => Some(1) } 48 | }, 49 | "Push the button" 50 | ) 51 | ) 52 | ) 53 | } 54 | } 55 | ) 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/data/FromServer.scala: -------------------------------------------------------------------------------- 1 | package korolev.data 2 | 3 | import akka.actor.typed.ActorRef 4 | import korolev.internal.Frontend 5 | import ujson._ 6 | 7 | sealed trait FromServer 8 | 9 | object FromServer { 10 | 11 | case class Procedure(procedure: Frontend.Procedure, args: List[Any]) extends FromServer 12 | 13 | object Procedure { 14 | 15 | def apply(code: Int, args: List[Any]): Procedure = { 16 | new Procedure(Frontend.Procedure(code).get, args) 17 | } 18 | 19 | def fromJson(ast: Value): Either[String, Procedure] = 20 | try { 21 | ast match { 22 | case arr: Arr => 23 | val Num(procedureId) :: argsAsts = arr.value.toList 24 | val code = procedureId.toInt 25 | 26 | Frontend.Procedure(code) match { 27 | case Some(procedure) => 28 | val args = argsAsts.collect { 29 | case Str(s) => s 30 | case Num(n) if n.toString.contains(".") => n.toDouble 31 | case Num(n) => n.toInt 32 | case False => false 33 | case True => true 34 | case Null => null 35 | } 36 | Right(FromServer.Procedure(procedure, args)) 37 | case None => Left(s"unknown procedure #$code") 38 | } 39 | case other => 40 | Left(s"Unexpected JSON #$other") 41 | } 42 | } catch { 43 | case e: MatchError => 44 | Left(s"can't parse ast $e") 45 | } 46 | } 47 | 48 | case class ErrorOccurred(error: Error) extends FromServer 49 | 50 | case class Connected(ref: ActorRef[ToServer]) extends FromServer 51 | 52 | case object Closed extends FromServer 53 | 54 | } 55 | -------------------------------------------------------------------------------- /interop/slf4j/src/main/scala/korolev/slf4j/Slf4jReporter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.slf4j 18 | 19 | import korolev.effect.Reporter 20 | import org.slf4j.LoggerFactory 21 | 22 | object Slf4jReporter extends Reporter { 23 | 24 | // All messages will be emited from one source. 25 | // It's unnecessary to show Korolev internals to enduser. 26 | private val logger = LoggerFactory.getLogger("Korolev") 27 | 28 | def error(message: String, cause: Throwable): Unit = logger.error(message, cause) 29 | def error(message: String): Unit = logger.error(message) 30 | def warning(message: String, cause: Throwable): Unit = logger.warn(message, cause) 31 | def warning(message: String): Unit = logger.warn(message) 32 | def info(message: String): Unit = logger.info(message) 33 | 34 | def debug(message: String): Unit = logger.debug(message) 35 | def debug(message: String, arg1: Any): Unit = logger.debug(message, arg1) 36 | def debug(message: String, arg1: Any, arg2: Any): Unit = logger.debug(message, arg1, arg2) 37 | def debug(message: String, arg1: Any, arg2: Any, arg3: Any): Unit = logger.debug(message.format(arg1, arg2, arg3)) 38 | } 39 | -------------------------------------------------------------------------------- /modules/web/src/main/scala/korolev/web/FormData.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.web 18 | 19 | import java.nio.ByteBuffer 20 | import java.nio.charset.StandardCharsets 21 | 22 | import korolev.web.FormData.Entry 23 | 24 | final case class FormData(content: Seq[Entry]) { 25 | 26 | def text(name: String): String = { 27 | val array = bytes(name).array() 28 | new String(array, StandardCharsets.UTF_8) 29 | } 30 | 31 | def bytes(name: String): ByteBuffer = bytesOpt(name).get 32 | 33 | def bytesOpt(name: String): Option[ByteBuffer] = { 34 | apply(name).map(_.value) 35 | } 36 | 37 | def contentType(name: String): Option[String] = { 38 | apply(name) flatMap { entry => 39 | entry.headers collectFirst { 40 | case (k, v) if k.toLowerCase == "content-type" => v 41 | } 42 | } 43 | } 44 | 45 | def apply(name: String): Option[Entry] = 46 | content.find(_.name == name) 47 | } 48 | 49 | object FormData { 50 | 51 | case class Entry(name: String, value: ByteBuffer, headers: Seq[(String, String)]) { 52 | lazy val asString: String = new String(value.array(), StandardCharsets.UTF_8) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/util/LoggingReporter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka.util 18 | 19 | import akka.actor.ActorSystem 20 | import akka.event.{LogSource, Logging} 21 | import korolev.effect.Reporter 22 | 23 | final class LoggingReporter(actorSystem: ActorSystem) extends Reporter { 24 | 25 | private implicit val logSource: LogSource[LoggingReporter] = new LogSource[LoggingReporter] { 26 | def genString(t: LoggingReporter): String = "korolev" 27 | } 28 | 29 | private val log = Logging(actorSystem, this) 30 | 31 | def error(message: String, cause: Throwable): Unit = log.error(cause, message) 32 | def error(message: String): Unit = log.error(message) 33 | def warning(message: String, cause: Throwable): Unit = log.warning(s"$message: {}", cause) 34 | def warning(message: String): Unit = log.warning(message) 35 | def info(message: String): Unit = log.info(message) 36 | def debug(message: String): Unit = log.debug(message) 37 | def debug(message: String, arg1: Any): Unit = log.debug(message, arg1) 38 | def debug(message: String, arg1: Any, arg2: Any): Unit = log.debug(message, arg1, arg2) 39 | def debug(message: String, arg1: Any, arg2: Any, arg3: Any): Unit = log.debug(message, arg1, arg2, arg3) 40 | } 41 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/util/Countdown.scala: -------------------------------------------------------------------------------- 1 | package korolev.akka.util 2 | 3 | import korolev.effect.Effect 4 | 5 | import java.util.concurrent.atomic.AtomicReference 6 | import scala.annotation.tailrec 7 | 8 | final class Countdown[F[_]: Effect] { 9 | 10 | private final val state = new AtomicReference(Countdown.State(None, 0)) 11 | 12 | def value(): F[Long] = Effect[F].delay(unsafeValue) 13 | 14 | def add(n: Int): F[Unit] = Effect[F].delay(unsafeAdd(n)) 15 | 16 | def decOrLock(): F[Unit] = Effect[F].promise[Unit] { cb => 17 | @tailrec 18 | def aux(): Unit = { 19 | val ref = state.get() 20 | if (ref.n == 0) { 21 | val newValue = ref.copy(pending = Some(cb)) 22 | if (!state.compareAndSet(ref, newValue)) { 23 | aux() 24 | } 25 | } else { 26 | val newValue = ref.copy(n = ref.n - 1) 27 | if (state.compareAndSet(ref, newValue)) { 28 | cb(Countdown.unitToken) 29 | } else { 30 | aux() 31 | } 32 | } 33 | } 34 | aux() 35 | } 36 | 37 | def unsafeAdd(x: Long): Unit = { 38 | // x should be positive 39 | if (x > 0) { 40 | @tailrec 41 | def aux(): Unit = { 42 | val ref = state.get() 43 | ref.pending match { 44 | case Some(cb) => 45 | if (state.compareAndSet(ref, Countdown.State(pending = None, n = ref.n + x - 1))) { 46 | cb(Countdown.unitToken) 47 | } else { 48 | aux() 49 | } 50 | case None => 51 | if (!state.compareAndSet(ref, ref.copy(n = ref.n + x))) { 52 | aux() 53 | } 54 | } 55 | } 56 | aux() 57 | } 58 | } 59 | 60 | def unsafeValue: Long = state.get().n 61 | } 62 | 63 | object Countdown { 64 | val unitToken = Right(()) 65 | case class State(pending: Option[Effect.Promise[Unit]], n: Long) 66 | } 67 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/internal/EventRegistry.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.internal 18 | 19 | import korolev.effect.{Effect, Reporter} 20 | import korolev.effect.syntax._ 21 | 22 | import java.util.concurrent.atomic.AtomicReference 23 | import scala.annotation.tailrec 24 | 25 | /** 26 | * Save information about what type of events are already 27 | * listening on the client 28 | */ 29 | final class EventRegistry[F[_]: Effect](frontend: Frontend[F])(implicit reporter: Reporter) { 30 | 31 | private val knownEventTypes = new AtomicReference(Set("submit")) 32 | 33 | /** 34 | * Notifies client side that he should listen 35 | * all events of the type. If event already listening 36 | * on the client side, client will be not notified again. 37 | */ 38 | def registerEventType(`type`: String): Unit = { 39 | @tailrec 40 | def aux(): Unit = { 41 | val ref = knownEventTypes.get 42 | if (!ref.contains(`type`)) { 43 | val newValue = ref + `type` 44 | if (knownEventTypes.compareAndSet(ref, newValue)) { 45 | frontend 46 | .listenEvent(`type`, preventDefault = false) 47 | .runAsyncForget 48 | } else { 49 | aux() 50 | } 51 | } 52 | } 53 | aux() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/javaSerialization.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.state 18 | 19 | import java.io._ 20 | 21 | object javaSerialization { 22 | 23 | implicit def serializer[T]: StateSerializer[T] = new StateSerializer[T] { 24 | def serialize(value: T): Array[Byte] = { 25 | val byteStream = new ByteArrayOutputStream() 26 | val objectStream = new ObjectOutputStream(byteStream) 27 | try { 28 | objectStream.writeObject(value) 29 | byteStream.toByteArray 30 | } 31 | finally { 32 | objectStream.close() 33 | byteStream.close() 34 | } 35 | } 36 | } 37 | 38 | implicit def deserializer[T]: StateDeserializer[T] = new StateDeserializer[T] { 39 | def deserialize(data: Array[Byte]): Option[T] = { 40 | val byteStream = new ByteArrayInputStream(data) 41 | val objectStream = new ObjectInputStream(byteStream) 42 | 43 | try { 44 | val value = objectStream.readObject() 45 | val typed = value.asInstanceOf[T] 46 | Some(typed) 47 | } 48 | catch { 49 | case _:InvalidClassException => 50 | // That means state type was changed 51 | None 52 | } finally { 53 | objectStream.close() 54 | byteStream.close() 55 | } 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /interop/zio2-streams/src/main/scala/korolev/zio/streams/package.scala: -------------------------------------------------------------------------------- 1 | package korolev.zio 2 | 3 | import korolev.effect.{Effect as KorolevEffect, Stream as KorolevStream} 4 | import zio.stream.ZStream 5 | import zio._ 6 | 7 | package object streams { 8 | 9 | implicit class KorolevStreamOps[R, O](stream: KorolevStream[RIO[R, *], O]) { 10 | 11 | def toZStream: ZStream[R, Throwable, O] = { 12 | ZStream.unfoldZIO(()) { _ => 13 | stream 14 | .pull() 15 | .map(mv => mv.map(v => (v, ()))) 16 | } 17 | } 18 | } 19 | 20 | private type Finalizer = Exit[Any, Any] => UIO[Any] 21 | 22 | implicit class ZStreamOps[R, O](stream: ZStream[R, Throwable, O]) { 23 | 24 | def toKorolev(implicit eff: KorolevEffect[RIO[R, *]]): RIO[R, ZKorolevStream[R, O]] = { 25 | Ref.make(List.empty[Finalizer]).flatMap { finalizersRef => 26 | val scope = new Scope { 27 | def addFinalizerExit(finalizer: Finalizer)(implicit trace: Trace): UIO[Unit] = 28 | finalizersRef.update(finalizer :: _) 29 | def forkWith(executionStrategy: => ExecutionStrategy)(implicit trace: Trace): UIO[Scope.Closeable] = 30 | ZIO.dieMessage("Can't fork ZKorolevStream") 31 | } 32 | (for { 33 | pull <- stream.toPull 34 | zStream = ZKorolevStream[R, O](pull, finalizersRef) 35 | } yield zStream).provideSomeLayer[R](ZLayer.succeed(scope)) 36 | } 37 | } 38 | } 39 | 40 | private[streams] case class ZKorolevStream[R, O] 41 | (zPull: ZIO[R, Option[Throwable], Chunk[O]], 42 | finalizersRef: Ref[List[Finalizer]] 43 | )(implicit eff: KorolevEffect[RIO[R, *]]) extends KorolevStream[RIO[R, *], Seq[O]] { 44 | 45 | def pull(): RIO[R, Option[Seq[O]]] = 46 | zPull.option 47 | 48 | def cancel(): RIO[R, Unit] = 49 | for { 50 | finalizers <- finalizersRef.get 51 | _ <- ZIO.collectAll(finalizers.map(_(Exit.unit))).unit 52 | } yield () 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /modules/korolev/src/main/es6/launcher.js: -------------------------------------------------------------------------------- 1 | import { Connection } from './connection.js'; 2 | import { Bridge, setProtocolDebugEnabled } from './bridge.js'; 3 | import { ConnectionLostWidget } from './utils.js'; 4 | 5 | function showKorolevIsNotReadyMessage() { 6 | console.log("Korolev is not ready"); 7 | } 8 | 9 | window['Korolev'] = { 10 | 'setProtocolDebugEnabled': setProtocolDebugEnabled, 11 | 'invokeCallback': () => showKorolevIsNotReadyMessage(), 12 | 'swapElementInRegistry': () => showKorolevIsNotReadyMessage(), 13 | 'ready': false 14 | }; 15 | 16 | window.document.addEventListener("DOMContentLoaded", () => { 17 | 18 | let reconnect = true 19 | let config = window['kfg']; 20 | let clw = new ConnectionLostWidget(config['clw']); 21 | let connection = new Connection( 22 | config['sid'], 23 | config['r'], 24 | window.location 25 | ); 26 | 27 | window['Korolev']['disconnect'] = (reconnect = false) => { 28 | connection.disconnect(reconnect); 29 | } 30 | 31 | window['Korolev']['connect'] = () => connection.connect(); 32 | 33 | connection.dispatcher.addEventListener('open', () => { 34 | 35 | clw.hide(); 36 | let bridge = new Bridge(config, connection); 37 | let globalObject = window['Korolev'] 38 | 39 | globalObject['swapElementInRegistry'] = (a, b) => bridge._korolev.swapElementInRegistry(a, b); 40 | globalObject['element'] = (id) => bridge._korolev.element(id); 41 | globalObject['invokeCallback'] = (name, arg) => bridge._korolev.invokeCustomCallback(name, arg); 42 | globalObject['ready'] = true; 43 | 44 | window.dispatchEvent(new Event('KorolevReady')); 45 | 46 | let closeHandler = (event) => { 47 | bridge.destroy(); 48 | clw.show(); 49 | connection 50 | .dispatcher 51 | .removeEventListener('close', closeHandler); 52 | }; 53 | connection 54 | .dispatcher 55 | .addEventListener('close', closeHandler); 56 | }); 57 | 58 | connection.connect(); 59 | }); 60 | -------------------------------------------------------------------------------- /modules/effect/src/test/scala/korolev/effect/AsyncTableSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect 2 | 3 | import korolev.effect.AsyncTable.{AlreadyContainsKeyException, RemovedBeforePutException} 4 | import org.scalatest.freespec.AsyncFreeSpec 5 | 6 | import scala.concurrent.{ExecutionContext, Future} 7 | import syntax._ 8 | 9 | class AsyncTableSpec extends AsyncFreeSpec { 10 | 11 | implicit override def executionContext: ExecutionContext = 12 | scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | "put before get" in { 15 | for { 16 | table <- AsyncTable.empty[Future, Int, String] 17 | _ <- table.put(42, "Hello world") 18 | value <- table.get(42) 19 | } yield { 20 | assert(value == "Hello world") 21 | } 22 | } 23 | 24 | "put after get" in { 25 | for { 26 | table <- AsyncTable.empty[Future, Int, String] 27 | fiber <- table.get(42).start 28 | _ <- table.put(42, "Hello world") 29 | value <- fiber.join() 30 | } yield { 31 | assert(value == "Hello world") 32 | } 33 | } 34 | 35 | "remove before get" in recoverToSucceededIf[RemovedBeforePutException] { 36 | for { 37 | table <- AsyncTable.empty[Future, Int, String] 38 | fiber <- table.get(42).start 39 | _ <- table.remove(42) 40 | _ <- fiber.join() 41 | } yield () 42 | } 43 | 44 | "get twice" in { 45 | for { 46 | table <- AsyncTable.empty[Future, Int, String] 47 | fiber1 <- table.get(42).start 48 | fiber2 <- table.get(42).start 49 | _ <- table.put(42, "Hello world") 50 | value1 <- fiber1.join() 51 | value2 <- fiber2.join() 52 | } yield { 53 | assert(value1 == value2 && value1 == "Hello world") 54 | } 55 | } 56 | 57 | "put twice" in recoverToSucceededIf[AlreadyContainsKeyException] { 58 | for { 59 | table <- AsyncTable.empty[Future, Int, String] 60 | _ <- table.put(42, "Hello world") 61 | _ <- table.put(42, "Hello world") 62 | } yield () 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /modules/web-dsl/src/main/scala/korolev/web/dsl/dsl.scala: -------------------------------------------------------------------------------- 1 | package korolev.web 2 | 3 | import korolev.web.Response.Status 4 | import korolev.effect.Effect 5 | import korolev.effect.syntax._ 6 | 7 | package object dsl { 8 | 9 | def response[F[_]: Effect, A, B](source: A, status: Response.Status = Status.Ok, headers: Map[String, String] = Map.empty)( 10 | implicit bf: BodyFactory[F, A, B]): F[Response[B]] = bf.mkBody(source).map { body => 11 | val allHeaders = body.headers ++ headers 12 | Response(status, body.content, allHeaders.toSeq, body.contentLength) 13 | } 14 | 15 | def request[F[_]: Effect, B](method: Request.Method, pq: PathAndQuery)( 16 | implicit bf: EmptyBodyFactory[B]): F[Request[B]] = Effect[F].pure { 17 | val body = bf.emptyBody 18 | Request(method, pq, body.headers.toSeq, body.contentLength, body.content) 19 | } 20 | 21 | def request[F[_]: Effect, A, B](method: Request.Method, pq: PathAndQuery, source: A)( 22 | implicit bf: BodyFactory[F, A, B]): F[Request[B]] = bf.mkBody(source).map { body => 23 | Request(method, pq, body.headers.toSeq, body.contentLength, body.content) 24 | } 25 | 26 | def request[F[_]: Effect, A, B](method: Request.Method, pq: PathAndQuery, source: A, headers: Map[String, String])( 27 | implicit bf: BodyFactory[F, A, B]): F[Request[B]] = bf.mkBody(source).map { body => 28 | val allHeaders = body.headers ++ headers 29 | Request(method, pq, allHeaders.toSeq, body.contentLength, body.content) 30 | } 31 | 32 | // Routing 33 | 34 | final val / = PathAndQuery./ 35 | type / = PathAndQuery./ 36 | 37 | final val :& = PathAndQuery.:& 38 | type :& = PathAndQuery.:& 39 | 40 | final val :? = PathAndQuery.:? 41 | type :? = PathAndQuery.:? 42 | 43 | final val :?* = PathAndQuery.:?* 44 | final val *& = PathAndQuery.*& 45 | final val Root = PathAndQuery.Root 46 | 47 | object -> { 48 | def unapply[Body](request: Request[Body]): Option[(Request.Method, PathAndQuery)] = 49 | Some(request.method, request.pq) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/StateLoader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server 18 | 19 | import korolev.effect.Effect 20 | import korolev.state.DeviceId 21 | import korolev.web.Request.Head 22 | 23 | object StateLoader { 24 | 25 | /** 26 | * State is same for all sessions. 27 | * 28 | * @param initialState State factory 29 | * @tparam S Type of state 30 | */ 31 | def default[F[_] : Effect, S](initialState: S): StateLoader[F, S] = { 32 | val value = Effect[F].pure(initialState) 33 | (_, _) => value // always return same object 34 | } 35 | 36 | /** 37 | * State depends on deviceId. Useful when you want to 38 | * restore user authorization. 39 | * 40 | * {{{ 41 | * case class MyState(deviceId: DeviceId, ...) 42 | * 43 | * StateLoader.forDeviceId { deviceId => 44 | * MyStorage.getStateByDeviceId(deviceId) map { 45 | * case Some(state) => state 46 | * case None => MyState(deviceId, ...) 47 | * } 48 | * } 49 | * }}} 50 | */ 51 | def forDeviceId[F[_], S](initialState: DeviceId => F[S]): StateLoader[F, S] = 52 | (deviceId, _) => initialState(deviceId) 53 | 54 | /** 55 | * State depends on deviceId and HTTP-request. Second one 56 | * could be None if case when user reconnected to 57 | * restarted application and state wasn't restored. 58 | */ 59 | def apply[F[_], S](f: (DeviceId, Head) => F[S]): StateLoader[F, S] = f 60 | } 61 | -------------------------------------------------------------------------------- /modules/http/src/main/scala/korolev/http/HttpServer.scala: -------------------------------------------------------------------------------- 1 | package korolev.http 2 | 3 | import java.net.SocketAddress 4 | import java.nio.channels.AsynchronousChannelGroup 5 | 6 | import korolev.data.BytesLike 7 | import korolev.data.syntax._ 8 | import korolev.effect.io.ServerSocket 9 | import korolev.effect.syntax._ 10 | import korolev.effect.{Decoder, Effect, Stream} 11 | import korolev.http.protocol.Http11 12 | import korolev.web.{Request, Response} 13 | 14 | import scala.concurrent.ExecutionContext 15 | import scala.util.control.NonFatal 16 | 17 | object HttpServer { 18 | 19 | /** 20 | * @see [[ServerSocket.bind]] 21 | */ 22 | def apply[F[_]: Effect, B: BytesLike](address: SocketAddress, 23 | backlog: Int = 0, 24 | bufferSize: Int = 8096, 25 | group: AsynchronousChannelGroup = null, 26 | gracefulShutdown: Boolean = false) 27 | (f: Request[Stream[F, B]] => F[Response[Stream[F, B]]]) 28 | (implicit ec: ExecutionContext): F[ServerSocket.ServerSocketHandler[F]] = { 29 | 30 | val InternalServerErrorMessage = BytesLike[B].ascii("Internal server error") 31 | val http11 = new Http11[B] 32 | 33 | ServerSocket.accept[F, B](address, backlog, bufferSize, group, gracefulShutdown) { client => 34 | http11 35 | .decodeRequest(Decoder(client.stream)) 36 | .foreach { request => 37 | for { 38 | response <- f(request).recoverF { 39 | case NonFatal(error) => 40 | ec.reportFailure(error) 41 | Stream(InternalServerErrorMessage).mat() map { body => 42 | Response(Response.Status.InternalServerError, body, Nil, Some(InternalServerErrorMessage.length)) 43 | } 44 | } 45 | byteStream <- http11.renderResponse(response) 46 | _ <- byteStream.foreach(client.write) 47 | } yield () 48 | } 49 | } 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /modules/korolev/src/main/protocol.md: -------------------------------------------------------------------------------- 1 | ## Procedure codes 2 | 3 | From server to the client 4 | 5 | ```json 6 | [ 7 | 3, // procedure code of ExtractProperty 8 | "1_1", // id 9 | "propertyName", // propertyName 10 | 0 // descriptor 11 | ] 12 | ``` 13 | 14 | * 0 - SetRenderNum(n) 15 | * 1 - Reload() 16 | * 2 - ListenEvent(type, preventDefault) 17 | * 3 - ExtractProperty(descriptor, id, propertyName) 18 | * 4 - ModifyDOM(commands) 19 | * 5 - Focus(id) { 20 | * 6 - ChangePageUrl(path) 21 | * 7 - UploadForm(id, descriptor) 22 | * 8 - ReloadCss() 23 | * 9 - Keep-alive message from server (noop) 24 | * 10 - EvalJs(descriptor, code) 25 | * 11 - ExtractEventData(descriptor, renderNum) 26 | * 12 - ListFiles(id, descriptor) 27 | * 13 - UploadFile(id, descriptor, fileName) 28 | * 14 - ResetForm(id) 29 | * 15 - DownloadFile(name, descriptor) 30 | 31 | ### Modify dom commands 32 | 33 | ```json 34 | [ 35 | 4, // procedure code of ModifyDOM 36 | 0, // procedure code of Create 37 | "1_1", // id 38 | "1_1_1", // childId 39 | 0, // xmlNs 40 | "div", // tag 41 | 5, // procedure code of SetStyle 42 | "width", // name 43 | "100px" // value 44 | ] 45 | ``` 46 | 47 | * 0 - Create(id, childId, xmlNs, tag) 48 | * 1 - CreateText(id, childId, text) 49 | * 2 - Remove(id, childId) 50 | * 3 - SetAttr(id, xmlNs, name, value, isProperty) 51 | * 4 - RemoveAttr(id, xmlNs, name, isProperty) 52 | * 5 - SetStyle(id, name, value) 53 | * 6 - RemoveStyle(id, name) 54 | 55 | ## Callbacks 56 | 57 | From the client to the server 58 | 59 | ```json 60 | [ 61 | 0, // DOM event 62 | "0:1_1:click" // data 63 | ] 64 | ``` 65 | 66 | * 0 - DOM Event. Data: `$renderNum:$elementId:$eventType` 67 | * 1 - Custom callback. Data: `$name:$arg` 68 | * 2 - ExtractProperty response. Data:`$descriptor:$type:$value` 69 | * 3 - History Event. Data: URL 70 | * 4 - EvalJs response. Data: `$descriptor:$dataJson` 71 | * 5 - ExtractEventData response. Data: `$descriptor:$dataJson` 72 | * 6 - Heartbeat. Data: `$descriptor` 73 | -------------------------------------------------------------------------------- /modules/web/src/main/scala/korolev/web/Response.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.web 18 | 19 | import korolev.web.Response.Status 20 | 21 | import scala.annotation.switch 22 | 23 | final case class Response[Body]( 24 | status: Status, 25 | body: Body, 26 | headers: Seq[(String, String)], 27 | contentLength: Option[Long] 28 | ) { 29 | def header(name: String): Option[String] = 30 | headers.collectFirst { 31 | case (k, v) if k.equalsIgnoreCase(name) => v 32 | } 33 | } 34 | 35 | object Response { 36 | 37 | final case class Status(code: Int, phrase: String) { 38 | val codeAsString: String = code.toString 39 | } 40 | 41 | object Status { 42 | val Ok: Status = new Status(200, "OK") 43 | val Created = new Status(201, "Created") 44 | val ResetContent = new Status(205, "Reset Content") 45 | val NotFound: Status = new Status(404, "Not Found") 46 | val BadRequest: Status = new Status(400, "Bad Request") 47 | val Gone: Status = new Status(410, "Gone") 48 | val SwitchingProtocols: Status = new Status(101, "Switching Protocols") 49 | val InternalServerError: Status = new Status(500, "Internal Server Error") 50 | 51 | def apply(code: Int, phrase: String): Status = { 52 | (code: @switch) match { 53 | case 200 => Ok 54 | case 404 => NotFound 55 | case 400 => BadRequest 56 | case 410 => Gone 57 | case 101 => SwitchingProtocols 58 | case _ => new Status(code, phrase) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /interop/fs2-ce2/src/test/scala/Fs2InteropSpec.scala: -------------------------------------------------------------------------------- 1 | import _root_.fs2.{Stream => Fs2Stream} 2 | import _root_.cats.effect.{IO, _} 3 | import korolev.cats._ 4 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 5 | import korolev.fs2._ 6 | import org.scalatest.flatspec.AsyncFlatSpec 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | class Fs2InteropSpec extends AsyncFlatSpec with Matchers { 10 | 11 | private implicit val cs: ContextShift[IO] = 12 | IO.contextShift(executionContext) 13 | 14 | "KorolevStream.toFs2" should "provide fs2.Stream that contain exactly same values as original Korolev stream" in { 15 | val values = List(1, 2, 3, 4, 5) 16 | KorolevStream(values: _*) 17 | .mat[IO]() 18 | .flatMap { korolevStream => 19 | korolevStream 20 | .toFs2 21 | .compile 22 | .toList 23 | } 24 | .unsafeToFuture() 25 | .map { result => 26 | result shouldEqual values 27 | } 28 | } 29 | 30 | it should "provide fs2.Stream which handle values asynchronously" in { 31 | val queue = Queue[IO, Int]() 32 | val io = 33 | for { 34 | fiber <- KorolevEffect[IO] 35 | .start { 36 | queue 37 | .stream 38 | .toFs2 39 | .compile 40 | .toList 41 | } 42 | _ <- queue.offer(1) 43 | _ <- queue.offer(2) 44 | _ <- queue.offer(3) 45 | _ <- queue.offer(4) 46 | _ <- queue.offer(5) 47 | _ <- queue.stop() 48 | result <- fiber.join() 49 | } yield { 50 | result shouldEqual List(1, 2, 3, 4, 5) 51 | } 52 | io.unsafeToFuture() 53 | } 54 | 55 | "Fs2Stream.toKorolevStream" should "provide korolev.effect.Stream that contain exactly same values as original fs2.Stream" in { 56 | val values = Vector(1, 2, 3, 4, 5) 57 | Fs2Stream[IO, Int](values: _*) 58 | .toKorolev() 59 | .flatMap { korolevStream => 60 | korolevStream 61 | .fold(Vector.empty[Int])((acc, value) => acc :+ value) 62 | .map(result => result shouldEqual values) 63 | } 64 | .unsafeToFuture() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /interop/fs2-ce3/src/test/scala/Fs2InteropSpec.scala: -------------------------------------------------------------------------------- 1 | import _root_.fs2.{Stream => Fs2Stream} 2 | import _root_.cats.effect.{IO, _} 3 | import _root_.cats.effect.unsafe.IORuntime 4 | 5 | import korolev.cats._ 6 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 7 | import korolev.fs2._ 8 | import org.scalatest.flatspec.AsyncFlatSpec 9 | import org.scalatest.matchers.should.Matchers 10 | 11 | class Fs2InteropSpec extends AsyncFlatSpec with Matchers { 12 | 13 | private implicit val runtime: IORuntime = _root_.cats.effect.unsafe.IORuntime.global 14 | 15 | "KorolevStream.toFs2" should "provide fs2.Stream that contain exactly same values as original Korolev stream" in { 16 | val values = List(1, 2, 3, 4, 5) 17 | KorolevStream(values: _*) 18 | .mat[IO]() 19 | .flatMap { korolevStream => 20 | korolevStream 21 | .toFs2 22 | .compile 23 | .toList 24 | } 25 | .unsafeToFuture() 26 | .map { result => 27 | result shouldEqual values 28 | } 29 | } 30 | 31 | it should "provide fs2.Stream which handle values asynchronously" in { 32 | val queue = Queue[IO, Int]() 33 | val io = 34 | for { 35 | fiber <- KorolevEffect[IO] 36 | .start { 37 | queue 38 | .stream 39 | .toFs2 40 | .compile 41 | .toList 42 | } 43 | _ <- queue.offer(1) 44 | _ <- queue.offer(2) 45 | _ <- queue.offer(3) 46 | _ <- queue.offer(4) 47 | _ <- queue.offer(5) 48 | _ <- queue.stop() 49 | result <- fiber.join() 50 | } yield { 51 | result shouldEqual List(1, 2, 3, 4, 5) 52 | } 53 | io.unsafeToFuture() 54 | } 55 | 56 | "Fs2Stream.toKorolevStream" should "provide korolev.effect.Stream that contain exactly same values as original fs2.Stream" in { 57 | val values = Vector(1, 2, 3, 4, 5) 58 | Fs2Stream[IO, Int](values: _*) 59 | .toKorolev() 60 | .flatMap { korolevStream => 61 | korolevStream 62 | .fold(Vector.empty[Int])((acc, value) => acc :+ value) 63 | .map(result => result shouldEqual values) 64 | } 65 | .unsafeToFuture() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/state/IdGenerator.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.state 18 | 19 | import java.security.SecureRandom 20 | 21 | import korolev.effect.Effect 22 | 23 | trait IdGenerator[F[_]] { 24 | def generateDeviceId()(implicit F: Effect[F]): F[DeviceId] 25 | def generateSessionId()(implicit F: Effect[F]): F[SessionId] 26 | } 27 | 28 | object IdGenerator { 29 | 30 | val DefaultDeviceIdLength = 16 31 | val DefaultSessionIdLength = 8 32 | 33 | def default[F[_]](deviceIdLength: Int = DefaultDeviceIdLength, 34 | sessionIdLength: Int = DefaultSessionIdLength): IdGenerator[F] = 35 | new DefaultIdGenerator[F](deviceIdLength, sessionIdLength) 36 | 37 | private class DefaultIdGenerator[F[_]](deviceIdLength: Int, 38 | sessionIdLength: Int) extends IdGenerator[F] { 39 | def generateDeviceId()(implicit F: Effect[F]): F[DeviceId] = 40 | Effect[F].delay { 41 | secureRandomString(deviceIdLength) 42 | } 43 | 44 | def generateSessionId()(implicit F: Effect[F]): F[SessionId] = 45 | Effect[F].delay { 46 | secureRandomString(sessionIdLength) 47 | } 48 | 49 | private val Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 50 | private val rnd = new SecureRandom 51 | 52 | private def secureRandomString(len: Int): String = { 53 | val sb = new StringBuilder(len) 54 | var i = 0 55 | while (i < len) { 56 | sb.append(Alphabet.charAt(rnd.nextInt(Alphabet.length))) 57 | i += 1 58 | } 59 | sb.toString 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /modules/effect/src/test/scala/korolev/effect/QueueSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect 2 | 3 | import org.scalatest.Assertion 4 | import org.scalatest.freespec.AsyncFreeSpec 5 | import korolev.effect.syntax._ 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | class QueueSpec extends AsyncFreeSpec { 10 | 11 | implicit override def executionContext: ExecutionContext = 12 | scala.concurrent.ExecutionContext.Implicits.global 13 | 14 | "Concurrent offer/pull invocations" in { 15 | val queue = Queue[Future, Int]() 16 | 17 | for { 18 | s1 <- Stream(0 until 100:_*).mat() 19 | s2 <- Stream(100 until 200:_*).mat() 20 | s3 <- Stream(200 until 300:_*).mat() 21 | ss = Seq(s1, s2, s3) 22 | _ <- Future.sequence(ss.map(s => Effect[Future].fork(s.foreach(queue.enqueue)))) 23 | _ <- queue.stop() 24 | } yield () 25 | 26 | queue 27 | .stream 28 | .fold(0)((x, _) => x + 1) 29 | .map(count => assert(count == 300)) 30 | } 31 | 32 | ".canOffer" - { 33 | 34 | def checkCanOffer(maxSize: Int): Future[Assertion] = { 35 | val queue = Queue[Future, Int](maxSize) 36 | 37 | def offeringProcess = 38 | for { 39 | s1 <- Stream(0 until 1000:_*).mat() 40 | s2 <- Stream(1000 until 2000:_*).mat() 41 | s3 <- Stream(2000 until 3000:_*).mat() 42 | ss = Seq(s1, s2, s3) 43 | fs = ss.map { s => 44 | Effect[Future].fork { 45 | s.foreach { i => 46 | def aux(): Future[Unit] = queue.offer(i) flatMap { 47 | case false => queue.canOffer *> aux() 48 | case true => Future.unit 49 | } 50 | aux() 51 | } 52 | } 53 | } 54 | _ <- Future.sequence(fs) 55 | _ <- queue.stop() 56 | } yield () 57 | 58 | for { 59 | offering <- Effect[Future].start(offeringProcess) 60 | count <- queue 61 | .stream 62 | .fold(0)((x, _) => x + 1) 63 | _ <- offering.join() 64 | } yield { 65 | assert(count == 3000) 66 | } 67 | } 68 | 69 | "maxSize = 1" in checkCanOffer(1) 70 | "maxSize = 5" in checkCanOffer(5) 71 | "maxSize = 10" in checkCanOffer(10) 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/Router.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import korolev.effect.Effect 20 | import korolev.effect.syntax._ 21 | 22 | import korolev.util.Lens 23 | import korolev.web.PathAndQuery 24 | 25 | /** 26 | * URL routing definition 27 | * 28 | * @param fromState From current state to Uri 29 | * @param toState From Uri to state 30 | * 31 | * @tparam F A async control 32 | * @tparam S Type of State 33 | */ 34 | final case class Router[F[_], S]( 35 | fromState: PartialFunction[S, PathAndQuery], 36 | toState: PartialFunction[PathAndQuery, S => F[S]] 37 | ) { 38 | 39 | /** 40 | * Compose two routers to one. 41 | * {{{ 42 | * val articlesRouter = Router(...) 43 | * val authorsRouter = Router(...) 44 | * val router = articlesRouter ++ authorsRouter 45 | * }}} 46 | */ 47 | def ++(that: Router[F, S]): Router[F, S] = 48 | Router(fromState.orElse(that.fromState), toState.orElse(that.toState)) 49 | } 50 | 51 | object Router { 52 | 53 | def empty[F[_], S]: Router[F, S] = Router(PartialFunction.empty, PartialFunction.empty) 54 | 55 | def apply[F[_]: Effect, S, S2](lens: Lens[S, S2])( 56 | fromState: PartialFunction[S2, PathAndQuery] = PartialFunction.empty, 57 | toState: PartialFunction[PathAndQuery, S2 => F[S2]] = PartialFunction.empty): Router[F, S] = 58 | Router( 59 | fromState = lens.read.andThen(fromState), 60 | toState = toState.andThen { f => s => 61 | lens 62 | .get(s) 63 | .fold(Effect[F].pure(s)) { s2 => 64 | f(s2).map(newS2 => lens.update(s, newS2)) 65 | } 66 | } 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /examples/web-component/src/main/scala/WebComponentExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.akka._ 3 | import korolev.server._ 4 | import korolev.state.javaSerialization._ 5 | 6 | import scala.concurrent.Future 7 | import scala.concurrent.ExecutionContext.Implicits.global 8 | 9 | object WebComponentExample extends SimpleAkkaHttpKorolevApp { 10 | 11 | import State.globalContext._ 12 | import levsha.dsl._ 13 | import html._ 14 | 15 | private def setLatLon(lat: Double, lon: Double): (Access => EventResult) = { 16 | (access: Access) => { 17 | access.transition { case s => 18 | s.copy(lat = lat, lon = lon) 19 | } 20 | } 21 | } 22 | 23 | // Define leaflet map 24 | val leafletMap = TagDef("leaflet-map") 25 | val latitude = AttrDef("latitude") 26 | val longitude = AttrDef("longitude") 27 | val zoom = AttrDef("zoom") 28 | 29 | val service = akkaHttpService{ 30 | KorolevServiceConfig [Future, State, Any] ( 31 | stateLoader = StateLoader.default(State()), 32 | document = state => optimize { 33 | Html( 34 | head( 35 | script(src := "https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.24/webcomponents-lite.min.js"), 36 | link(rel := "import", href := "https://leaflet-extras.github.io/leaflet-map/bower_components/leaflet-map/leaflet-map.html") 37 | ), 38 | body( 39 | div( 40 | button("San Francisco", event("click")(setLatLon(37.7576793, -122.5076402))), 41 | button("London", event("click")(setLatLon(51.528308, -0.3817983))), 42 | button("New York", event("click")(setLatLon(40.705311, -74.2581908))), 43 | button("Moscow", event("click")(setLatLon(55.748517, 37.0720941))), 44 | button("Korolev", event("click")(setLatLon(55.9226846, 37.7961706))) 45 | ), 46 | leafletMap ( 47 | width @= "500px", height @= "300px", 48 | latitude := state.lat.toString, 49 | longitude := state.lon.toString, 50 | zoom := "10" 51 | ) 52 | ) 53 | ) 54 | } 55 | ) 56 | } 57 | 58 | } 59 | 60 | case class State(lon: Double = 0, lat: Double = 0) 61 | 62 | object State { 63 | val globalContext = Context[Future, State, Any] 64 | } 65 | 66 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/util/KorolevStreamSubscriber.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka.util 18 | 19 | import korolev.effect.{Effect, Stream} 20 | import org.reactivestreams.{Subscriber, Subscription} 21 | 22 | final class KorolevStreamSubscriber[F[_]: Effect,T] extends Stream[F, T] with Subscriber[T] { 23 | 24 | @volatile private var subscription: Subscription = _ 25 | 26 | @volatile private var pullCallback: Either[Throwable, Option[T]] => Unit = _ 27 | 28 | @volatile private var completeValue: Either[Throwable, None.type] = _ 29 | 30 | def onSubscribe(subscription: Subscription): Unit = { 31 | this.subscription = subscription 32 | if (pullCallback != null) 33 | subscription.request(1) 34 | } 35 | 36 | def onNext(value: T): Unit = { 37 | val cb = pullCallback 38 | pullCallback = null 39 | cb(Right(Some(value))) 40 | } 41 | 42 | def onError(error: Throwable): Unit = { 43 | completeWith(Left(error)) 44 | } 45 | 46 | def onComplete(): Unit = { 47 | completeWith(Right(None)) 48 | } 49 | 50 | private def completeWith(that: Either[Throwable, None.type]): Unit = { 51 | completeValue = that 52 | if (pullCallback != null) { 53 | val cb = pullCallback 54 | pullCallback = null 55 | cb(that) 56 | } 57 | } 58 | 59 | def pull(): F[Option[T]] = Effect[F].promise { cb => 60 | if (completeValue == null) { 61 | pullCallback = cb 62 | if (subscription != null) { 63 | subscription.request(1) 64 | } 65 | } else { 66 | cb(completeValue) 67 | } 68 | } 69 | 70 | def cancel(): F[Unit] = 71 | Effect[F].delay(subscription.cancel()) 72 | } 73 | -------------------------------------------------------------------------------- /modules/korolev/src/test/scala/korolev/FormDataCodecSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import korolev.server.internal.FormDataCodec 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import java.nio.ByteBuffer 8 | import java.nio.charset.StandardCharsets 9 | 10 | class FormDataCodecSpec extends AnyFlatSpec with Matchers { 11 | "decode" should "parse valid multipart/form-data body" in { 12 | val body = """--Asrf456BGe4h 13 | |Content-Disposition: form-data; name="DestAddress" 14 | | 15 | |brutal-vasya@example.com 16 | |--Asrf456BGe4h 17 | |Content-Disposition: form-data; name="MessageTitle" 18 | | 19 | |I'm indignant 20 | |--Asrf456BGe4h 21 | |Content-Disposition: form-data; name="MessageText" 22 | | 23 | |Hello, Vasily! Your hand lion, which you left with me 24 | |last week, tore my whole sofa. Please take it away 25 | |soon! In the attachment, two pictures with consequences. 26 | |--Asrf456BGe4h 27 | |Content-Disposition: form-data; name="AttachedFile1"; filename="horror-photo-1.jpg" 28 | |Content-Type: image/jpeg 29 | | 30 | | 31 | |--Asrf456BGe4h 32 | |Content-Disposition: form-data; name="AttachedFile2"; filename="horror-photo-2.jpg" 33 | |Content-Type: image/jpeg 34 | | 35 | | 36 | |--Asrf456BGe4h-- 37 | """.stripMargin 38 | 39 | val bodyBuffer = ByteBuffer.wrap(body.getBytes(StandardCharsets.US_ASCII)) 40 | val codec = new FormDataCodec(100500) 41 | val formData = codec.decode(bodyBuffer, "Asrf456BGe4h") 42 | 43 | formData.text("DestAddress") should be ("brutal-vasya@example.com") 44 | formData.text("MessageTitle") should be ("I'm indignant") 45 | formData.bytes("AttachedFile2") should be { 46 | ByteBuffer.wrap("".getBytes) 47 | } 48 | } 49 | 50 | "decode" should "parse empty multipart/form-data body" in { 51 | val body = """------WebKitFormBoundaryrBVqcOqR4KNX8jT9--\r\n 52 | """.stripMargin 53 | 54 | val bodyBuffer = ByteBuffer.wrap(body.getBytes(StandardCharsets.US_ASCII)) 55 | val codec = new FormDataCodec(100500) 56 | val formData = codec.decode(bodyBuffer, "----WebKitFormBoundaryVLDwcP1YkcvPtjGM") 57 | 58 | formData.bytesOpt("any") should be (None) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/io/FileIO.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.effect.io 18 | 19 | import korolev.data.BytesLike 20 | 21 | import java.io.{BufferedReader, FileInputStream, FileOutputStream, FileReader} 22 | import java.nio.file.Path 23 | import korolev.effect.syntax._ 24 | import korolev.effect.{Effect, Stream} 25 | 26 | object FileIO { 27 | 28 | def readBytes[F[_]: Effect, B: BytesLike](path: Path): F[Stream[F, B]] = { 29 | val inputStream = new FileInputStream(path.toFile) 30 | JavaIO.fromInputStream(inputStream) 31 | } 32 | 33 | def readLines[F[_]: Effect](path: Path): F[Stream[F, String]] = { 34 | Stream.unfoldResource[F, BufferedReader, Unit, String]( 35 | default = (), 36 | create = Effect[F].delay(new BufferedReader(new FileReader(path.toFile))), 37 | loop = (reader, _) => Effect[F].delay { 38 | ((), Option(reader.readLine())) 39 | } 40 | ) 41 | } 42 | 43 | /** 44 | * {{{ 45 | * chunks.to(File.write(path, append = true)) 46 | * }}} 47 | */ 48 | def write[F[_]: Effect, B: BytesLike](path: Path, append: Boolean = false): Stream[F, B] => F[Unit] = { stream => 49 | val outputStream = new FileOutputStream(path.toFile, append) 50 | def aux(): F[Unit] = { 51 | stream.pull().flatMap { 52 | case Some(chunk) => Effect[F] 53 | .delay(outputStream.write(BytesLike[B].asArray(chunk))) 54 | .after(aux()) 55 | .recover { 56 | case error => 57 | outputStream.close() 58 | throw error 59 | } 60 | case None => 61 | Effect[F].delay(outputStream.close()) 62 | } 63 | } 64 | aux() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/context-scope/src/main/scala/BlogView.scala: -------------------------------------------------------------------------------- 1 | import ViewState.{Article, Comment} 2 | import korolev.Context 3 | 4 | import scala.concurrent.Future 5 | import scala.concurrent.ExecutionContext.Implicits.global 6 | 7 | final class BlogView(val ctx: Context.Scope[Future, ViewState, ViewState.Tab.Blog, Any]) { 8 | 9 | import ctx._ 10 | 11 | import levsha.dsl._ 12 | import html._ 13 | 14 | private val nameInput: ctx.ElementId = elementId() 15 | 16 | private val commentInput: ctx.ElementId = elementId() 17 | 18 | def onSendComment(article: Article)(access: Access): Future[Unit] = { 19 | for { 20 | name <- access.valueOf(nameInput) 21 | text <- access.valueOf(commentInput) 22 | comment = Comment(text, name) 23 | _ <- access.transition { state => 24 | val xs = state.articles 25 | val i = xs.indexWhere(_.id == article.id) 26 | val x = xs(i) 27 | val upd = xs.updated(i, x.copy(comments = x.comments :+ comment)) 28 | state.copy(articles = upd) 29 | } 30 | } yield () 31 | } 32 | 33 | def onAddComment(article: Article)(access: Access): Future[Unit] = { 34 | access.transition(_.copy(addCommentFor = Some(article.id))) 35 | } 36 | 37 | def apply(state: ViewState.Tab.Blog): Node = optimize { 38 | div( 39 | width @= "500px", 40 | state.articles map { article => 41 | div( 42 | p(article.text), 43 | div(marginTop @= "20px", marginLeft @= "20px", 44 | article.comments map { comment => 45 | div( 46 | div(fontWeight @= "bold", s"${comment.author}:"), 47 | p(comment.text) 48 | ) 49 | }, 50 | state.addCommentFor match { 51 | case Some(article.id) => 52 | form( 53 | input(nameInput, `type` := "text", placeholder := "Name"), 54 | input(commentInput, `type` := "text", placeholder := "Comment"), 55 | button("Send comment"), 56 | event("submit")(onSendComment(article)) 57 | ) 58 | case _ => 59 | button( 60 | "Add comment", 61 | event("click")(onAddComment(article)) 62 | ) 63 | } 64 | ) 65 | ) 66 | } 67 | ) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /interop/zio/src/main/scala/korolev/zio/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import korolev.effect.Effect 20 | import _root_.zio.{Runtime, Task, ZIO} 21 | 22 | package object zio { 23 | 24 | /** 25 | * Provides [[Effect]] instance for ZIO[Any, Throwable, *]. 26 | * Use this method if your app uses [[Throwable]] to express errors. 27 | */ 28 | def taskEffectInstance[R](runtime: Runtime[R]): Effect[Task] = 29 | new ZioEffect[Any, Throwable](runtime, identity, identity) 30 | 31 | /** 32 | * Provides [[Effect]] instance for ZIO with arbitrary runtime 33 | * and error types. Korolev uses Throwable inside itself. 34 | * That means if you want to work with your own [[E]], 35 | * you should provide functions to convert [[Throwable]] 36 | * to [[E]] and vice versa. 37 | * 38 | * {{{ 39 | * sealed trait MyError 40 | * object MyError { 41 | * case class UserNotFound(id: Long) extends MyError 42 | * case object DoNotLikeIt extends MyError 43 | * case class Unexpected(e: Throwable) extends MyError 44 | * } 45 | * case class MyErrorException(error: MyError) extends Throwable 46 | * 47 | * val runtime = new DefaultRuntime {} 48 | * implicit val zioEffect = korolev.zio.zioEffectInstance(runtime)(MyError.Unexpected)(MyErrorException) 49 | * 50 | * val ctx = Context[IO[MyError, *], MyState, Any] 51 | * }}} 52 | */ 53 | final def zioEffectInstance[R, E](runtime: Runtime[R]) 54 | (liftError: Throwable => E) 55 | (unliftError: E => Throwable): Effect[ZIO[R, E, *]] = 56 | new ZioEffect[R, E](runtime, liftError, unliftError) 57 | } 58 | -------------------------------------------------------------------------------- /examples/zio/src/main/scala/ZioExample.scala: -------------------------------------------------------------------------------- 1 | import korolev.Context 2 | import korolev.effect.Effect 3 | import korolev.server.{KorolevServiceConfig, StateLoader} 4 | import korolev.state.javaSerialization.* 5 | import korolev.zio.taskEffectLayer 6 | import korolev.server.standalone 7 | import korolev.server 8 | import zio.* 9 | 10 | import java.net.InetSocketAddress 11 | import scala.concurrent.ExecutionContext.Implicits.global 12 | 13 | object ZioExample extends ZIOAppDefault { 14 | 15 | val address = new InetSocketAddress("localhost", 8080) 16 | 17 | val ctx = Context[Task, Option[Int], Any] 18 | 19 | import ctx._ 20 | 21 | val aInput = elementId() 22 | val bInput = elementId() 23 | 24 | import levsha.dsl._ 25 | import html._ 26 | 27 | def renderForm(maybeResult: Option[Int]) = optimize { 28 | form( 29 | input( 30 | aInput, 31 | name := "a-input", 32 | `type` := "number", 33 | event("input")(onChange) 34 | ), 35 | span("+"), 36 | input( 37 | bInput, 38 | name := "b-input", 39 | `type` := "number", 40 | event("input")(onChange) 41 | ), 42 | span(s"= ${maybeResult.fold("?")(_.toString)}") 43 | ) 44 | } 45 | 46 | def onChange(access: Access) = 47 | for { 48 | a <- access.valueOf(aInput) 49 | _ <- ZIO.logInfo(s"a = $a") 50 | b <- access.valueOf(bInput) 51 | _ <- ZIO.logInfo(s"b = $b") 52 | _ <- access.transition(_ => Some(a.toInt + b.toInt)).unless(a.trim.isEmpty || b.trim.isEmpty) 53 | } yield () 54 | 55 | final val app = ZIO.service[Effect[Task]].flatMap { implicit taskEffect => 56 | val config = 57 | KorolevServiceConfig[Task, Option[Int], Any]( 58 | stateLoader = StateLoader.default(None), 59 | document = maybeResult => optimize { 60 | Html( 61 | body(renderForm(maybeResult)) 62 | ) 63 | } 64 | ) 65 | for { 66 | _ <- ZIO.logInfo(s"Try to start server at $address") 67 | handler <- standalone.buildServer[Task, Array[Byte]]( 68 | service = server.korolevService(config), 69 | address = address, 70 | gracefulShutdown = false 71 | ) 72 | _ <- ZIO.unit.forkDaemon *> ZIO.logInfo(s"Server started") 73 | _ <- handler.awaitShutdown() 74 | } yield () 75 | } 76 | 77 | final val run = 78 | app.provide(taskEffectLayer) 79 | } -------------------------------------------------------------------------------- /examples/context-scope/src/main/scala/ContextScopeExample.scala: -------------------------------------------------------------------------------- 1 | import ViewState.Tab.{About, Blog} 2 | import korolev.* 3 | import korolev.akka.* 4 | import korolev.server.* 5 | import korolev.state.javaSerialization.* 6 | import korolev.util.Lens 7 | 8 | import scala.concurrent.Future 9 | import scala.concurrent.ExecutionContext.Implicits.global 10 | 11 | object ContextScopeExample extends SimpleAkkaHttpKorolevApp { 12 | 13 | val context = Context[Future, ViewState, Any] 14 | 15 | import context._ 16 | import levsha.dsl._ 17 | import html._ 18 | 19 | final private val blogLens = Lens[ViewState, Blog]( 20 | read = { case ViewState(_, s: Blog) => s }, 21 | write = { case (orig, s) => orig.copy(tab = s) } 22 | ) 23 | 24 | final private val blogView = new BlogView(context.scope(blogLens)) 25 | 26 | val service: AkkaHttpService = akkaHttpService { 27 | KorolevServiceConfig[Future, ViewState, Any] ( 28 | stateLoader = StateLoader.default(ViewState("My blog", Blog.default)), 29 | document = { state => 30 | val isBlog = state.tab.isInstanceOf[Blog] 31 | val isAbout = state.tab.isInstanceOf[About] 32 | 33 | optimize { 34 | Html( 35 | body( 36 | h1(state.blogName), 37 | div( 38 | div( 39 | when(isBlog)(fontWeight @= "bold"), 40 | when(isBlog)(borderBottom @= "1px solid black"), 41 | event("click")(access => access.transition(_.copy(tab = Blog.default))), 42 | padding @= "5px", 43 | display @= "inline-block", 44 | "Blog" 45 | ), 46 | div( 47 | when(isAbout)(fontWeight @= "bold"), 48 | when(isAbout)(borderBottom @= "1px solid black"), 49 | event("click")(access => access.transition(_.copy(tab = About.default))), 50 | padding @= "5px", 51 | display @= "inline-block", 52 | "About" 53 | ) 54 | ), 55 | div( 56 | marginTop @= "20px", 57 | state.tab match { 58 | case blog: Blog => blogView(blog) 59 | case about: About => p(about.text) 60 | } 61 | ) 62 | ) 63 | ) 64 | } 65 | } 66 | ) 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/services/ServerSideRenderingService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal.services 18 | 19 | import korolev.effect.Effect 20 | import korolev.effect.syntax._ 21 | import korolev.server.internal.{Cookies, Html5RenderContext, HttpResponse} 22 | import korolev.server.{HttpRequest, HttpResponse, KorolevServiceConfig} 23 | import korolev.web.Response.Status 24 | import korolev.web.{Headers, PathAndQuery} 25 | 26 | private[korolev] final class ServerSideRenderingService[F[_]: Effect, S, M](sessionsService: SessionsService[F, S, _], 27 | pageService: PageService[F, S, M], 28 | config: KorolevServiceConfig[F, S, M]) { 29 | 30 | def canBeRendered(pq: PathAndQuery): Boolean = 31 | config.router.toState.isDefinedAt(pq) 32 | 33 | def serverSideRenderedPage(request: HttpRequest[F]): F[HttpResponse[F]] = { 34 | 35 | for { 36 | qsid <- sessionsService.initSession(request) 37 | state <- sessionsService.initAppState(qsid, request) 38 | rc = new Html5RenderContext[F, S, M]() 39 | proxy = pageService.setupStatelessProxy(rc, qsid) 40 | _ = rc.builder.append("\n") 41 | _ = config.document(state)(proxy) 42 | response <- HttpResponse( 43 | Status.Ok, 44 | rc.mkString, 45 | Seq( 46 | Headers.ContentTypeHtmlUtf8, 47 | Headers.CacheControlNoCache, 48 | Headers.setCookie(Cookies.DeviceId, qsid.deviceId, config.rootPath.mkString, 49 | maxAge = 60 * 60 * 24 * 365 * 10 /* 10 years */) 50 | ) 51 | ) 52 | } yield { 53 | response 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /misc/performance-benchmark/src/main/scala/korolev/ScenarioLoader.scala: -------------------------------------------------------------------------------- 1 | package korolev 2 | 3 | import java.io.File 4 | 5 | import korolev.data.FromServer.Procedure 6 | import korolev.data.ToServer.Callback 7 | import korolev.data.{Scenario, ScenarioStep} 8 | 9 | import scala.concurrent.{ExecutionContext, Future} 10 | import ujson._ 11 | 12 | object ScenarioLoader { 13 | 14 | final val FromServerArrow = "->" 15 | final val ToServerArrow = "<-" 16 | 17 | def fromFile(file: File)(implicit executionContext: ExecutionContext): Future[Either[List[(Int, String)], Scenario]] = 18 | Future { 19 | val source = io.Source.fromFile(file) 20 | fromString(source.mkString).map { steps => 21 | Scenario(file.getName, steps) 22 | } 23 | }.recover { 24 | case ex => 25 | println(ex) 26 | throw ex 27 | } 28 | 29 | def fromString(s: String): Either[List[(Int, String)], Vector[ScenarioStep]] = { 30 | 31 | val lines = s 32 | .split('\n') 33 | .toList 34 | .zipWithIndex 35 | .map { 36 | case (line, i) if line.indexOf(FromServerArrow) > -1 => 37 | val index = line.indexOf(FromServerArrow) 38 | val source = line.substring(index + FromServerArrow.length) 39 | val json = read(source) 40 | Procedure 41 | .fromJson(json) 42 | .map(ScenarioStep.Expect(None, _)) 43 | .left 44 | .map(i -> _) 45 | case (line, i) if line.indexOf(ToServerArrow) > -1 => 46 | val index = line.indexOf(ToServerArrow) 47 | val source = line.substring(index + ToServerArrow.length) 48 | val json = read(source) 49 | Callback 50 | .fromJson(json) 51 | .map(value => ScenarioStep.Send(None, value)) 52 | .left 53 | .map(i -> _) 54 | } 55 | 56 | def sort(errors: List[(Int, String)], 57 | steps: List[ScenarioStep], 58 | lines: List[Either[(Int, String), ScenarioStep]]): (List[(Int, String)], List[ScenarioStep]) = { 59 | lines match { 60 | case Left(x) :: xs => sort(x :: errors, steps, xs) 61 | case Right(x) :: xs => sort(errors, x :: steps, xs) 62 | case Nil => (errors, steps) 63 | } 64 | } 65 | 66 | val (errors, stepsReversed) = sort(Nil, Nil, lines) 67 | if (errors.nonEmpty) Left(errors) 68 | else Right(stepsReversed.reverse.toVector) 69 | } 70 | } -------------------------------------------------------------------------------- /modules/web/src/main/scala/korolev/web/Headers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.web 18 | 19 | import java.util.Base64 20 | 21 | object Headers { 22 | 23 | def basicAuthorization(userInfo: String): (String, String) = { 24 | val credentials = Base64.getEncoder.encodeToString(userInfo.getBytes) 25 | Authorization -> s"Basic $credentials" 26 | } 27 | 28 | final val Authorization = "Authorization" 29 | final val Host = "Host" 30 | final val Connection = "Connection" 31 | final val Upgrade = "Upgrade" 32 | final val ContentType = "Content-Type" 33 | final val ContentLength = "Content-Length" 34 | final val Cookie = "Cookie" 35 | final val SetCookie = "Set-Cookie" 36 | final val CacheControl = "Cache-Control" 37 | final val Pragma = "Pragma" 38 | final val SecWebSocketKey = "Sec-WebSocket-Key" 39 | final val SecWebSocketAccept = "Sec-WebSocket-Accept" 40 | final val TransferEncoding = "Transfer-Encoding" 41 | final val SecWebSocketVersion = "Sec-WebSocket-Version" 42 | final val SecWebSocketProtocol = "Sec-WebSocket-Protocol" 43 | final val AcceptEncoding = "Accept-Encoding" 44 | 45 | final val SecWebSocketVersion13 = SecWebSocketVersion -> "13" 46 | final val TransferEncodingChunked = TransferEncoding -> "chunked" 47 | final val CacheControlNoCache = CacheControl -> "no-store, no-cache, must-revalidate" 48 | final val PragmaNoCache = Pragma -> "no-cache" 49 | final val ContentTypeTextUtf8 = ContentType -> "text/plain; charset=utf-8" 50 | final val ContentTypeHtmlUtf8 = ContentType -> "text/html; charset=utf-8" 51 | final val ConnectionUpgrade = Connection -> "Upgrade" 52 | final val UpgradeWebSocket = Upgrade -> "websocket" 53 | 54 | def setCookie(cookie: String, value: String, path: String, maxAge: Int): (String, String) = 55 | SetCookie -> s"$cookie=$value; Path=$path; Max-Age=$maxAge; SameSite=Lax; HttpOnly" 56 | 57 | } 58 | -------------------------------------------------------------------------------- /modules/testkit/src/test/scala/PseudoHtmlSpec.scala: -------------------------------------------------------------------------------- 1 | import org.scalatest.flatspec.AnyFlatSpec 2 | import org.scalatest.matchers.should.Matchers 3 | import korolev.testkit._ 4 | import levsha.{Id, XmlNs} 5 | 6 | class PseudoHtmlSpec extends AnyFlatSpec with Matchers { 7 | 8 | "PseudoDom.render" should "map levsha.Node to PseudoDom.Element" in { 9 | import levsha.dsl._ 10 | import html._ 11 | 12 | val node = div() 13 | val rr = PseudoHtml.render(node) 14 | 15 | rr.pseudoDom shouldEqual PseudoHtml.Element(Id("1"), XmlNs.html, "div", Map.empty, Map.empty, List.empty) 16 | } 17 | 18 | it should "map nested levsha.Node to correspondent pseudo DOM elements" in { 19 | import levsha.dsl._ 20 | import html._ 21 | import PseudoHtml._ 22 | 23 | val node = body(ul(li("1"), li("2"), li("3"))) 24 | val rr = PseudoHtml.render(node) 25 | 26 | rr.pseudoDom shouldEqual Element(Id("1"), XmlNs.html, "body", Map.empty, Map.empty, List( 27 | Element(Id("1_1"), XmlNs.html, "ul", Map.empty, Map.empty, List( 28 | Element(Id("1_1_1"), XmlNs.html, "li", Map.empty, Map.empty, List(Text(Id("1_1_1_1"), "1"))), 29 | Element(Id("1_1_2"), XmlNs.html, "li", Map.empty, Map.empty, List(Text(Id("1_1_2_1"), "2"))), 30 | Element(Id("1_1_3"), XmlNs.html, "li", Map.empty, Map.empty, List(Text(Id("1_1_3_1"), "3"))), 31 | )) 32 | )) 33 | } 34 | 35 | it should "map attributes well" in { 36 | import levsha.dsl._ 37 | import html._ 38 | 39 | val node = div(clazz := "foo bar", id := "baz") 40 | val rr = PseudoHtml.render(node) 41 | 42 | rr.pseudoDom shouldEqual PseudoHtml.Element(Id("1"), XmlNs.html, "div", Map("class" -> "foo bar", "id" -> "baz"), Map.empty, List.empty) 43 | } 44 | 45 | it should "map styles well" in { 46 | import levsha.dsl._ 47 | import html._ 48 | 49 | val node = div(backgroundColor @= "red", border @= "1px") 50 | val rr = PseudoHtml.render(node) 51 | 52 | rr.pseudoDom shouldEqual PseudoHtml.Element(Id("1"), XmlNs.html, "div", Map.empty, Map("background-color" -> "red", "border" -> "1px"), List.empty) 53 | } 54 | 55 | "byName" should "find list of Element by value of name attribute" in { 56 | 57 | import levsha.dsl._ 58 | import html._ 59 | 60 | val dom = body( 61 | div("Hello world"), 62 | button( 63 | name := "my-button", 64 | "Click me" 65 | ) 66 | ) 67 | 68 | val pd = PseudoHtml.render(dom).pseudoDom 69 | pd.byName("my-button").headOption.map(_.id) shouldEqual Some(Id("1_2")) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /modules/standalone/src/main/scala/korolev/server/KorolevApp.scala: -------------------------------------------------------------------------------- 1 | package korolev.server 2 | 3 | import java.net.{InetSocketAddress, SocketAddress} 4 | import java.nio.channels.AsynchronousChannelGroup 5 | import java.util.concurrent.Executors 6 | 7 | import korolev.Context 8 | import korolev.data.BytesLike 9 | import korolev.effect.Effect 10 | import korolev.effect.io.ServerSocket.ServerSocketHandler 11 | import korolev.effect.syntax._ 12 | import korolev.state.{StateDeserializer, StateSerializer} 13 | 14 | import scala.concurrent.{ExecutionContext, ExecutionContextExecutorService} 15 | 16 | abstract class KorolevApp[ 17 | F[_] : Effect, 18 | B: BytesLike, 19 | S: StateSerializer : StateDeserializer, 20 | M](address: SocketAddress = new InetSocketAddress("localhost", 8080), 21 | gracefulShutdown: Boolean = false) { 22 | 23 | implicit lazy val executionContext: ExecutionContextExecutorService = 24 | ExecutionContext.fromExecutorService(Executors.newCachedThreadPool()) 25 | 26 | val context: Context[F, S, M] = Context[F, S, M] 27 | 28 | val config: F[KorolevServiceConfig[F, S, M]] 29 | 30 | val channelGroup: AsynchronousChannelGroup = 31 | AsynchronousChannelGroup.withThreadPool(executionContext) 32 | 33 | private def logServerStarted(config: KorolevServiceConfig[F, S, M]) = Effect[F].delay { 34 | config.reporter.info(s"Server stated at $address") 35 | } 36 | 37 | private def addShutdownHook(config: KorolevServiceConfig[F, S, M], 38 | handler: ServerSocketHandler[F]) = Effect[F].delay { 39 | Runtime.getRuntime.addShutdownHook( 40 | new Thread { 41 | override def run(): Unit = { 42 | config.reporter.info("Shutdown signal received.") 43 | config.reporter.info("Stopping serving new requests.") 44 | config.reporter.info("Waiting clients disconnection.") 45 | handler 46 | .stopServingRequests() 47 | .after(handler.awaitShutdown()) 48 | .runSyncForget(config.reporter) 49 | } 50 | } 51 | ) 52 | } 53 | 54 | def main(args: Array[String]): Unit = { 55 | val job = 56 | for { 57 | cfg <- config 58 | handler <- standalone.buildServer[F, B](korolevService(cfg), address, channelGroup, gracefulShutdown) 59 | _ <- logServerStarted(cfg) 60 | _ <- addShutdownHook(cfg, handler) 61 | _ <- handler.awaitShutdown() 62 | } yield () 63 | Effect[F].run(job) match { 64 | case Left(e) => e.printStackTrace() 65 | case Right(_) => 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/util/JsCode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.util 18 | 19 | import korolev.Context.ElementId 20 | import korolev.util.JsCode.{Element, Part} 21 | 22 | import scala.annotation.tailrec 23 | 24 | sealed trait JsCode { 25 | def ::(s: String): Part = Part(s, this) 26 | def ::(s: ElementId): Element = Element(s, this) 27 | 28 | def mkString(elementToId: ElementId => levsha.Id): String = { 29 | @tailrec 30 | def aux(acc: String, jsCode: JsCode): String = jsCode match { 31 | case JsCode.End => acc 32 | case JsCode.Part(x, xs) => aux(acc + x, xs) 33 | case JsCode.Element(x, xs) => 34 | val id = elementToId(x.asInstanceOf[ElementId]) 35 | aux(acc + s"Korolev.element('$id')", xs) 36 | } 37 | aux("", this) 38 | } 39 | } 40 | 41 | object JsCode { 42 | 43 | case class Part(value: String, tail: JsCode) extends JsCode 44 | case class Element(elementId: ElementId, tail: JsCode) extends JsCode 45 | case object End extends JsCode 46 | 47 | def apply(s: String): JsCode = s :: End 48 | 49 | def apply(parts: List[String], inclusions: List[Any]): JsCode = { 50 | @tailrec 51 | def combine(acc: JsCode, ps: List[String], is: List[Any]): JsCode = ps match { 52 | case Nil => acc 53 | case px :: pxs => 54 | is match { 55 | case (ix: ElementId) :: ixs => combine(ix :: px :: acc, pxs, ixs) 56 | case (ix: String) :: ixs => combine(ix :: px :: acc, pxs, ixs) 57 | case ix :: ixs => combine(ix.toString :: px :: acc, pxs, ixs) 58 | case Nil => combine(px :: acc, pxs, Nil) 59 | } 60 | } 61 | @tailrec 62 | def reverse(acc: JsCode, jsCode: JsCode): JsCode = jsCode match { 63 | case Part(x, xs) => reverse(x :: acc, xs) 64 | case Element(x, xs) => reverse(x.asInstanceOf[ElementId] :: acc, xs) 65 | case End => acc 66 | } 67 | reverse(End, combine(End, parts, inclusions)) 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /modules/web-dsl/src/main/scala/korolev/web/dsl/BodyFactory.scala: -------------------------------------------------------------------------------- 1 | package korolev.web.dsl 2 | 3 | import korolev.data.BytesLike 4 | import korolev.web.Headers 5 | import korolev.effect.{Effect, Stream} 6 | import korolev.data.syntax._ 7 | import korolev.effect.syntax._ 8 | 9 | trait BodyFactory[F[_], A, B] { 10 | def mkBody(source: A): F[BodyFactory.Body[B]] 11 | } 12 | 13 | trait EmptyBodyFactory[A] { 14 | def emptyBody: BodyFactory.Body[A] 15 | } 16 | 17 | object BodyFactory { 18 | 19 | final case class Body[B](content: B, headers: Map[String, String], contentLength: Option[Long]) 20 | 21 | private final class StringBytesLikeBodyFactory[F[_]: Effect, B: BytesLike] extends BodyFactory[F, String, B] { 22 | def mkBody(source: String): F[Body[B]] = Effect[F].pure { 23 | val bytes = BytesLike[B].utf8(source) 24 | Body( 25 | content = bytes, 26 | headers = Map(Headers.ContentTypeTextUtf8), 27 | contentLength = Some(bytes.length) 28 | ) 29 | } 30 | } 31 | 32 | private final class JsonBodyFactory[F[_]: Effect, J: JsonCodec, B](implicit bf: BodyFactory[F, String, B]) 33 | extends BodyFactory[F, J, B] { 34 | 35 | def mkBody(source: J): F[Body[B]] = 36 | bf.mkBody(implicitly[JsonCodec[J]].encode(source)).map { body => 37 | body.copy(headers = Map(Headers.ContentType -> "application/json; charset=utf-8")) 38 | } 39 | } 40 | 41 | private final class StreamedBodyFactory[F[_]: Effect, A, B](implicit bf: BodyFactory[F, A, B]) 42 | extends BodyFactory[F, A, Stream[F, B]] { 43 | 44 | def mkBody(source: A): F[Body[Stream[F, B]]] = { 45 | bf.mkBody(source).flatMap { body => 46 | Stream(body.content).mat().map { stream => 47 | Body( 48 | content = stream, 49 | headers = body.headers, 50 | contentLength = body.contentLength 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | 57 | implicit def stringBytesLikeBodyFactory[F[_]: Effect, B: BytesLike]: BodyFactory[F, String, B] = 58 | new StringBytesLikeBodyFactory[F, B]() 59 | 60 | implicit def jsonBodyFactory[F[_]: Effect, J: JsonCodec, B](implicit bf: BodyFactory[F, String, B]): BodyFactory[F, J, B] = 61 | new JsonBodyFactory[F, J, B]() 62 | 63 | implicit def streamedBodyFactory[F[_]: Effect, A, B](implicit bf: BodyFactory[F, A, B]): BodyFactory[F, A, Stream[F, B]] = 64 | new StreamedBodyFactory[F, A, B]() 65 | 66 | implicit def streamedEmptyBodyFactory[F[_]: Effect, A]: EmptyBodyFactory[Stream[F, A]] = new EmptyBodyFactory[Stream[F, A]] { 67 | val emptyBody: Body[Stream[F, A]] = 68 | Body(Stream.empty, Map.empty, None) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /modules/standalone/src/main/scala/korolev/server/standalone.scala: -------------------------------------------------------------------------------- 1 | package korolev.server 2 | 3 | import java.net.SocketAddress 4 | import java.nio.channels.AsynchronousChannelGroup 5 | import korolev.data.{Bytes, BytesLike} 6 | import korolev.data.syntax.* 7 | import korolev.effect.io.ServerSocket 8 | import korolev.effect.syntax.* 9 | import korolev.effect.{Effect, Stream} 10 | import korolev.http.HttpServer 11 | import korolev.http.protocol.WebSocketProtocol 12 | import korolev.web.{Headers, Request} 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | object standalone { 17 | 18 | def buildServer[F[_]: Effect, B: BytesLike](service: KorolevService[F], 19 | address: SocketAddress, 20 | group: AsynchronousChannelGroup = null, 21 | gracefulShutdown: Boolean) 22 | (implicit ec: ExecutionContext): F[ServerSocket.ServerSocketHandler[F]] = { 23 | val webSocketProtocol = new WebSocketProtocol[B] 24 | HttpServer[F, B](address, group = group, gracefulShutdown = gracefulShutdown) { request => 25 | val protocols = request 26 | .header(Headers.SecWebSocketProtocol) 27 | .toSeq 28 | .flatMap(_.split(',')) 29 | .filterNot(_.isBlank) 30 | webSocketProtocol.findIntention(request) match { 31 | case Some(intention) => 32 | val f = webSocketProtocol.upgrade[F](intention) { (request: Request[Stream[F, WebSocketProtocol.Frame.Merged[B]]]) => 33 | val b2 = request.body.collect { 34 | case WebSocketProtocol.Frame.Binary(message, _) => 35 | message.as[Bytes] 36 | } 37 | // TODO service.ws should work with websocket frame 38 | val wsRequest = WebSocketRequest(request.copy(body = b2), protocols) 39 | service.ws(wsRequest).map { wsResponse => 40 | val response = wsResponse.httpResponse 41 | val updatedBody: Stream[F, WebSocketProtocol.Frame.Merged[B]] = 42 | response.body.map(m => WebSocketProtocol.Frame.Binary(m.as[B])) 43 | val updatedHeaders = (Headers.SecWebSocketProtocol -> wsResponse.selectedProtocol) +: response.headers 44 | response.copy(body = updatedBody, headers = updatedHeaders) 45 | } 46 | } 47 | f(request) 48 | case _ => 49 | // This is just HTTP query 50 | service 51 | .http(request.copy(body = request.body.map(Bytes.wrap(_)))) 52 | .map(response => response.copy(body = response.body.map(_.as[B]))) 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /interop/zio-streams/src/test/scala/korolev/zio/streams/ZIOStreamsInteropTest.scala: -------------------------------------------------------------------------------- 1 | package korolev.zio.streams 2 | 3 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 4 | import zio.{Runtime, Task} 5 | import korolev.zio._ 6 | import org.scalatest.flatspec.AsyncFlatSpec 7 | import org.scalatest.matchers.should.Matchers 8 | import zio.stream.{ZSink, ZStream} 9 | 10 | class ZIOStreamsInteropTest extends AsyncFlatSpec with Matchers { 11 | 12 | implicit val runtime: Runtime[zio.ZEnv] = Runtime.default 13 | implicit val effect: KorolevEffect[Task] = taskEffectInstance[Any](runtime) 14 | 15 | "KorolevStream.toZStream" should "provide zio.Stream that contain exactly same values as original Korolev stream" in { 16 | 17 | val values = List(1, 2, 3, 4, 5) 18 | 19 | val io = KorolevStream(values: _*) 20 | .mat[Task]() 21 | .flatMap { (korolevStream: KorolevStream[Task, Int]) => 22 | korolevStream 23 | .toZStream 24 | .run(ZSink.foldLeft(List.empty[Int]){ case (acc, v) => acc :+ v}) 25 | } 26 | 27 | runtime.unsafeRunToFuture(io).map { result => 28 | result shouldEqual values 29 | } 30 | } 31 | 32 | it should "provide zio.Stream which handle values asynchronously" in { 33 | val queue = Queue[Task, Int]() 34 | val stream: KorolevStream[Task, Int] = queue.stream 35 | val io = 36 | for { 37 | fiber <- KorolevEffect[Task] 38 | .start { 39 | Task(stream 40 | .toZStream. 41 | run(ZSink.foldLeft(List.empty[Int]){ case (acc, v) => acc :+ v}) 42 | ) 43 | } 44 | _ <- queue.offer(1) 45 | _ <- queue.offer(2) 46 | _ <- queue.offer(3) 47 | _ <- queue.offer(4) 48 | _ <- queue.offer(5) 49 | _ <- queue.stop() 50 | result <- fiber.join() 51 | } yield { 52 | result.map(r => r shouldEqual List(1, 2, 3, 4, 5)) 53 | } 54 | runtime.unsafeRunToFuture(io.flatten) 55 | } 56 | 57 | "ZStream.toKorolevStream" should "provide korolev.effect.Stream that contain exactly same values as original zio.Stream" in { 58 | 59 | val v1 = Vector(1, 2, 3, 4, 5) 60 | val v2 = Vector(5, 4, 3, 2, 1) 61 | val values = v1 ++ v2 62 | val io = ZStream.fromIterable(v1) 63 | .concat(ZStream.fromIterable(v2)) // concat need for multiple chunks test 64 | .toKorolev 65 | .use { (korolevStream: KorolevStream[Task, Seq[Int]]) => 66 | korolevStream 67 | .unchunk 68 | .fold(Vector.empty[Int])((acc, value) => acc :+ value) 69 | .map(result => 70 | result shouldEqual values 71 | ) 72 | } 73 | runtime.unsafeRunToFuture(io) 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /interop/zio2-streams/src/test/scala/korolev/zio/streams/ZIOStreamsInteropTest.scala: -------------------------------------------------------------------------------- 1 | package korolev.zio.streams 2 | 3 | import korolev.effect.{Queue, Effect => KorolevEffect, Stream => KorolevStream} 4 | import zio.{Runtime, Task} 5 | import korolev.zio._ 6 | import _root_.zio._ 7 | import org.scalatest.flatspec.AsyncFlatSpec 8 | import org.scalatest.matchers.should.Matchers 9 | import zio.stream.{ZSink, ZStream} 10 | 11 | class ZIOStreamsInteropTest extends AsyncFlatSpec with Matchers { 12 | 13 | private val runtime = Runtime.default 14 | private implicit val effect: KorolevEffect[Task] = taskEffectInstance[Any](runtime) 15 | 16 | "KorolevStream.toZStream" should "provide zio.Stream that contain exactly same values as original Korolev stream" in { 17 | 18 | val values = List(1, 2, 3, 4, 5) 19 | 20 | val io = KorolevStream(values: _*) 21 | .mat[Task]() 22 | .flatMap { (korolevStream: KorolevStream[Task, Int]) => 23 | korolevStream.toZStream 24 | .run(ZSink.foldLeft(List.empty[Int]) { case (acc, v) => acc :+ v }) 25 | } 26 | 27 | zio.Unsafe.unsafeCompat { implicit u => 28 | runtime.unsafe.runToFuture(io).map { result => 29 | result shouldEqual values 30 | } 31 | } 32 | } 33 | 34 | it should "provide zio.Stream which handle values asynchronously" in { 35 | val queue = Queue[Task, Int]() 36 | val stream: KorolevStream[Task, Int] = queue.stream 37 | val io = 38 | for { 39 | fiber <- stream.toZStream.run(ZSink.foldLeft(List.empty[Int]) { case (acc, v) => acc :+ v }).fork 40 | _ <- queue.offer(1) 41 | _ <- queue.offer(2) 42 | _ <- queue.offer(3) 43 | _ <- queue.offer(4) 44 | _ <- queue.offer(5) 45 | _ <- queue.stop() 46 | result <- fiber.join 47 | } yield { 48 | result shouldEqual List(1, 2, 3, 4, 5) 49 | } 50 | zio.Unsafe.unsafeCompat { implicit u => 51 | runtime.unsafe.runToFuture(io) 52 | } 53 | } 54 | 55 | "ZStream.toKorolevStream" should "provide korolev.effect.Stream that contain exactly same values as original zio.Stream" in { 56 | 57 | val v1 = Vector(1, 2, 3, 4, 5) 58 | val v2 = Vector(5, 4, 3, 2, 1) 59 | val values = v1 ++ v2 60 | val io = 61 | ZStream 62 | .fromIterable(v1) 63 | .concat(ZStream.fromIterable(v2)) // concat need for multiple chunks test 64 | .toKorolev 65 | .flatMap { korolevStream => 66 | korolevStream 67 | .unchunk 68 | .fold(Vector.empty[Int])((acc, value) => acc :+ value) 69 | .map(result => result shouldEqual values) 70 | } 71 | zio.Unsafe.unsafeCompat { implicit u => 72 | runtime.unsafe.runToFuture(io) 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/internal/services/FilesService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server.internal.services 18 | 19 | import korolev.data.Bytes 20 | import korolev.effect.{AsyncTable, Effect, Stream} 21 | import korolev.effect.io.JavaIO 22 | import korolev.effect.syntax._ 23 | import korolev.server.HttpResponse 24 | import korolev.web.PathAndQuery._ 25 | import korolev.web.{Headers, MimeTypes, Path, PathAndQuery, Response} 26 | 27 | private[korolev] final class FilesService[F[_]: Effect](commonService: CommonService[F]) { 28 | 29 | type ResponseFactory = () => F[HttpResponse[F]] 30 | 31 | private val table = AsyncTable.unsafeCreateEmpty[F, Path, ResponseFactory] 32 | 33 | private val notFoundToken = Effect[F].pure(() => commonService.notFoundResponseF) 34 | 35 | def resourceFromClasspath(pq: PathAndQuery): F[HttpResponse[F]] = { 36 | val path = pq.asPath 37 | table 38 | .getFill(path) { 39 | val fsPath = path.mkString 40 | val maybeResourceStream = Option(this.getClass.getResourceAsStream(fsPath)) 41 | maybeResourceStream.fold(notFoundToken) { javaSyncStream => 42 | val _ / fileName = path 43 | val fileExtension = fileName.lastIndexOf('.') match { 44 | case -1 => "bin" // default file extension 45 | case index => fileName.substring(index + 1) 46 | } 47 | val headers = MimeTypes.typeForExtension.get(fileExtension) match { 48 | case Some(mimeType) => Seq(Headers.ContentType -> mimeType) 49 | case None => Nil 50 | } 51 | val size = javaSyncStream.available().toLong 52 | for { 53 | stream <- JavaIO.fromInputStream[F, Bytes](javaSyncStream) // TODO configure chunk size 54 | chunks <- stream.fold(Vector.empty[Bytes])(_ :+ _) 55 | template = Stream.emits(chunks) 56 | } yield { 57 | () => template.mat() map { stream => 58 | Response(Response.Status.Ok, stream, headers, Some(size)) 59 | } 60 | } 61 | } 62 | } 63 | .flatMap(_()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/context-scope/src/main/scala/ViewState.scala: -------------------------------------------------------------------------------- 1 | case class ViewState(blogName: String, tab: ViewState.Tab) 2 | 3 | object ViewState { 4 | 5 | case class Comment(text: String, author: String) 6 | case class Article(id: Int, text: String, comments: List[Comment]) 7 | 8 | sealed trait Tab 9 | 10 | object Tab { 11 | case class Blog(articles: List[Article], addCommentFor: Option[Int] = None) extends Tab 12 | object Blog { 13 | val default: Blog = { 14 | Blog( 15 | List( 16 | Article(0, 17 | """In the beginning, the creators of the Web designed browser 18 | |as a thin client for web servers. The browser displayed 19 | |hypertext pages received from a server. It was simple and 20 | |elegant. As is often the case, a beautiful idea confronted 21 | |reality, and after a few years, browser manufacturers added 22 | |support for a scripting language. At first, it was intended 23 | |only for decorating. Until the middle of the first decade, 24 | |it was considered proper to create websites with JS as just 25 | |as an option.""".stripMargin, 26 | List( 27 | Comment("Yoy are beauty guy! Author, write more!", "Oleg"), 28 | Comment("Blog post is shit, author is an asshole", "Vitalij") 29 | ) 30 | ), 31 | Article(1, 32 | """The modern approach of website development is the result 33 | |of the increasing requirements to user interface interactivity. 34 | |Tasks to improve interactivity fell on the shoulders of 35 | |template designers. Often they do not have the competence 36 | |and authority to develop a "cross-cutting" solution. 37 | |Template designers have learned JS and became front-end 38 | |engineers. The logic gradually began to flow from the 39 | |server to the client. It's convenient for the frontend-guy 40 | |to write everything on the client side. For the backend-guy, 41 | |it's convenient not to think about the user. "I'll give 42 | |you JSON, and then I don't care" -- they say. Two years 43 | |ago, serverless architecture became popular. The suggestion 44 | |was that the JS applications would work directly with the 45 | |database and message queues.""".stripMargin, 46 | List( 47 | Comment("Hello, bear", "Oleg"), 48 | Comment("I didn't read but strongly disagree.", "Vitalij") 49 | ) 50 | ) 51 | ) 52 | ) 53 | } 54 | } 55 | case class About(text: String) extends Tab 56 | object About { 57 | val default = About("I'm a cow") 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/zio-http/src/main/scala/ZioHttpExample.scala: -------------------------------------------------------------------------------- 1 | 2 | import zio.{RIO, Runtime, ZIO, ZIOAppDefault, ExitCode as ZExitCode} 3 | import korolev.Context 4 | import korolev.server.{KorolevServiceConfig, StateLoader} 5 | import korolev.web.PathAndQuery 6 | import korolev.zio.Zio2Effect 7 | import korolev.state.javaSerialization.* 8 | import korolev.zio.http.ZioHttpKorolev 9 | import zio.http.HttpApp 10 | import zio.http.Response 11 | import zio.http.Server 12 | import zio.http.Status 13 | 14 | import scala.concurrent.ExecutionContext 15 | 16 | 17 | object ZioHttpExample extends ZIOAppDefault { 18 | 19 | type AppTask[A] = RIO[Any, A] 20 | 21 | private class Service()(implicit runtime: Runtime[Any]) { 22 | 23 | import levsha.dsl._ 24 | import levsha.dsl.html._ 25 | import scala.concurrent.duration._ 26 | 27 | implicit val ec: ExecutionContext = Runtime.defaultExecutor.asExecutionContext 28 | implicit val effect: Zio2Effect[Any, Throwable] = new Zio2Effect[Any, Throwable](runtime, identity, identity) 29 | 30 | val ctx = Context[ZIO[Any, Throwable, *], Option[Int], Any] 31 | 32 | import ctx._ 33 | 34 | 35 | def config = KorolevServiceConfig [AppTask, Option[Int], Any] ( 36 | stateLoader = StateLoader.default(Option.empty[Int]), 37 | rootPath = PathAndQuery.Root, 38 | document = { 39 | case Some(n) => optimize { 40 | Html( 41 | body( 42 | delay(3.seconds) { access => 43 | access.transition { 44 | case _ => None 45 | } 46 | }, 47 | button( 48 | "Push the button " + n, 49 | event("click") { access => 50 | access.transition { 51 | case s => s.map(_ + 1) 52 | } 53 | } 54 | ), 55 | "Wait 3 seconds!" 56 | ) 57 | ) 58 | } 59 | case None => optimize { 60 | Html( 61 | body( 62 | button( 63 | event("click") { access => 64 | access.transition { _ => Some(1) } 65 | }, 66 | "Push the button" 67 | ) 68 | ) 69 | ) 70 | } 71 | } 72 | ) 73 | 74 | def route(): HttpApp[Any, Throwable] = { 75 | new ZioHttpKorolev[Any].service(config) 76 | } 77 | 78 | } 79 | 80 | private def getAppRoute(): ZIO[Any, Nothing, HttpApp[Any, Throwable]] = { 81 | ZIO.runtime[Any].map { implicit rts => 82 | new Service().route() 83 | } 84 | } 85 | 86 | 87 | override def run = 88 | for { 89 | httpApp <- getAppRoute() 90 | _ <- Server 91 | .serve(httpApp.catchAllZIO(_ => ZIO.succeed(Response.status(Status.InternalServerError)))) 92 | .provide(Server.defaultWithPort(8088)) 93 | .orDie 94 | } yield ZExitCode.success 95 | 96 | } 97 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/Component.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import korolev.Context._ 20 | import korolev.effect.Effect 21 | import korolev.state.{StateDeserializer, StateSerializer} 22 | import levsha.Document.Node 23 | 24 | import scala.util.Random 25 | 26 | /** 27 | * Component definition. Every Korolev application is a component. 28 | * Extent it to declare component in object oriented style. 29 | * 30 | * @param id Unique identifier of the component. 31 | * Use it when you create component declaration dynamically 32 | * 33 | * @tparam F Control monad 34 | * @tparam S State of the component 35 | * @tparam E Type of events produced by component 36 | */ 37 | abstract class Component[ 38 | F[_], 39 | S, 40 | P, 41 | E 42 | ]( 43 | val initialState: Either[P => F[S], S], 44 | val id: String 45 | ) { 46 | 47 | def this(initialState: S) = this(Right(initialState), Component.randomId()) 48 | def this(initialState: S, id: String) = this(Right(initialState), id) 49 | def this(loadState: P => F[S]) = this(Left(loadState), Component.randomId()) 50 | 51 | /** 52 | * Component context. 53 | * 54 | * {{{ 55 | * import context._ 56 | * }}} 57 | */ 58 | val context = Context[F, S, E] 59 | 60 | /** 61 | * Component render 62 | */ 63 | def render(parameters: P, state: S): context.Node 64 | 65 | def renderNoState(parameters: P): context.Node = 66 | levsha.dsl.html.div() 67 | } 68 | 69 | object Component { 70 | 71 | /** (context, state) => document */ 72 | type Render[F[_], S, P, E] = (Context[F, S, E], P, S) => Node[Binding[F, S, E]] 73 | 74 | /** 75 | * Create component in functional style 76 | * @param f Component renderer 77 | * @see [[Component]] 78 | */ 79 | def apply[F[_]: Effect, S: StateSerializer: StateDeserializer, P, E]( 80 | initialState: S, 81 | id: String = Component.randomId())(f: Render[F, S, P, E]): Component[F, S, P, E] = { 82 | new Component[F, S, P, E](initialState, id) { 83 | def render(parameters: P, state: S): Node[Binding[F, S, E]] = f(context, parameters, state) 84 | } 85 | } 86 | 87 | final val TopLevelComponentId = "top-level" 88 | 89 | private[korolev] def randomId() = Random.alphanumeric.take(6).mkString 90 | } 91 | -------------------------------------------------------------------------------- /modules/effect/src/test/scala/korolev/effect/StreamSpec.scala: -------------------------------------------------------------------------------- 1 | package korolev.effect 2 | 3 | import org.scalatest.flatspec.AsyncFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class StreamSpec extends AsyncFlatSpec with Matchers { 7 | 8 | "fold" should "accumulated all values left to right" in { 9 | Stream(1,2,3) 10 | .mat() 11 | .flatMap(_.fold(0) { case (acc, x) => acc + x }) 12 | .map(result => assert(result == 6)) 13 | } 14 | 15 | "concat" should "concatenate two streams" in { 16 | for { 17 | left <- Stream.apply(1,2,3).mat() 18 | right <- Stream.apply(4,5,6).mat() 19 | result <- (left ++ right).fold("") { 20 | case (acc, x) => acc + x 21 | } 22 | } yield assert(result == "123456") 23 | } 24 | 25 | "flatMapAsync" should "merge streams" in { 26 | val seq1 = Seq(1, 2, 3) 27 | val seq2 = Seq(4, 5,6) 28 | val seq3 = Seq(7, 8, 9) 29 | Stream.apply(seq1, seq2, seq3) 30 | .mat() 31 | .flatMap { stream => 32 | stream.flatMapAsync { 33 | chunk => Stream.emits(chunk).mat() 34 | }.fold(Seq.empty[Int]) { case (acc, x) => acc :+ x } 35 | }.map { result => assert(result == seq1 ++ seq2 ++ seq3) } 36 | } 37 | 38 | "flatMapMergeAsync" should "merge streams concurrently with factor 2" in { 39 | val seq1 = Seq(1, 2, 3) 40 | val seq2 = Seq(4, 5,6) 41 | val seq3 = Seq(7, 8, 9) 42 | Stream.apply(seq1, seq2, seq3) 43 | .mat() 44 | .flatMap { stream => 45 | stream.flatMapMergeAsync(2){ 46 | chunk => Stream.emits(chunk).mat() 47 | }.fold(Seq.empty[Int]) { case (acc, x) => acc :+ x } 48 | }.map { result => 49 | assert(result.toSet == (seq1 ++ seq2 ++ seq3).toSet) 50 | } 51 | } 52 | 53 | "flatMapMergeAsync" should "merge streams concurrently with factor 3" in { 54 | val seq1 = Seq(1, 2, 3) 55 | val seq2 = Seq(4, 5,6) 56 | val seq3 = Seq(7, 8, 9) 57 | val seq4 = Seq(10, 11, 12) 58 | Stream.apply(seq1, seq2, seq3, seq4) 59 | .mat() 60 | .flatMap { stream => 61 | stream.flatMapMergeAsync(3){ 62 | chunk => Stream.emits(chunk).mat() 63 | }.fold(Seq.empty[Int]) { case (acc, x) => acc :+ x } 64 | }.map { result => 65 | assert(result.toSet == (seq1 ++ seq2 ++ seq3 ++ seq4).toSet) 66 | } 67 | } 68 | 69 | "flatMapMergeAsync" should "merge streams with empty stream concurrently with factor 3" in { 70 | val seq1 = Seq(1, 2, 3) 71 | val seq2 = Seq(4, 5,6) 72 | val seq3 = Seq() 73 | val seq4 = Seq(10, 11, 12, 13, 14 ,15) 74 | Stream.apply(seq1, seq2, seq3, seq4) 75 | .mat() 76 | .flatMap { stream => 77 | stream.flatMapMergeAsync(3){ 78 | chunk => Stream.emits(chunk).mat() 79 | }.fold(Seq.empty[Int]) { case (acc, x) => acc :+ x } 80 | }.map { result => 81 | assert(result.toSet == (seq1 ++ seq2 ++ seq3 ++ seq4).toSet) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Korolev 2 | 3 | 4 | 5 | [![Build Status](https://travis-ci.org/fomkin/korolev.svg?branch=master)](https://travis-ci.org/fomkin/korolev) 6 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffomkin%2Fkorolev.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffomkin%2Fkorolev?ref=badge_shield) 7 | [![Gitter](https://badges.gitter.im/fomkin/korolev.svg)](https://gitter.im/fomkin/korolev?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 8 | [![Join the chat at https://telegram.me/korolev_io](https://img.shields.io/badge/chat-on_telegram_(russian)-0088cc.svg)](https://telegram.me/korolev_io) 9 | 10 | Not long ago we have entered the era of single-page applications. Some people say that we no longer need a server. They say that JavaScript applications can connect to DBMS directly. Fat clients. **We disagree with this.** This project is an attempt to solve the problems of modern fat web. 11 | 12 | Korolev runs a single-page application on the server side, keeping in the browser only a bridge to receive commands and send events. The page loads instantly and works fast, because it does a minimal amount of computation. It's important that Korolev provides a unified environment for full stack development. Client and server are now combined into a single app without any REST protocol or something else in the middle. 13 | 14 | ## Why? 15 | 16 | * Lightning-fast page loading speed (~6kB of uncompressed JS) 17 | * Comparable to static HTML client-side RAM consumption 18 | * Indexable pages out of the box 19 | * Routing out of the box 20 | * Build extremely large app without increasing size of the page 21 | * No need to make CRUD REST service 22 | * Connect to infrastructure (DBMS, Message queue) directly from application 23 | 24 | ## Examples 25 | 26 | * [Features](https://github.com/fomkin/korolev/tree/master/examples) 27 | * [Multiplayer match-three game build on Korolev](https://match3.fomkin.org/) 28 | * Goldbricker - Encrypted ToDo List (coming at summer 2020) 29 | 30 | ## Documentation 31 | 32 | * [User guide (open site)](https://fomkin.org/korolev/user-guide.html), [(download PDF)](https://fomkin.org/korolev/user-guide.pdf) 33 | * [API overview](https://www.javadoc.io/doc/org.fomkin/korolev_2.13/1.1.0) 34 | 35 | ## Articles 36 | 37 | * [Slimming pill for Web](https://dev.to/fomkin/korolev-slimming-pill-for-web-549a) 38 | * [Лекарство для веба](https://habr.com/ru/post/429028/) 39 | 40 | ## Tools 41 | 42 | * [HTML to Levsha DSL converter](https://fomkin.org/korolev/html-to-levsha) 43 | 44 | [comment]: <> ([![Browser support results](https://fomkin.org/korolev/browser-support.svg)](https://saucelabs.com/u/yelbota)) 45 | 46 | ## License 47 | 48 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffomkin%2Fkorolev.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffomkin%2Fkorolev?ref=badge_large) 49 | -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/syntax.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.effect 18 | 19 | import scala.concurrent.ExecutionContext 20 | 21 | object syntax { 22 | 23 | implicit final class ListEffectOps[F[_]: Effect, A](effects: List[F[A]]) { 24 | def sequence: F[List[A]] = Effect[F].sequence(effects) 25 | } 26 | 27 | implicit final class EffectOps[F[_]: Effect, A](effect: F[A]) { 28 | 29 | def map[B](f: A => B): F[B] = Effect[F].map(effect)(f) 30 | 31 | /** 32 | * Alias for {{{.flatMap(_ => ())}}} 33 | */ 34 | def unit: F[Unit] = Effect[F].map(effect)(_ => ()) 35 | 36 | def flatMap[B](f: A => F[B]): F[B] = Effect[F].flatMap(effect)(f) 37 | 38 | /** 39 | * Alias for [[after]] 40 | */ 41 | def *>[B](fb: => F[B]): F[B] = Effect[F].flatMap(effect)(_ => fb) 42 | 43 | def as[B](b: B): F[B] = Effect[F].map(effect)(_ => b) 44 | 45 | /** 46 | * Do 'm' right after [[effect]] 47 | */ 48 | def after[B](m: => F[B]): F[B] = Effect[F].flatMap(effect)(_ => m) 49 | 50 | def recover[AA >: A](f: PartialFunction[Throwable, AA]): F[AA] = Effect[F].recover[A, AA](effect)(f) 51 | 52 | def recoverF[AA >: A](f: PartialFunction[Throwable, F[AA]]): F[AA] = Effect[F].recoverF[A, AA](effect)(f) 53 | 54 | // def onError(f: Throwable => Unit): F[A] = 55 | // Effect[F].onError(effect)(f) 56 | // 57 | // def onErrorF(f: Throwable => F[Unit]): F[A] = 58 | // Effect[F].onErrorF(effect)(f) 59 | 60 | def start(implicit ec: ExecutionContext): F[Effect.Fiber[F, A]] = Effect[F].start(effect) 61 | 62 | def runAsync(f: Either[Throwable, A] => Unit): Unit = Effect[F].runAsync(effect)(f) 63 | 64 | def runAsyncSuccess(f: A => Unit)(implicit er: Reporter): Unit = 65 | Effect[F].runAsync(effect) { 66 | case Right(x) => f(x) 67 | case Left(e) => er.error("Unhandled error", e) 68 | } 69 | def runSyncForget(implicit reporter: Reporter): Unit = 70 | Effect[F].run(effect) match { 71 | case Left(value) => reporter.error("Unhandled error", value) 72 | case Right(value) => () 73 | } 74 | 75 | def runAsyncForget(implicit er: Reporter): Unit = 76 | Effect[F].runAsync(effect) { 77 | case Right(_) => // do nothing 78 | case Left(e) => er.error("Unhandled error", e) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /project/JsUtils.scala: -------------------------------------------------------------------------------- 1 | import java.nio.charset.StandardCharsets 2 | import java.nio.file.Files 3 | import java.util 4 | 5 | import sbt.{File, IO, Logger} 6 | import com.google.javascript.jscomp.{AbstractCommandLineRunner, CompilationLevel, Compiler, CompilerOptions, SourceFile, SourceMap} 7 | import com.google.javascript.jscomp.CompilerOptions.LanguageMode 8 | 9 | import scala.collection.JavaConverters._ 10 | 11 | object JsUtils { 12 | 13 | def assembleJs(source: File, target: File, log: Logger): Seq[File] = { 14 | 15 | log.info("Assembling ES6 sources using Google Closure Compiler") 16 | 17 | val sourceOutputFile = new File(target, "korolev-client.min.js") 18 | val sourceMapOutputFile = new File(target, "korolev-client.min.js.map") 19 | 20 | val (sourceOutput, compilationResult) = { 21 | val compiler = new Compiler() 22 | val externs = AbstractCommandLineRunner 23 | .getBuiltinExterns(CompilerOptions.Environment.BROWSER) 24 | 25 | val inputs = { 26 | val xs = source.listFiles().map { file => 27 | val path = file.getAbsolutePath 28 | val charset = StandardCharsets.UTF_8 29 | SourceFile.fromFile(path, charset) 30 | } 31 | util.Arrays.asList[SourceFile](xs:_*) 32 | } 33 | val options = { 34 | val options = new CompilerOptions() 35 | options.setLanguageIn(LanguageMode.ECMASCRIPT_2018) 36 | options.setLanguageOut(LanguageMode.ECMASCRIPT5_STRICT) 37 | options.setSourceMapIncludeSourcesContent(true) 38 | options.setSourceMapLocationMappings(List(new SourceMap.PrefixLocationMapping(source.getAbsolutePath, "korolev-sources")).asJava) 39 | options.setSourceMapOutputPath(sourceMapOutputFile.getName) 40 | options.setEnvironment(CompilerOptions.Environment.BROWSER) 41 | 42 | CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options) 43 | options 44 | } 45 | val result = compiler.compile(externs, inputs, options) 46 | compiler.getSourceMap.setWrapperPrefix("(function(){") 47 | (compiler.toSource, result) 48 | } 49 | 50 | val sourceMapOutput = { 51 | val stringBuilder = new java.lang.StringBuilder() 52 | compilationResult.sourceMap.appendTo(stringBuilder, sourceMapOutputFile.getName) 53 | stringBuilder.toString 54 | } 55 | 56 | IO.write(sourceOutputFile, s"(function(){$sourceOutput}).call(this);\n//# sourceMappingURL=/static/korolev-client.min.js.map\n") 57 | IO.write(sourceMapOutputFile, sourceMapOutput) 58 | // 59 | // val korolevSources = new File(target, "korolev-sources") 60 | // val mappingSourceFiles = source 61 | // .listFiles() 62 | // .filter(_.isFile) 63 | // .map { file => 64 | // val targetFile = new File(korolevSources, file.getName) 65 | // IO.copyFile(file, targetFile) 66 | // targetFile 67 | // } 68 | // 69 | // mappingSourceFiles ++ 70 | Seq(sourceOutputFile, sourceMapOutputFile) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /modules/korolev/src/main/es6/bridge.js: -------------------------------------------------------------------------------- 1 | import { Korolev, CallbackType } from './korolev.js'; 2 | import { Connection } from './connection.js'; 3 | 4 | const ProtocolDebugEnabledKey = "$bridge.protocolDebugEnabled"; 5 | 6 | var protocolDebugEnabled = window.localStorage.getItem(ProtocolDebugEnabledKey) === 'true'; 7 | 8 | export class Bridge { 9 | 10 | /** 11 | * @param {Connection} connection 12 | */ 13 | constructor(config, connection) { 14 | this._korolev = new Korolev(config, this._onCallback.bind(this)); 15 | this._korolev.registerRoot(document.children[0]); 16 | this._connection = connection; 17 | this._messageHandler = this._onMessage.bind(this); 18 | 19 | connection.dispatcher.addEventListener("message", this._messageHandler); 20 | 21 | let interval = parseInt(config['heartbeatInterval'], 10); 22 | 23 | if (interval > 0) { 24 | this._intervalId = setInterval(() => this._onCallback(CallbackType.HEARTBEAT), interval); 25 | } 26 | } 27 | 28 | /** 29 | * @param {CallbackType} type 30 | * @param {string} [args] 31 | */ 32 | _onCallback(type, args) { 33 | let message = JSON.stringify(args !== undefined ? [type, args] : [type]); 34 | if (protocolDebugEnabled) 35 | console.log('<-', message); 36 | this._connection.send(message); 37 | } 38 | 39 | _onMessage(event) { 40 | if (protocolDebugEnabled) 41 | console.log('->', event.data); 42 | let commands = /** @type {Array} */ (JSON.parse(event.data)); 43 | let pCode = commands.shift(); 44 | let k = this._korolev; 45 | switch (pCode) { 46 | case 0: k.setRenderNum.apply(k, commands); break; 47 | case 1: 48 | this._connection.disconnect(false); 49 | window.location.reload(); 50 | break; 51 | case 2: k.listenEvent.apply(k, commands); break; 52 | case 3: k.extractProperty.apply(k, commands); break; 53 | case 4: k.modifyDom(commands); break; 54 | case 5: k.focus.apply(k, commands); break; 55 | case 6: k.changePageUrl.apply(k, commands); break; 56 | case 7: k.uploadForm.apply(k, commands); break; 57 | case 8: k.reloadCss.apply(k, commands); break; 58 | case 9: break; 59 | case 10: k.evalJs.apply(k, commands); break; 60 | case 11: k.extractEventData.apply(k, commands); break; 61 | case 12: k.listFiles.apply(k, commands); break; 62 | case 13: k.uploadFile.apply(k, commands); break; 63 | case 14: k.resetForm.apply(k, commands); break; 64 | case 15: k.downloadFile.apply(k, commands); break; 65 | default: console.error(`Procedure ${pCode} is undefined`); 66 | } 67 | } 68 | 69 | destroy() { 70 | clearInterval(this._intervalId); 71 | this._connection.dispatcher.removeEventListener("message", this._messageHandler); 72 | this._korolev.destroy(); 73 | } 74 | } 75 | 76 | /** @param {boolean} value */ 77 | export function setProtocolDebugEnabled(value) { 78 | window.localStorage.setItem(ProtocolDebugEnabledKey, value.toString()); 79 | protocolDebugEnabled = value; 80 | } 81 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/util/AkkaByteStringBytesLike.scala: -------------------------------------------------------------------------------- 1 | package korolev.akka.util 2 | 3 | import akka.util.ByteString 4 | import korolev.data.BytesLike 5 | 6 | import java.nio.ByteBuffer 7 | import java.nio.charset.{Charset, StandardCharsets} 8 | 9 | final class AkkaByteStringBytesLike extends BytesLike[ByteString] { 10 | 11 | val empty: ByteString = ByteString.empty 12 | 13 | def ascii(s: String): ByteString = ByteString.fromString(s, StandardCharsets.US_ASCII) 14 | 15 | def utf8(s: String): ByteString = ByteString.fromString(s, StandardCharsets.UTF_8) 16 | 17 | def wrapArray(bytes: Array[Byte]): ByteString = ByteString.fromArrayUnsafe(bytes) 18 | 19 | def copyBuffer(buffer: ByteBuffer): ByteString = ByteString.fromByteBuffer(buffer) 20 | 21 | def copyToBuffer(b: ByteString, buffer: ByteBuffer): Int = b.copyToBuffer(buffer) 22 | 23 | def copyFromArray(bytes: Array[Byte]): ByteString = ByteString.fromArray(bytes) 24 | 25 | def copyFromArray(bytes: Array[Byte], offset: Int, size: Int): ByteString = ByteString.fromArray(bytes, offset, size) 26 | 27 | def copyToArray(value: ByteString, array: Array[Byte], sourceOffset: Int, targetOffset: Int, length: Int): Unit = 28 | value.drop(sourceOffset).copyToArray(array, targetOffset, length) 29 | 30 | def asAsciiString(bytes: ByteString): String = 31 | bytes.decodeString(StandardCharsets.US_ASCII) 32 | 33 | def asUtf8String(bytes: ByteString): String = 34 | bytes.decodeString(StandardCharsets.UTF_8) 35 | 36 | def asString(bytes: ByteString, charset: Charset): String = 37 | bytes.decodeString(charset) 38 | 39 | def asArray(bytes: ByteString): Array[Byte] = 40 | bytes.toArray 41 | 42 | def asBuffer(bytes: ByteString): ByteBuffer = 43 | bytes.asByteBuffer 44 | 45 | def eq(l: ByteString, r: ByteString): Boolean = 46 | l.equals(r) 47 | 48 | def get(bytes: ByteString, i: Long): Byte = 49 | bytes(i.toInt) 50 | 51 | def length(bytes: ByteString): Long = 52 | bytes.length.toLong 53 | 54 | def concat(left: ByteString, right: ByteString): ByteString = 55 | left.concat(right) 56 | 57 | def slice(bytes: ByteString, start: Long, end: Long): ByteString = 58 | bytes.slice(start.toInt, end.toInt) 59 | 60 | def mapWithIndex(bytes: ByteString, f: (Byte, Long) => Byte): ByteString = { 61 | var i = 0 62 | bytes.map { x => 63 | val res = f(x, i) 64 | i += 1 65 | res 66 | } 67 | } 68 | 69 | def indexOf(where: ByteString, that: Byte): Long = 70 | where.indexOf(that) 71 | 72 | def indexOf(where: ByteString, that: Byte, from: Long): Long = 73 | where.indexOf(that, from.toInt) 74 | 75 | def lastIndexOf(where: ByteString, that: Byte): Long = 76 | where.lastIndexOf(that) 77 | 78 | def indexOfSlice(where: ByteString, that: ByteString): Long = 79 | where.indexOfSlice(that) 80 | 81 | def lastIndexOfSlice(where: ByteString, that: ByteString): Long = 82 | where.lastIndexOfSlice(that) 83 | 84 | def foreach(bytes: ByteString, f: Byte => Unit): Unit = 85 | bytes.foreach(f) 86 | } 87 | -------------------------------------------------------------------------------- /interop/ce3/src/main/scala/korolev/cats/IO3Effect.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.cats 18 | 19 | import _root_.cats.Traverse 20 | import _root_.cats.effect._ 21 | import _root_.cats.effect.unsafe.IORuntime 22 | import _root_.cats.instances.list._ 23 | import korolev.effect.{Effect => KEffect} 24 | 25 | import scala.concurrent.{ExecutionContext, Future} 26 | import scala.util.Try 27 | 28 | class IO3Effect(runtime: IORuntime) extends KEffect[IO] { 29 | 30 | def pure[A](value: A): IO[A] = 31 | IO.pure(value) 32 | 33 | def delay[A](value: => A): IO[A] = 34 | IO.delay(value) 35 | 36 | def fail[A](e: Throwable): IO[A] = IO.raiseError(e) 37 | 38 | def fork[A](m: => IO[A])(implicit ec: ExecutionContext): IO[A] = 39 | m.evalOn(ec) 40 | 41 | def blocking[A](f: => A)(implicit ec: ExecutionContext): IO[A] = 42 | IO.blocking(f) 43 | 44 | def unit: IO[Unit] = 45 | IO.unit 46 | 47 | def never[T]: IO[T] = 48 | IO.never 49 | 50 | def fromTry[A](value: => Try[A]): IO[A] = 51 | IO.fromTry(value) 52 | 53 | def start[A](m: => IO[A])(implicit ec: ExecutionContext): IO[KEffect.Fiber[IO, A]] = 54 | m.startOn(ec) 55 | .map { fiber => 56 | new KEffect.Fiber[IO, A] { 57 | def join(): IO[A] = fiber.joinWithNever 58 | } 59 | } 60 | 61 | def promise[A](cb: (Either[Throwable, A] => Unit) => Unit): IO[A] = 62 | IO.async_(cb) 63 | 64 | def promiseF[A](cb: (Either[Throwable, A] => Unit) => IO[Unit]): IO[A] = 65 | Async[IO].async(cb(_).as(None)) 66 | 67 | def flatMap[A, B](m: IO[A])(f: A => IO[B]): IO[B] = 68 | m.flatMap(f) 69 | 70 | def map[A, B](m: IO[A])(f: A => B): IO[B] = 71 | m.map(f) 72 | 73 | def recover[A, AA >: A](m: IO[A])(f: PartialFunction[Throwable, AA]): IO[AA] = 74 | m.handleErrorWith(e => f.andThen(IO.pure[AA] _).applyOrElse(e, IO.raiseError[AA] _)) 75 | 76 | def recoverF[A, AA >: A](m: IO[A])(f: PartialFunction[Throwable, IO[AA]]): IO[AA] = 77 | m.handleErrorWith(e => f.applyOrElse(e, IO.raiseError[AA] _)) 78 | 79 | def sequence[A](in: List[IO[A]]): IO[List[A]] = 80 | Traverse[List].sequence(in) 81 | 82 | def runAsync[A](m: IO[A])(callback: Either[Throwable, A] => Unit): Unit = 83 | m.unsafeRunAsync(callback)(runtime) 84 | 85 | def run[A](m: IO[A]): Either[Throwable, A] = 86 | Try(m.unsafeRunSync()(runtime)).toEither 87 | 88 | def toFuture[A](m: IO[A]): Future[A] = 89 | m.unsafeToFuture()(runtime) 90 | } 91 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/instances.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka 18 | 19 | import akka.NotUsed 20 | import akka.stream.OverflowStrategy 21 | import akka.stream.scaladsl.{Sink, Source} 22 | import akka.util.ByteString 23 | import korolev.akka.util.{AkkaByteStringBytesLike, KorolevStreamPublisher, KorolevStreamSubscriber} 24 | import korolev.data.BytesLike 25 | import korolev.effect.{Effect, Stream} 26 | import org.reactivestreams.Publisher 27 | 28 | import scala.concurrent.ExecutionContext 29 | 30 | object instances { 31 | 32 | implicit final class SinkCompanionOps(value: Sink.type) { 33 | def korolevStream[F[_]: Effect, T]: Sink[T, Stream[F, T]] = { 34 | val subscriber = new KorolevStreamSubscriber[F, T]() 35 | Sink 36 | .fromSubscriber(subscriber) 37 | .mapMaterializedValue(_ => subscriber) 38 | } 39 | } 40 | 41 | implicit final class StreamCompanionOps(value: Stream.type) { 42 | def fromPublisher[F[_]: Effect, T](publisher: Publisher[T]): Stream[F, T] = { 43 | val result = new KorolevStreamSubscriber[F, T]() 44 | publisher.subscribe(result) 45 | result 46 | } 47 | } 48 | 49 | implicit final class KorolevStreamsOps[F[_]: Effect, T](stream: Stream[F, T]) { 50 | 51 | /** 52 | * Converts korolev [[korolev.effect.Stream]] to [[Publisher]]. 53 | * 54 | * If `fanout` is `true`, the `Publisher` will support multiple `Subscriber`s and 55 | * the size of the `inputBuffer` configured for this operator becomes the maximum number of elements that 56 | * the fastest [[org.reactivestreams.Subscriber]] can be ahead of the slowest one before slowing 57 | * the processing down due to back pressure. 58 | * 59 | * If `fanout` is `false` then the `Publisher` will only support a single `Subscriber` and 60 | * reject any additional `Subscriber`s with [[korolev.akka.util.KorolevStreamPublisher.MultipleSubscribersProhibitedException]]. 61 | */ 62 | def asPublisher(fanout: Boolean = false)(implicit ec: ExecutionContext): Publisher[T] = 63 | new KorolevStreamPublisher(stream, fanout) 64 | 65 | def asAkkaSource(implicit ec: ExecutionContext): Source[T, NotUsed] = { 66 | val publisher = new KorolevStreamPublisher(stream, fanout = false) 67 | Source.fromPublisher(publisher) 68 | } 69 | } 70 | 71 | implicit final val akkaByteStringBytesLike: BytesLike[ByteString] = 72 | new AkkaByteStringBytesLike() 73 | } 74 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/Extension.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import Extension.Handlers 20 | import korolev.effect.Effect 21 | 22 | trait Extension[F[_], S, M] { 23 | 24 | /** 25 | * Invokes then new sessions created 26 | * @return 27 | */ 28 | def setup(access: Context.BaseAccess[F, S, M]): F[Handlers[F, S, M]] 29 | } 30 | 31 | object Extension { 32 | 33 | abstract class Handlers[F[_]: Effect, S, M] { 34 | 35 | /** 36 | * Invokes when state updated. 37 | * @return 38 | */ 39 | def onState(state: S): F[Unit] = Effect[F].unit 40 | 41 | /** 42 | * Invokes when message published. 43 | * @see Context.BaseAccess.publish 44 | */ 45 | def onMessage(message: M): F[Unit] = Effect[F].unit 46 | 47 | /** 48 | * Invokes when user closes tab. 49 | * @return 50 | */ 51 | def onDestroy(): F[Unit] = Effect[F].unit 52 | } 53 | 54 | private final class HandlersImpl[F[_]: Effect, S, M](_onState: S => F[Unit], 55 | _onMessage: M => F[Unit], 56 | _onDestroy: () => F[Unit]) extends Handlers[F, S, M] { 57 | override def onState(state: S): F[Unit] = _onState(state) 58 | override def onMessage(message: M): F[Unit] = _onMessage(message) 59 | override def onDestroy(): F[Unit] = _onDestroy() 60 | } 61 | 62 | object Handlers { 63 | 64 | /** 65 | * @param onState Invokes when state updated. 66 | * @param onMessage Invokes when message published. 67 | * @param onDestroy Invokes when user closes tab. 68 | */ 69 | def apply[F[_]: Effect, S, M](onState: S => F[Unit] = null, 70 | onMessage: M => F[Unit] = null, 71 | onDestroy: () => F[Unit] = null): Handlers[F, S, M] = 72 | new HandlersImpl( 73 | if (onState == null) _ => Effect[F].unit else onState, 74 | if (onMessage == null) _ => Effect[F].unit else onMessage, 75 | if (onDestroy == null) () => Effect[F].unit else onDestroy 76 | ) 77 | } 78 | 79 | def apply[F[_], S, M](f: Context.BaseAccess[F, S, M] => F[Handlers[F, S, M]]): Extension[F, S, M] = 80 | (access: Context.BaseAccess[F, S, M]) => f(access) 81 | 82 | def pure[F[_]: Effect, S, M](f: Context.BaseAccess[F, S, M] => Handlers[F, S, M]): Extension[F, S, M] = 83 | (access: Context.BaseAccess[F, S, M]) => Effect[F].pure(f(access)) 84 | } 85 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/KorolevServiceConfig.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.server 18 | 19 | import korolev.effect.{Effect, Reporter} 20 | import korolev.state.IdGenerator 21 | import korolev.{Context, Extension, Router} 22 | import levsha.Document 23 | import scala.concurrent.ExecutionContext 24 | import scala.concurrent.duration._ 25 | 26 | import korolev.web.{Path, PathAndQuery} 27 | 28 | case class KorolevServiceConfig[F[_], S, M]( 29 | stateLoader: StateLoader[F, S], 30 | stateStorage: korolev.state.StateStorage[F, S] = null, // By default it StateStorage.DefaultStateStorage 31 | http: PartialFunction[HttpRequest[F], F[HttpResponse[F]]] = PartialFunction.empty[HttpRequest[F], F[HttpResponse[F]]], 32 | router: Router[F, S] = Router.empty[F, S], 33 | rootPath: Path = PathAndQuery.Root, 34 | @deprecated("Use `document` instead of `render`. Do not use `render` and `document` together.", "0.16.0") render: S => Document.Node[Context.Binding[F, S, M]] = (_: S) => levsha.dsl.html.body(), 35 | @deprecated("Add head() tag to `document`. Do not use `head` and `document` together.", "0.16.0") head: S => Seq[Document.Node[Context.Binding[F, S, M]]] = (_: S) => Seq.empty, 36 | document: S => Document.Node[Context.Binding[F, S, M]] = null, // TODO (_: S) => levsha.dsl.html.Html(), 37 | connectionLostWidget: Document.Node[Context.Binding[F, S, M]] = 38 | KorolevServiceConfig.defaultConnectionLostWidget[Context.Binding[F, S, M]], 39 | maxFormDataEntrySize: Int = 1024 * 8, 40 | extensions: List[Extension[F, S, M]] = Nil, 41 | idGenerator: IdGenerator[F] = IdGenerator.default[F](), 42 | heartbeatInterval: FiniteDuration = 5.seconds, 43 | reporter: Reporter = Reporter.PrintReporter, 44 | recovery: PartialFunction[Throwable, S => S] = PartialFunction.empty[Throwable, S => S], 45 | sessionIdleTimeout: FiniteDuration = 60.seconds, 46 | delayedRender: FiniteDuration = 0.seconds, 47 | )(implicit val executionContext: ExecutionContext) 48 | 49 | object KorolevServiceConfig { 50 | 51 | def defaultConnectionLostWidget[MiscType]: Document.Node[MiscType] = { 52 | import levsha.dsl._ 53 | import html._ 54 | optimize { 55 | div( 56 | position @= "fixed", 57 | top @= "0", 58 | left @= "0", 59 | right @= "0", 60 | backgroundColor @= "lightyellow", 61 | borderBottom @= "1px solid black", 62 | padding @= "10px", 63 | "Connection lost. Waiting to resume." 64 | ) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/server/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import korolev.Context.Binding 20 | import korolev.data.Bytes 21 | import korolev.effect.{Effect, Stream} 22 | import korolev.server.internal.services._ 23 | import korolev.server.internal.{FormDataCodec, KorolevServiceImpl} 24 | import korolev.state.{DeviceId, StateDeserializer, StateSerializer} 25 | import korolev.web.Request.Head 26 | import korolev.web.{Request, Response} 27 | 28 | package object server { 29 | 30 | type HttpRequest[F[_]] = Request[Stream[F, Bytes]] 31 | type HttpResponse[F[_]] = Response[Stream[F, Bytes]] 32 | 33 | final case class WebSocketRequest[F[_]](httpRequest: Request[Stream[F, Bytes]], protocols: Seq[String]) 34 | final case class WebSocketResponse[F[_]](httpResponse: Response[Stream[F, Bytes]], selectedProtocol: String) 35 | 36 | type StateLoader[F[_], S] = (DeviceId, Head) => F[S] 37 | 38 | def korolevService[F[_]: Effect, S: StateSerializer: StateDeserializer, M]( 39 | config: KorolevServiceConfig[F, S, M]): KorolevService[F] = { 40 | 41 | // TODO remove this when render/node fields will be removed 42 | val actualConfig = 43 | if (config.document == null) { 44 | config.copy(document = { (state: S) => 45 | import levsha.dsl._ 46 | import html._ 47 | optimize[Binding[F, S, M]] { 48 | Html( 49 | head(config.head(state)), 50 | config.render(state) 51 | ) 52 | } 53 | })(config.executionContext) 54 | } else { 55 | config 56 | } 57 | 58 | import config.executionContext 59 | 60 | val commonService = new CommonService[F]() 61 | val filesService = new FilesService[F](commonService) 62 | val pageService = new PageService[F, S, M](actualConfig) 63 | val sessionsService = new SessionsService[F, S, M](actualConfig, pageService) 64 | val messagingService = new MessagingService[F](actualConfig.reporter, commonService, sessionsService) 65 | val formDataCodec = new FormDataCodec(actualConfig.maxFormDataEntrySize) 66 | val postService = new PostService[F](actualConfig.reporter, sessionsService, commonService, formDataCodec) 67 | val ssrService = new ServerSideRenderingService[F, S, M](sessionsService, pageService, actualConfig) 68 | 69 | new KorolevServiceImpl[F]( 70 | config.http, 71 | commonService, 72 | filesService, 73 | messagingService, 74 | postService, 75 | ssrService 76 | ) 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /interop/akka/src/test/scala/CountdownSpec.scala: -------------------------------------------------------------------------------- 1 | import korolev.akka.util.Countdown 2 | import korolev.effect.Effect.FutureEffect 3 | import korolev.effect.syntax._ 4 | import org.scalatest.freespec.AsyncFreeSpec 5 | 6 | import java.util.concurrent.atomic.AtomicReference 7 | import scala.concurrent.Future 8 | 9 | class CountdownSpec extends AsyncFreeSpec { 10 | 11 | private case class Promise() extends Exception 12 | 13 | class ThrowOnPromise(var switch: Boolean = false) extends FutureEffect { 14 | 15 | def toggle(): Future[Unit] = Future.successful { 16 | switch = !switch 17 | } 18 | 19 | override def promise[A](cb: (Either[Throwable, A] => Unit) => Unit): Future[A] = { 20 | var released = false 21 | val p = super.promise { (f: Either[Throwable, A] => Unit) => 22 | cb { x => 23 | released = true 24 | f(x) 25 | } 26 | } 27 | if (switch) { 28 | if (released) p 29 | else Future.failed(Promise()) 30 | } else { 31 | p 32 | } 33 | } 34 | } 35 | 36 | "decOrLock should lock when count is 0" in recoverToSucceededIf[Promise] { 37 | val countdown = new Countdown[Future]()(new ThrowOnPromise(switch = true)) 38 | for { 39 | _ <- countdown.add(3) 40 | _ <- countdown.decOrLock() 41 | _ <- countdown.decOrLock() 42 | _ <- countdown.decOrLock() 43 | _ <- countdown.decOrLock() // should lock 44 | } yield () 45 | } 46 | 47 | "decOrLock should not lock until count > 0" in { 48 | val countdown = new Countdown[Future]()(new ThrowOnPromise) 49 | for { 50 | _ <- countdown.add(3) 51 | _ <- countdown.decOrLock() 52 | _ <- countdown.decOrLock() 53 | _ <- countdown.decOrLock() 54 | } yield succeed 55 | } 56 | 57 | "add should release taken lock" in { 58 | val effectInstance = new ThrowOnPromise(switch = false) 59 | val countdown = new Countdown[Future]()(effectInstance) 60 | val result = new AtomicReference[String]("") 61 | for { 62 | _ <- countdown.add(3) 63 | _ <- countdown.decOrLock() 64 | _ <- countdown.decOrLock() 65 | _ <- countdown.decOrLock() 66 | fiber <- countdown 67 | .decOrLock() // should lock 68 | .map(_ => result.getAndUpdate(_ + " world")) 69 | .start 70 | _ = result.set("hello") 71 | _ <- countdown.add(3) 72 | _ <- fiber.join() 73 | } yield assert(result.get == "hello world") 74 | } 75 | 76 | "count the lock released by the add invocation" in recoverToSucceededIf[Promise] { 77 | val effectInstance = new ThrowOnPromise(switch = false) 78 | val countdown = new Countdown[Future]()(effectInstance) 79 | for { 80 | _ <- countdown.add(3) 81 | _ <- countdown.decOrLock() 82 | _ <- countdown.decOrLock() 83 | _ <- countdown.decOrLock() 84 | fiber <- countdown 85 | .decOrLock() // should lock 86 | .start 87 | _ <- countdown.add(3) 88 | _ <- fiber.join() 89 | _ <- effectInstance.toggle() 90 | _ <- countdown.decOrLock() 91 | _ <- countdown.decOrLock() 92 | _ <- countdown.decOrLock() // should lock 93 | } yield () 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /examples/extension/src/main/scala/ExtensionExample.scala: -------------------------------------------------------------------------------- 1 | import akka.stream.OverflowStrategy 2 | import akka.stream.scaladsl.{Sink, Source} 3 | import korolev._ 4 | import korolev.akka._ 5 | import korolev.server._ 6 | import korolev.state.javaSerialization._ 7 | 8 | import scala.concurrent.ExecutionContext.Implicits.global 9 | import scala.concurrent.Future 10 | 11 | object ExtensionExample extends SimpleAkkaHttpKorolevApp { 12 | 13 | private val ctx = Context[Future, List[String], String] 14 | 15 | import ctx._ 16 | 17 | private val (queue, queueSource) = Source 18 | .queue[String](10, OverflowStrategy.fail) 19 | .preMaterialize() 20 | 21 | private val topicListener = Extension.pure[Future, List[String], String] { access => 22 | val queueSink = queueSource.runWith(Sink.queue[String]) 23 | def aux(): Future[Unit] = queueSink.pull() flatMap { 24 | case Some(message) => access 25 | .transition(_ :+ message) 26 | .flatMap(_ => aux()) 27 | case None => 28 | Future.unit 29 | } 30 | aux() 31 | Extension.Handlers[Future, List[String], String]( 32 | onMessage = message => queue.offer(message).map(_ => ()), 33 | onDestroy = () => Future.successful(queueSink.cancel()) 34 | ) 35 | } 36 | 37 | private def onSubmit(access: Access) = { 38 | for { 39 | sessionId <- access.sessionId 40 | name <- access.valueOf(nameElement) 41 | text <- access.valueOf(textElement) 42 | userName = 43 | if (name.trim.isEmpty) s"Anonymous #${sessionId.hashCode().toHexString}" 44 | else name 45 | _ <- 46 | if (text.trim.isEmpty) Future.unit 47 | else access.publish(s"$userName: $text") 48 | _ <- access.property(textElement).set("value", "") 49 | } yield () 50 | } 51 | 52 | private val nameElement = elementId() 53 | private val textElement = elementId() 54 | 55 | private val config = KorolevServiceConfig[Future, List[String], String]( 56 | stateLoader = StateLoader.default(Nil), 57 | extensions = List(topicListener), 58 | document = { message => 59 | 60 | import levsha.dsl._ 61 | import html._ 62 | 63 | optimize { 64 | Html( 65 | body( 66 | div( 67 | backgroundColor @= "yellow", 68 | padding @= "10px", 69 | border @= "1px solid black", 70 | "This is a chat. Open this app in few browser tabs or on few different computers" 71 | ), 72 | div( 73 | marginTop @= "10px", 74 | padding @= "10px", 75 | height @= "250px", 76 | backgroundColor @= "#eeeeee", 77 | message map { x => 78 | div(x) 79 | } 80 | ), 81 | form( 82 | marginTop @= "10px", 83 | input(`type` := "text", placeholder := "Name", nameElement), 84 | input(`type` := "text", placeholder := "Message", textElement), 85 | button("Sent"), 86 | event("submit")(onSubmit) 87 | ) 88 | ) 89 | ) 90 | } 91 | } 92 | ) 93 | 94 | val service: AkkaHttpService = 95 | akkaHttpService(config) 96 | } 97 | -------------------------------------------------------------------------------- /modules/korolev/src/main/scala/korolev/internal/DevMode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.internal 18 | 19 | import java.io.{File, RandomAccessFile} 20 | import java.nio.ByteBuffer 21 | 22 | import levsha.impl.DiffRenderContext 23 | 24 | private[korolev] object DevMode { 25 | 26 | private val DevModeKey = "korolev.dev" 27 | private val DevModeDirectoryKey = "korolev.dev.directory" 28 | private val DevModeDefaultDirectory = "target/korolev/" 29 | 30 | class ForRenderContext(identifier: String) { 31 | 32 | lazy val file = new File(DevMode.renderStateDirectory, identifier) 33 | 34 | lazy val saved: Boolean = 35 | DevMode.isActive && file.exists 36 | 37 | def isActive = DevMode.isActive 38 | 39 | def loadRenderContext() = 40 | if (saved) { 41 | val nioFile = new RandomAccessFile(file, "r") 42 | val channel = nioFile.getChannel 43 | try { 44 | val buffer = ByteBuffer.allocate(channel.size.toInt) 45 | channel.read(buffer) 46 | buffer.position(0) 47 | Some(buffer) 48 | } finally { 49 | nioFile.close() 50 | channel.close() 51 | } 52 | } else { 53 | None 54 | } 55 | 56 | def saveRenderContext(renderContext: DiffRenderContext[_]): Unit = { 57 | val nioFile = new RandomAccessFile(file, "rw") 58 | val channel = nioFile.getChannel 59 | try { 60 | val buffer = renderContext.save() 61 | channel.write(buffer) 62 | () 63 | } finally { 64 | nioFile.close() 65 | channel.close() 66 | } 67 | } 68 | } 69 | 70 | val isActive = sys.env.get(DevModeKey) 71 | .orElse(sys.props.get(DevModeKey)) 72 | .fold(false)(_ == "true") 73 | 74 | lazy val workDirectory = { 75 | val directoryPath = sys.env.get(DevModeDirectoryKey) 76 | .orElse(sys.props.get(DevModeDirectoryKey)) 77 | .getOrElse(DevModeDefaultDirectory) 78 | 79 | val file = new File(directoryPath) 80 | if (!file.exists()) { 81 | file.mkdirs() 82 | } else if (!file.isDirectory) { 83 | throw new ExceptionInInitializerError(s"$directoryPath should be directory") 84 | } 85 | file 86 | } 87 | 88 | lazy val sessionsDirectory = { 89 | val file = new File(workDirectory, "sessions") 90 | file.mkdir() 91 | file 92 | } 93 | 94 | lazy val renderStateDirectory = { 95 | val file = new File(workDirectory, "render-contexts") 96 | file.mkdir() 97 | file 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /examples/file-streaming/src/main/scala/OneByOneFileStreamingExample.scala: -------------------------------------------------------------------------------- 1 | import korolev.Context 2 | import korolev.Context.FileHandler 3 | import korolev.akka._ 4 | import korolev.effect.io.FileIO 5 | import korolev.monix._ 6 | import korolev.server.{KorolevServiceConfig, StateLoader} 7 | import korolev.state.javaSerialization._ 8 | import monix.eval.Task 9 | import monix.execution.Scheduler.Implicits.global 10 | 11 | import java.nio.file.Paths 12 | import scala.concurrent.duration.DurationInt 13 | 14 | object OneByOneFileStreamingExample extends SimpleAkkaHttpKorolevApp { 15 | 16 | case class State(aliveIndicator: Boolean, progress: Map[String, (Long, Long)], inProgress: Boolean) 17 | 18 | val globalContext = Context[Task, State, Any] 19 | 20 | import globalContext._ 21 | import levsha.dsl._ 22 | import html._ 23 | 24 | val fileInput = elementId() 25 | 26 | def onUploadClick(access: Access): Task[Unit] = { 27 | 28 | def showProgress(fileName: String, loaded: Long, total: Long): Task[Unit] = access 29 | .transition { state => 30 | val updated = state.progress + ((fileName, (loaded, total))) 31 | state.copy(progress = updated) 32 | } 33 | 34 | for { 35 | files <- access.listFiles(fileInput) 36 | _ <- access.transition(_.copy(progress = files.map { case FileHandler(fileName, size) => (fileName, (0L, size))}.toMap, inProgress = true)) 37 | _ <- Task.sequence { 38 | files.map { (handler: FileHandler) => 39 | val size = handler.size 40 | access.downloadFileAsStream(handler).flatMap { data => 41 | // File will be saved in 'downloads' directory 42 | // in the root of the example project 43 | val path = Paths.get(handler.fileName) 44 | data 45 | .over(0L) { 46 | case (acc, chunk) => 47 | val loaded = chunk.fold(acc)(_.length + acc) 48 | showProgress(handler.fileName, loaded, size) 49 | .map(_ => loaded) 50 | } 51 | .to(FileIO.write(path)) 52 | } 53 | } 54 | } 55 | _ <- access.transition(_.copy(inProgress = false)) 56 | } yield () 57 | } 58 | 59 | val service = akkaHttpService { 60 | KorolevServiceConfig[Task, State, Any] ( 61 | stateLoader = StateLoader.default(State(aliveIndicator = false, Map.empty, inProgress = false)), 62 | document = { 63 | case State(aliveIndicator, progress, inProgress) => optimize { 64 | Html( 65 | body( 66 | delay(1.second) { access => access.transition(_.copy(aliveIndicator = !aliveIndicator)) }, 67 | div(when(aliveIndicator)(backgroundColor @= "red"), "Online"), 68 | input(`type` := "file", multiple, fileInput), 69 | ul( 70 | progress.map { 71 | case (name, (loaded, total)) => 72 | li(s"$name: $loaded / $total") 73 | } 74 | ), 75 | button( 76 | "Upload", 77 | when(inProgress)(disabled), 78 | event("click")(onUploadClick) 79 | ) 80 | ) 81 | ) 82 | } 83 | } 84 | ) 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /examples/routing/src/main/scala/PathAndQueryRoutingExample.scala: -------------------------------------------------------------------------------- 1 | import korolev._ 2 | import korolev.akka._ 3 | 4 | import scala.concurrent.ExecutionContext.Implicits.global 5 | import korolev.server._ 6 | import korolev.state.javaSerialization._ 7 | import korolev.web.PathAndQuery.OptionQueryParam 8 | import korolev.web.PathAndQuery.*& 9 | 10 | import scala.concurrent.Future 11 | 12 | object PathAndQueryRoutingExample extends SimpleAkkaHttpKorolevApp { 13 | object BeginOptionQueryParam extends OptionQueryParam("begin") 14 | object EndOptionQueryParam extends OptionQueryParam("end") 15 | 16 | case class State(begin: Option[String] = None, end: Option[String] = None) 17 | 18 | object State { 19 | val globalContext = Context[Future, State, Any] 20 | } 21 | 22 | import State.globalContext._ 23 | 24 | import levsha.dsl._ 25 | import html._ 26 | 27 | val beginElementId = elementId() 28 | val endElementId = elementId() 29 | 30 | val service = akkaHttpService { 31 | KorolevServiceConfig[Future, State, Any]( 32 | stateLoader = StateLoader.default(State()), 33 | document = state => 34 | optimize { 35 | Html( 36 | head( 37 | title(s"Search form example") 38 | ), 39 | body( 40 | div("Enter search parameters and look to URI"), 41 | p(), 42 | div( 43 | form( 44 | input( 45 | beginElementId, 46 | `type` := "text", 47 | placeholder := "Enter begin", 48 | state.begin.map(begin => value := begin) 49 | ), 50 | input( 51 | endElementId, 52 | `type` := "text", 53 | placeholder := "Enter end", 54 | state.end.map(end => value := end) 55 | ), 56 | button( 57 | "Search!", 58 | event("click"){access => 59 | for { 60 | begin <- access.valueOf(beginElementId) 61 | end <- access.valueOf(endElementId) 62 | _ <- access.transition { s => 63 | s.copy(begin = trimToEmpty(begin), end = trimToEmpty(end)) 64 | } 65 | } yield () 66 | } 67 | ) 68 | ) 69 | ) 70 | ) 71 | ) 72 | }, 73 | router = Router( 74 | fromState = { 75 | case State(begin, end) => 76 | (Root / "search").withParam("begin", begin).withParam("end", end) 77 | }, 78 | toState = { 79 | case Root => 80 | initialState => 81 | Future.successful(initialState) 82 | case Root / "search" :?* BeginOptionQueryParam(begin) *& EndOptionQueryParam(end) => _ => 83 | val result = State(begin, end) 84 | Future.successful(result) 85 | } 86 | ) 87 | ) 88 | } 89 | 90 | private def trimToEmpty(value: String): Option[String] = { 91 | if (value.trim.isEmpty) { 92 | None 93 | } else { 94 | Some(value) 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /modules/effect/src/main/scala/korolev/effect/Reporter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.effect 18 | 19 | /** 20 | * Korolev INTERNAL reporting subsystem. 21 | * Do not use it in application code. 22 | */ 23 | trait Reporter { 24 | implicit val Implicit: Reporter = this 25 | def error(message: String, cause: Throwable): Unit 26 | def error(message: String): Unit 27 | def warning(message: String, cause: Throwable): Unit 28 | def warning(message: String): Unit 29 | def info(message: String): Unit 30 | def debug(message: String): Unit 31 | def debug(message: String, arg1: Any): Unit 32 | def debug(message: String, arg1: Any, arg2: Any): Unit 33 | def debug(message: String, arg1: Any, arg2: Any, arg3: Any): Unit 34 | // def debug(message: String, args: Any*): Unit 35 | } 36 | 37 | object Reporter { 38 | 39 | object Level extends Enumeration { 40 | final val Debug = Value(0, "Debug") 41 | final val Info = Value(1, "Info") 42 | final val Warning = Value(2, "Warning") 43 | final val Error = Value(3, "Error") 44 | } 45 | 46 | final object PrintReporter extends Reporter { 47 | 48 | var level: Level.Value = Level.Debug 49 | 50 | def error(message: String, error: Throwable): Unit = if (level <= Level.Error) { 51 | println(s"[ERROR] $message") 52 | error.printStackTrace(System.out) 53 | } 54 | def error(message: String): Unit = if (level <= Level.Error) { 55 | println(s"[ERROR] $message") 56 | } 57 | def warning(message: String, error: Throwable): Unit = if (level <= Level.Warning) { 58 | println(s"[WARNING] $message") 59 | error.printStackTrace(System.out) 60 | } 61 | def warning(message: String): Unit = if (level <= Level.Warning) { 62 | println(s"[WARNING] $message") 63 | } 64 | def info(message: String): Unit = if (level <= Level.Info) { 65 | println(s"[INFO] $message") 66 | } 67 | def debug(message: String): Unit = if (level <= Level.Debug) { 68 | println(s"[DEBUG] $message") 69 | } 70 | def debug(message: String, arg1: Any): Unit = if (level <= Level.Debug) { 71 | println(s"[DEBUG] ${message.format(arg1)}") 72 | } 73 | def debug(message: String, arg1: Any, arg2: Any): Unit = if (level <= Level.Debug) { 74 | println(s"[DEBUG] ${message.format(arg1, arg2)}") 75 | } 76 | def debug(message: String, arg1: Any, arg2: Any, arg3: Any): Unit = if (level <= Level.Debug) { 77 | println(s"[DEBUG] ${message.format(arg1, arg2, arg3)}") 78 | } 79 | // def debug(message: String, args: Any*): Unit = if (level <= Level.Debug) { 80 | // println(s"[DEBUG] ${String.format(message, args:_*)}") 81 | // } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /interop/akka/src/main/scala/korolev/akka/util/KorolevStreamPublisher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev.akka.util 18 | 19 | import korolev.akka.util.KorolevStreamPublisher.MultipleSubscribersProhibitedException 20 | import korolev.effect.syntax._ 21 | import korolev.effect.{Effect, Hub, Reporter, Stream} 22 | import org.reactivestreams.{Publisher, Subscriber, Subscription} 23 | 24 | import java.util.concurrent.atomic.AtomicReference 25 | import scala.annotation.tailrec 26 | import scala.concurrent.ExecutionContext 27 | 28 | final class KorolevStreamPublisher[F[_] : Effect, T](stream: Stream[F, T], 29 | fanout: Boolean) 30 | (implicit ec: ExecutionContext) extends Publisher[T] { 31 | 32 | private implicit val reporter: Reporter = Reporter.PrintReporter 33 | 34 | private var hasActualSubscriber = false 35 | 36 | private val hub = 37 | if (fanout) Hub(stream) 38 | else null 39 | 40 | private final class StreamSubscription(stream: Stream[F, T], 41 | subscriber: Subscriber[_ >: T]) extends Subscription { 42 | 43 | private val countdown = new Countdown[F]() 44 | 45 | private def loop(): F[Unit] = 46 | for { 47 | _ <- countdown.decOrLock() 48 | maybeItem <- stream.pull() 49 | _ <- maybeItem match { 50 | case Some(item) => 51 | subscriber.onNext(item) 52 | loop() 53 | case None => 54 | subscriber.onComplete() 55 | Effect[F].unit 56 | } 57 | } yield () 58 | 59 | loop().runAsync { 60 | case Left(error) => subscriber.onError(error) 61 | case Right(_) => () 62 | } 63 | 64 | def request(n: Long): Unit = { 65 | countdown.unsafeAdd(n) 66 | } 67 | 68 | def cancel(): Unit = { 69 | stream 70 | .cancel() 71 | .runAsyncForget 72 | } 73 | } 74 | 75 | def subscribe(subscriber: Subscriber[_ >: T]): Unit = { 76 | if (hub != null) { 77 | hub.newStream().runAsyncSuccess { newStream => 78 | val subscription = new StreamSubscription(newStream, subscriber) 79 | subscriber.onSubscribe(subscription) 80 | } 81 | } else { 82 | if (hasActualSubscriber) 83 | throw MultipleSubscribersProhibitedException() 84 | subscriber.onSubscribe(new StreamSubscription(stream, subscriber)) 85 | } 86 | hasActualSubscriber = true 87 | } 88 | } 89 | 90 | object KorolevStreamPublisher { 91 | final case class MultipleSubscribersProhibitedException() 92 | extends Exception("Multiple subscribers prohibited for this KorolevStreamPublisher") 93 | } -------------------------------------------------------------------------------- /interop/ce2/src/main/scala/korolev/cats/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017-2020 Aleksey Fomkin 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package korolev 18 | 19 | import _root_.cats.Traverse 20 | import _root_.cats.effect._ 21 | import _root_.cats.instances.list._ 22 | import korolev.effect.{Effect => KEffect} 23 | 24 | import scala.collection.concurrent.TrieMap 25 | import scala.concurrent.{ExecutionContext, Future, blocking => futureBlocking} 26 | import scala.util.Try 27 | 28 | package object cats { 29 | 30 | implicit object IOEffect extends KEffect[IO] { 31 | 32 | private val cs = TrieMap.empty[ExecutionContext, ContextShift[IO]] 33 | 34 | def pure[A](value: A): IO[A] = 35 | IO.pure(value) 36 | 37 | def delay[A](value: => A): IO[A] = 38 | IO.delay(value) 39 | 40 | def fail[A](e: Throwable): IO[A] = IO.raiseError(e) 41 | 42 | def fork[A](m: => IO[A])(implicit ec: ExecutionContext): IO[A] = 43 | cs.getOrElseUpdate(ec, IO.contextShift(ec)).shift *> m 44 | 45 | def blocking[A](f: => A)(implicit ec: ExecutionContext): IO[A] = 46 | IO.fromFuture(IO.pure(Future(futureBlocking(f))))(IO.contextShift(ec)) 47 | 48 | def unit: IO[Unit] = 49 | IO.unit 50 | 51 | def never[T]: IO[T] = 52 | IO.never 53 | 54 | def fromTry[A](value: => Try[A]): IO[A] = 55 | IO.fromTry(value) 56 | 57 | def start[A](m: => IO[A])(implicit ec: ExecutionContext): IO[KEffect.Fiber[IO, A]] = 58 | m.start(cs.getOrElseUpdate(ec, IO.contextShift(ec))) 59 | .map { fiber => 60 | new KEffect.Fiber[IO, A] { 61 | def join(): IO[A] = fiber.join 62 | } 63 | } 64 | 65 | def promise[A](cb: (Either[Throwable, A] => Unit) => Unit): IO[A] = 66 | IO.async(cb) 67 | 68 | def promiseF[A](cb: (Either[Throwable, A] => Unit) => IO[Unit]): IO[A] = 69 | IO.asyncF(cb) 70 | 71 | def flatMap[A, B](m: IO[A])(f: A => IO[B]): IO[B] = 72 | m.flatMap(f) 73 | 74 | def map[A, B](m: IO[A])(f: A => B): IO[B] = 75 | m.map(f) 76 | 77 | def recover[A, AA >: A](m: IO[A])(f: PartialFunction[Throwable, AA]): IO[AA] = 78 | m.handleErrorWith(e => f.andThen(IO.pure[AA] _).applyOrElse(e, IO.raiseError[AA] _)) 79 | 80 | def recoverF[A, AA >: A](m: IO[A])(f: PartialFunction[Throwable, IO[AA]]): IO[AA] = 81 | m.handleErrorWith(e => f.applyOrElse(e, IO.raiseError[AA] _)) 82 | 83 | def sequence[A](in: List[IO[A]]): IO[List[A]] = 84 | Traverse[List].sequence(in) 85 | 86 | def runAsync[A](m: IO[A])(callback: Either[Throwable, A] => Unit): Unit = { 87 | m.unsafeRunAsync(callback) 88 | } 89 | 90 | def run[A](m: IO[A]): Either[Throwable, A] = 91 | Try(m.unsafeRunSync()).toEither 92 | 93 | def toFuture[A](m: IO[A]): Future[A] = 94 | m.unsafeToFuture() 95 | } 96 | } 97 | --------------------------------------------------------------------------------