├── .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 | [](https://travis-ci.org/fomkin/korolev)
6 | [](https://app.fossa.io/projects/git%2Bgithub.com%2Ffomkin%2Fkorolev?ref=badge_shield)
7 | [](https://gitter.im/fomkin/korolev?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
8 | [-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]: <> ([](https://saucelabs.com/u/yelbota))
45 |
46 | ## License
47 |
48 | [](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 |
--------------------------------------------------------------------------------