├── project ├── build.properties └── plugins.sbt ├── lagomjs-persistence-scaladsl └── js │ └── src │ └── main │ └── scala │ └── com │ └── lightbend │ └── lagom │ ├── internal │ └── .gitkeep │ └── scaladsl │ └── persistence │ ├── ReadSide.scala │ ├── testkit │ └── .gitkeep │ ├── AkkaTaggerAdapter.scala │ ├── CommandEnvelope.scala │ ├── EventStreamElement.scala │ ├── PersistentEntity.scala │ ├── PersistentEntityRef.scala │ ├── ReadSideProcessor.scala │ ├── PersistenceComponents.scala │ └── PersistentEntityRegistry.scala ├── lagomjs-client └── js │ └── src │ ├── main │ ├── scala │ │ └── com │ │ │ └── lightbend │ │ │ └── lagom │ │ │ └── internal │ │ │ ├── NettyFutureConverters.scala │ │ │ └── client │ │ │ ├── ConfigExtensions.scala │ │ │ ├── CircuitBreakerMetricsProviderImpl.scala │ │ │ ├── CircuitBreakersPanelInternal.scala │ │ │ ├── WebSocketClient.scala │ │ │ ├── ClientServiceCallInvoker.scala │ │ │ └── WebSocketStreamBuffer.scala │ └── resources │ │ └── reference.conf │ └── test │ └── scala │ └── com │ └── lightbend │ └── lagom │ └── internal │ └── client │ └── CircuitBreakersPanelInternalSpec.scala ├── lagomjs-api └── js │ └── src │ └── main │ ├── java │ └── javax │ │ └── inject │ │ ├── Inject.java │ │ └── Singleton.java │ └── scala │ ├── java │ ├── io │ │ ├── File.scala │ │ └── package.scala │ └── security │ │ └── Principal.scala │ ├── com │ └── lightbend │ │ └── lagom │ │ └── internal │ │ └── api │ │ └── HeaderUtils.scala │ └── play │ ├── core │ ├── Execution.scala │ └── utils │ │ └── AsciiSet.scala │ ├── api │ ├── Environment.scala │ ├── Mode.scala │ ├── Configuration.scala │ ├── mvc │ │ └── Results.scala │ ├── libs │ │ └── streams │ │ │ └── AkkaStreams.scala │ └── http │ │ └── StandardValues.scala │ └── utils │ └── UriEncoding.scala ├── lagomjs-client-scaladsl └── js │ └── src │ └── main │ └── scala │ ├── lagomjs │ └── Config.scala │ └── com │ └── lightbend │ └── lagom │ ├── internal │ └── scaladsl │ │ └── client │ │ ├── ScaladslWebSocketClient.scala │ │ └── ScaladslServiceClientInvoker.scala │ └── scaladsl │ └── client │ ├── ServiceLocators.scala │ └── ServiceClient.scala ├── lagomjs-spi └── src │ └── main │ └── scala │ └── com │ └── lightbend │ └── lagom │ └── internal │ └── spi │ ├── ServiceDescription.scala │ ├── CircuitBreakerMetricsProvider.scala │ ├── ServiceDiscovery.scala │ ├── ServiceAcl.scala │ └── CircuitBreakerMetrics.scala ├── .gitignore ├── .scalafmt.conf ├── README.md ├── LICENSE └── lagomjs-api-scaladsl └── js └── src └── main └── scala └── com └── lightbend └── lagom └── scaladsl └── api └── Service.scala /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.13 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/internal/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/NettyFutureConverters.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/ReadSide.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/testkit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/java/javax/inject/Inject.java: -------------------------------------------------------------------------------- 1 | package javax.inject; 2 | 3 | public @interface Inject {} 4 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/AkkaTaggerAdapter.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/CommandEnvelope.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/EventStreamElement.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/PersistentEntity.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/PersistentEntityRef.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/ReadSideProcessor.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/java/javax/inject/Singleton.java: -------------------------------------------------------------------------------- 1 | package javax.inject; 2 | 3 | public @interface Singleton {} 4 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/PersistenceComponents.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-persistence-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/persistence/PersistentEntityRegistry.scala: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/java/io/File.scala: -------------------------------------------------------------------------------- 1 | package java.io 2 | 3 | /* 4 | * Skeleton of java.io.File removing all functionality for JS compatibility. 5 | */ 6 | class File(path: String) 7 | -------------------------------------------------------------------------------- /lagomjs-client-scaladsl/js/src/main/scala/lagomjs/Config.scala: -------------------------------------------------------------------------------- 1 | package lagomjs 2 | 3 | import com.typesafe.config.ConfigFactory 4 | 5 | object Config { 6 | val default: com.typesafe.config.Config = ConfigFactory.load() 7 | } 8 | -------------------------------------------------------------------------------- /lagomjs-spi/src/main/scala/com/lightbend/lagom/internal/spi/ServiceDescription.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.spi 2 | 3 | trait ServiceDescription { 4 | def name(): String 5 | def acls(): java.util.List[ServiceAcl] 6 | } 7 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/java/io/package.scala: -------------------------------------------------------------------------------- 1 | package java 2 | 3 | /* 4 | * Use java.io.StringWriter as java.io.CharArrayWriter for JS compatibility. 5 | */ 6 | package object io { 7 | type CharArrayWriter = java.io.StringWriter 8 | } 9 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/java/security/Principal.scala: -------------------------------------------------------------------------------- 1 | package java.security 2 | 3 | /* 4 | * Skeleton of java.security.Principal removing all functionality for JS compatibility. 5 | */ 6 | trait Principal { 7 | def getName: String 8 | } 9 | -------------------------------------------------------------------------------- /lagomjs-spi/src/main/scala/com/lightbend/lagom/internal/spi/CircuitBreakerMetricsProvider.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.spi 2 | 3 | trait CircuitBreakerMetricsProvider { 4 | def start(breakerId: String): CircuitBreakerMetrics 5 | } 6 | -------------------------------------------------------------------------------- /lagomjs-spi/src/main/scala/com/lightbend/lagom/internal/spi/ServiceDiscovery.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.spi 2 | 3 | trait ServiceDiscovery { 4 | def discoverServices(classLoader: ClassLoader): java.util.List[ServiceDescription] 5 | } 6 | -------------------------------------------------------------------------------- /lagomjs-spi/src/main/scala/com/lightbend/lagom/internal/spi/ServiceAcl.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.spi 2 | 3 | import java.util.Optional 4 | 5 | trait ServiceAcl { 6 | def method(): Optional[String] 7 | def pathPattern(): Optional[String] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | target/ 5 | 6 | lagomjs-api/shared/ 7 | lagomjs-api-scaladsl/shared/ 8 | lagomjs-client/shared/ 9 | lagomjs-client/js/src/main/resources/ 10 | lagomjs-client-scaladsl/shared/ 11 | lagomjs-macro-testkit/shared/ 12 | lagomjs-persistence-scaladsl/shared/ 13 | 14 | node_modules/ 15 | package-lock.json 16 | 17 | .idea 18 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/com/lightbend/lagom/internal/api/HeaderUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.api 6 | 7 | object HeaderUtils { 8 | 9 | /** 10 | * Normalize an HTTP header name. 11 | * 12 | * @param name the header name 13 | * @return the normalized header name 14 | */ 15 | @inline 16 | def normalize(name: String): String = name.toLowerCase 17 | } 18 | -------------------------------------------------------------------------------- /lagomjs-spi/src/main/scala/com/lightbend/lagom/internal/spi/CircuitBreakerMetrics.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.spi 2 | 3 | trait CircuitBreakerMetrics { 4 | def onOpen(): Unit 5 | def onClose(): Unit 6 | def onHalfOpen(): Unit 7 | def onCallSuccess(elapsedNanos: Long): Unit 8 | def onCallFailure(elapsedNanos: Long): Unit 9 | def onCallTimeoutFailure(elapsedNanos: Long): Unit 10 | def onCallBreakerOpenFailure(): Unit 11 | def stop(): Unit 12 | } 13 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | align = true 2 | assumeStandardLibraryStripMargin = true 3 | danglingParentheses = true 4 | docstrings = JavaDoc 5 | maxColumn = 120 6 | project.git = true 7 | rewrite.rules = [ AvoidInfix, ExpandImportSelectors, RedundantParens, SortModifiers, PreferCurlyFors ] 8 | rewrite.sortModifiers.order = [ "private", "protected", "final", "sealed", "abstract", "implicit", "override", "lazy" ] 9 | spaces.inImportCurlyBraces = true 10 | trailingCommas = preserve 11 | version = 2.3.2 12 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/core/Execution.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package play.core 6 | 7 | /* 8 | * Implementation of play.core.Execution.trampoline using the scalajs queue for JS compatibility. 9 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/core/Execution.scala 10 | */ 11 | 12 | private[play] object Execution { 13 | def trampoline = scala.scalajs.concurrent.JSExecutionContext.queue 14 | } 15 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.0.0") 2 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.2.0") 3 | 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") 5 | addSbtPlugin("org.akka-js" % "sbt-shocon" % "1.0.0") 6 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.3.4") 7 | 8 | libraryDependencies ++= Seq( 9 | "org.eclipse.jgit" % "org.eclipse.jgit.pgm" % "3.7.1.201504261725-r", 10 | "org.scala-js" %% "scalajs-env-jsdom-nodejs" % "1.1.0" 11 | ) 12 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/Environment.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package play.api 6 | 7 | import java.io.File 8 | 9 | /* 10 | * Skeleton of play.api.Environment removing all functionality for JS compatibility. 11 | */ 12 | case class Environment(rootPath: File, classLoader: ClassLoader, mode: Mode) 13 | 14 | object Environment { 15 | 16 | def simple(path: File = new File("."), mode: Mode = Mode.Test) = 17 | Environment(path, new ClassLoader() {}, mode) 18 | 19 | } 20 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/Mode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package play.api 6 | 7 | /* 8 | * Copy of play.api.Mode removing the Java interoperability for JS compatibility. 9 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/api/Mode.scala 10 | */ 11 | sealed abstract class Mode 12 | 13 | object Mode { 14 | 15 | case object Dev extends Mode 16 | case object Test extends Mode 17 | case object Prod extends Mode 18 | 19 | lazy val values: Set[Mode] = Set(Dev, Test, Prod) 20 | } 21 | -------------------------------------------------------------------------------- /lagomjs-client-scaladsl/js/src/main/scala/com/lightbend/lagom/internal/scaladsl/client/ScaladslWebSocketClient.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.scaladsl.client 2 | 3 | import akka.stream.Materializer 4 | import com.lightbend.lagom.internal.client.WebSocketClient 5 | import com.lightbend.lagom.internal.client.WebSocketClientConfig 6 | 7 | import scala.concurrent.ExecutionContext 8 | 9 | private[lagom] class ScaladslWebSocketClient(config: WebSocketClientConfig)( 10 | implicit ec: ExecutionContext, 11 | materializer: Materializer 12 | ) extends WebSocketClient(config)(ec, materializer) 13 | with ScaladslServiceApiBridge 14 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/Configuration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package play.api 6 | 7 | import com.typesafe.config.Config 8 | import com.typesafe.config.ConfigFactory 9 | 10 | import scala.collection.JavaConverters._ 11 | 12 | /* 13 | * Skeleton of play.api.Configuration removing all functionality for JS compatibility. 14 | */ 15 | case class Configuration(underlying: Config) 16 | 17 | object Configuration { 18 | def load(directSettings: Map[String, Any], defaultConfig: Config): Configuration = { 19 | // Prevent one level of config nesting as a temporary partial work around to: 20 | // https://github.com/akka-js/shocon/issues/55 21 | val combinedConfig = if (directSettings.nonEmpty) { 22 | val directConfig = ConfigFactory.parseMap(directSettings.asJava) 23 | directConfig.withFallback(defaultConfig) 24 | } else { 25 | defaultConfig 26 | } 27 | val resolvedConfig = ConfigFactory.load(combinedConfig) 28 | 29 | Configuration(resolvedConfig) 30 | } 31 | 32 | def empty = Configuration(ConfigFactory.empty()) 33 | } 34 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/mvc/Results.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | /* 6 | * Copy of play.api.mvc.Codec for JS compatibility. 7 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/api/mvc/Results.scala 8 | */ 9 | 10 | package play.api.mvc 11 | 12 | import akka.util.ByteString 13 | 14 | /** 15 | * A Codec handle the conversion of String to Byte arrays. 16 | * 17 | * @param charset The charset to be sent to the client. 18 | * @param encode The transformation function. 19 | */ 20 | case class Codec(charset: String)(val encode: String => ByteString, val decode: ByteString => String) 21 | 22 | /** 23 | * Default Codec support. 24 | */ 25 | object Codec { 26 | 27 | /** 28 | * Create a Codec from an encoding already supported by the JVM. 29 | */ 30 | def javaSupported(charset: String) = 31 | Codec(charset)(str => ByteString.apply(str, charset), bytes => bytes.decodeString(charset)) 32 | 33 | /** 34 | * Codec for UTF-8 35 | */ 36 | implicit val utf_8 = javaSupported("utf-8") 37 | 38 | /** 39 | * Codec for ISO-8859-1 40 | */ 41 | val iso_8859_1 = javaSupported("iso-8859-1") 42 | 43 | } 44 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/ConfigExtensions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.client 6 | 7 | import java.util 8 | 9 | import com.typesafe.config.Config 10 | import com.typesafe.config.ConfigException 11 | 12 | object ConfigExtensions { 13 | 14 | /** 15 | * INTERNAL API 16 | * 17 | * Utility method to support automatic wrapping of a String value in a [[java.util.List[String]]] 18 | * 19 | * This method will return a [[java.util.List[String]]] if the passed key is a [[java.util.List[String]]] or if it's [[String]], in which 20 | * case it returns a single element [[java.util.List[String]]]. 21 | * 22 | * @param config - a [[Config]] instance 23 | * @param key - the key to lookup 24 | * @throws ConfigException.WrongType in case value is neither a [[String]] nor a [[java.util.List[String]]] 25 | * @return a [[java.util.List[String]]] containing one or more values for the passed key if key it is found, empty list otherwise. 26 | */ 27 | def getStringList(config: Config, key: String): util.List[String] = { 28 | Option(config.getString(key)) 29 | .map(s => util.Arrays.asList(s)) 30 | .orElse(Option(config.getStringList(key))) 31 | .getOrElse(util.Arrays.asList()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/libs/streams/AkkaStreams.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package play.api.libs.streams 6 | 7 | import akka.Done 8 | import akka.stream.FlowShape 9 | import akka.stream.scaladsl.Broadcast 10 | import akka.stream.scaladsl.Flow 11 | import akka.stream.scaladsl.GraphDSL 12 | import akka.stream.scaladsl.Sink 13 | 14 | import scala.concurrent.Future 15 | 16 | /* 17 | * Copy of play.api.libs.AkkaStreams.ignoreAfterCancellation for JS compatibility. 18 | * https://github.com/playframework/playframework/blob/master/core/play-streams/src/main/scala/play/api/libs/streams/AkkaStreams.scala 19 | */ 20 | object AkkaStreams { 21 | 22 | /** 23 | * A flow that will ignore downstream cancellation, and instead will continue receiving and ignoring the stream. 24 | */ 25 | def ignoreAfterCancellation[T]: Flow[T, T, Future[Done]] = { 26 | Flow.fromGraph(GraphDSL.create(Sink.ignore) { implicit builder => ignore => 27 | import GraphDSL.Implicits._ 28 | // This pattern is an effective way to absorb cancellation, Sink.ignore will keep the broadcast always flowing 29 | // even after sink.inlet cancels. 30 | val broadcast = builder.add(Broadcast[T](2, eagerCancel = false)) 31 | broadcast.out(0) ~> ignore.in 32 | FlowShape(broadcast.in, broadcast.out(1)) 33 | }) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | #//#circuit-breaker-default 2 | # Circuit breakers for calls to other services are configured 3 | # in this section. A child configuration section with the same 4 | # name as the circuit breaker identifier will be used, with fallback 5 | # to the `lagom.circuit-breaker.default` section. 6 | lagom.circuit-breaker { 7 | 8 | # Default configuration that is used if a configuration section 9 | # with the circuit breaker identifier is not defined. 10 | default { 11 | # Possibility to disable a given circuit breaker. 12 | enabled = on 13 | 14 | # Number of failures before opening the circuit. 15 | max-failures = 10 16 | 17 | # Duration of time after which to consider a call a failure. 18 | call-timeout = 10s 19 | 20 | # Duration of time in open state after which to attempt to close 21 | # the circuit, by first entering the half-open state. 22 | reset-timeout = 15s 23 | 24 | # A whitelist of fqcn of Exceptions that the CircuitBreaker 25 | # should not consider failures. By default all exceptions are 26 | # considered failures. 27 | exception-whitelist = [] 28 | } 29 | } 30 | #//#circuit-breaker-default 31 | 32 | #//#web-socket-client-default 33 | # This configures the websocket clients used by this service. 34 | # This is a global configuration and it is currently not possible 35 | # to provide different configurations if multiple websocket services 36 | # are consumed. 37 | lagom.client.websocket { 38 | 39 | # Size of the internal WebSocket data receive buffer used to 40 | # compensate for the delay between WebSocket connection and stream 41 | # start and for the lack of WebSocket back-pressure. 42 | # Set to "unlimited" for an effectively ulimited buffer size. 43 | bufferSize = 16 44 | } 45 | #//#web-socket-client-default 46 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/CircuitBreakerMetricsProviderImpl.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.client 2 | 3 | import java.util.concurrent.CopyOnWriteArrayList 4 | import java.util.concurrent.atomic.AtomicLong 5 | import java.util.concurrent.atomic.AtomicReference 6 | 7 | import akka.actor.ActorSystem 8 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetrics 9 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetricsProvider 10 | 11 | class CircuitBreakerMetricsProviderImpl(val system: ActorSystem) extends CircuitBreakerMetricsProvider { 12 | private val metrics = new CopyOnWriteArrayList[CircuitBreakerMetricsImpl] 13 | 14 | override def start(breakerId: String): CircuitBreakerMetrics = { 15 | val m = new CircuitBreakerMetricsImpl(breakerId, this) 16 | metrics.add(m) 17 | m 18 | } 19 | 20 | private[lagom] def remove(m: CircuitBreakerMetricsImpl): Unit = 21 | metrics.remove(m) 22 | 23 | private[lagom] def allMetrics(): java.util.List[CircuitBreakerMetricsImpl] = 24 | metrics 25 | } 26 | 27 | object CircuitBreakerMetricsImpl { 28 | final val Closed = "closed" 29 | final val Open = "open" 30 | final val HalfOpen = "half-open" 31 | } 32 | 33 | class CircuitBreakerMetricsImpl(val breakerId: String, provider: CircuitBreakerMetricsProviderImpl) 34 | extends CircuitBreakerMetrics { 35 | import CircuitBreakerMetricsImpl._ 36 | 37 | private val log = org.scalajs.dom.console 38 | private val successValue = new AtomicLong(0L) 39 | private val failureValue = new AtomicLong(0L) 40 | private val stateValue = new AtomicReference[String](Closed) 41 | 42 | def successCount: Long = successValue.get() 43 | def failureCount: Long = failureValue.get() 44 | def state: String = stateValue.get() 45 | 46 | override def onOpen(): Unit = { 47 | stateValue.compareAndSet(Closed, Open) 48 | stateValue.compareAndSet(HalfOpen, Open) 49 | log.warn(s"Circuit breaker [${breakerId}] open") 50 | } 51 | 52 | override def onClose(): Unit = { 53 | stateValue.compareAndSet(Open, Closed) 54 | stateValue.compareAndSet(HalfOpen, Closed) 55 | log.info(s"Circuit breaker [${breakerId}] closed") 56 | } 57 | 58 | override def onHalfOpen(): Unit = { 59 | stateValue.compareAndSet(Open, HalfOpen) 60 | log.info(s"Circuit breaker [${breakerId}] half-open") 61 | } 62 | 63 | override def onCallSuccess(elapsedNanos: Long): Unit = { 64 | // updateThroughput() 65 | // updateLatency(elapsedNanos) 66 | updateSuccessCount() 67 | } 68 | 69 | override def onCallFailure(elapsedNanos: Long): Unit = { 70 | // updateThroughput() 71 | // updateFailureThroughput() 72 | // updateLatency(elapsedNanos) 73 | updateFailureCount() 74 | } 75 | 76 | override def onCallTimeoutFailure(elapsedNanos: Long): Unit = { 77 | // updateThroughput() 78 | // updateFailureThroughput() 79 | // updateLatency(elapsedNanos) 80 | updateFailureCount() 81 | } 82 | 83 | override def onCallBreakerOpenFailure(): Unit = { 84 | // updateThroughput() 85 | // updateFailureThroughput() 86 | updateFailureCount() 87 | } 88 | 89 | override def stop(): Unit = { 90 | provider.remove(this) 91 | } 92 | 93 | private def updateSuccessCount(): Unit = 94 | successValue.incrementAndGet() 95 | 96 | private def updateFailureCount(): Unit = 97 | failureValue.incrementAndGet() 98 | } 99 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/core/utils/AsciiSet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | /* 6 | * Implementation of play.core.utils.AsciiSet using scala.collection.immutable.BitSet for JS compatibility. 7 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/core/utils/AsciiSet.scala 8 | */ 9 | 10 | package play.core.utils 11 | 12 | import scala.collection.immutable.BitSet 13 | 14 | object AsciiSet { 15 | 16 | /** Create a set of a single character. */ 17 | def apply(c: Char): AsciiChar = new AsciiChar(c) 18 | 19 | /** Create a set of more than one character. */ 20 | def apply(c: Char, cs: Char*): AsciiSet = cs.foldLeft[AsciiSet](apply(c)) { 21 | case (acc, c1) => acc ||| apply(c1) 22 | } 23 | 24 | /** Some useful sets of ASCII characters. */ 25 | object Sets { 26 | // Core Rules (https://tools.ietf.org/html/rfc5234#appendix-B.1). 27 | // These are used in HTTP (https://tools.ietf.org/html/rfc7230#section-1.2). 28 | val Digit: AsciiSet = new AsciiRange('0', '9') 29 | val Lower: AsciiSet = new AsciiRange('a', 'z') 30 | val Upper: AsciiSet = new AsciiRange('A', 'Z') 31 | val Alpha: AsciiSet = Lower ||| Upper 32 | val AlphaDigit: AsciiSet = Alpha ||| Digit 33 | // https://en.wikipedia.org/wiki/ASCII#Printable_characters 34 | val VChar: AsciiSet = new AsciiRange(0x20, 0x7e) 35 | } 36 | } 37 | 38 | /** 39 | * A set of ASCII characters. The set should be built out of [[AsciiRange]], 40 | * [[AsciiChar]], [[AsciiUnion]], etc then converted to an [[AsciiBitSet]] 41 | * using `toBitSet` for fast querying. 42 | */ 43 | trait AsciiSet { 44 | 45 | /** 46 | * The internal method used to query for set membership. 47 | * Doesn't do any bounds checks. Also may be slow, so to 48 | * query from outside this package you should convert to an 49 | * [[AsciiBitSet]] using `toBitSet`. 50 | */ 51 | private[utils] def getInternal(i: Int): Boolean 52 | 53 | /** Join together two sets. */ 54 | def |||(that: AsciiSet): AsciiUnion = new AsciiUnion(this, that) 55 | 56 | /** Convert into an [[AsciiBitSet]] for fast querying. */ 57 | def toBitSet: AsciiBitSet = { 58 | val elems = (0 until 256).filter(this.getInternal) 59 | val bitSet = BitSet(elems: _*) 60 | new AsciiBitSet(bitSet) 61 | } 62 | } 63 | 64 | /** An inclusive range of ASCII characters */ 65 | private[play] final class AsciiRange(first: Int, last: Int) extends AsciiSet { 66 | assert(first >= 0 && first < last && last < 256) 67 | override def toString: String = s"(${Integer.toHexString(first)}- ${Integer.toHexString(last)})" 68 | private[utils] override def getInternal(i: Int): Boolean = i >= first && i <= last 69 | } 70 | private[play] object AsciiRange { 71 | 72 | /** Helper to construct an [[AsciiRange]]. */ 73 | def apply(first: Int, last: Int): AsciiRange = new AsciiRange(first, last) 74 | } 75 | 76 | /** A set with a single ASCII character in it. */ 77 | private[play] final class AsciiChar(i: Int) extends AsciiSet { 78 | assert(i >= 0 && i < 256) 79 | private[utils] override def getInternal(i: Int): Boolean = i == this.i 80 | } 81 | 82 | /** A union of two [[AsciiSet]]s. */ 83 | private[play] final class AsciiUnion(a: AsciiSet, b: AsciiSet) extends AsciiSet { 84 | require(a != null && b != null) 85 | private[utils] override def getInternal(i: Int): Boolean = a.getInternal(i) || b.getInternal(i) 86 | } 87 | 88 | /** 89 | * An efficient representation of a set of ASCII characters. Created by 90 | * building an [[AsciiSet]] then calling `toBitSet` on it. 91 | */ 92 | private[play] final class AsciiBitSet private[utils] (bitSet: BitSet) extends AsciiSet { 93 | final def get(i: Int): Boolean = { 94 | if (i < 0 || i > 255) 95 | throw new IllegalArgumentException(s"Character $i cannot match AsciiSet because it is out of range") 96 | getInternal(i) 97 | } 98 | private[utils] override def getInternal(i: Int): Boolean = bitSet(i) 99 | override def toBitSet: AsciiBitSet = this 100 | } 101 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/test/scala/com/lightbend/lagom/internal/client/CircuitBreakersPanelInternalSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.client 6 | 7 | import akka.actor.ActorSystem 8 | import akka.pattern.CircuitBreakerOpenException 9 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetricsProvider 10 | import com.typesafe.config.ConfigFactory 11 | import org.scalatest.AsyncFlatSpec 12 | import org.scalatest.BeforeAndAfterAll 13 | import org.scalatest.Matchers 14 | import org.scalatest.concurrent.Futures 15 | 16 | import scala.concurrent.Future 17 | 18 | class CircuitBreakersPanelInternalSpec extends AsyncFlatSpec with Matchers with BeforeAndAfterAll with Futures { 19 | val actorSystem = ActorSystem("CircuitBreakersPanelInternalSpec") 20 | 21 | override def afterAll() = { 22 | actorSystem.terminate() 23 | } 24 | 25 | behavior.of("CircuitBreakersPanelInternal") 26 | 27 | it should "keep the circuit closed on whitelisted exceptions" in { 28 | val fakeExceptionName = new FakeException("").getClass.getName 29 | val whitelist = Array(fakeExceptionName) 30 | 31 | // This CircuitBreakersPanelInternal has 'FakeException' whitelisted so when it's thrown on 32 | // the 2nd step it won't open the circuit. 33 | // NOTE the panel is configured to trip on a single exception (see config below) 34 | val panel: CircuitBreakersPanelInternal = panelWith(whitelist) 35 | 36 | val actual: Future[String] = for { 37 | _ <- successfulCall(panel, "123") 38 | _ <- failedCall(panel, new FakeException("boo")) 39 | x <- successfulCall(panel, "456") 40 | } yield x 41 | 42 | actual.map { result => 43 | result should be("456") 44 | } 45 | } 46 | 47 | it should "open the circuit when the exception is not whitelisted" in { 48 | val whitelist = Array.empty[String] 49 | 50 | // This CircuitBreakersPanelInternal has nothing whitelisted so when a FakeException 51 | // is thrown on the 2nd step it will open. That will cause the third call to fail 52 | // which is what we expect. 53 | // NOTE the panel is configured to trip on a single exception (see config below) 54 | val panel: CircuitBreakersPanelInternal = panelWith(whitelist) 55 | 56 | // Expect a CircuitBreakerOpenException 57 | recoverToSucceededIf[CircuitBreakerOpenException] { 58 | for { 59 | _ <- successfulCall(panel, "123") 60 | _ <- failedCall(panel, new FakeException("boo")) 61 | x <- successfulCall(panel, "456") 62 | } yield x 63 | } 64 | } 65 | 66 | // --------------------------------------------------------- 67 | 68 | private def successfulCall(panel: CircuitBreakersPanelInternal, mockedResponse: String) = { 69 | panel.withCircuitBreaker("cb")(Future.successful(mockedResponse)) 70 | } 71 | 72 | private def failedCall(panel: CircuitBreakersPanelInternal, failure: Exception) = { 73 | panel 74 | .withCircuitBreaker("cb")(Future.failed(failure)) 75 | .recover { 76 | case _ => 77 | Future.successful( 78 | "We expect a Failure but we must capture the exception thrown to move forward with the test." 79 | ) 80 | } 81 | } 82 | 83 | private def panelWith(whitelist: Array[String]) = { 84 | val config = configWithWhiteList(whitelist: _*) 85 | val cbConfig: CircuitBreakerConfig = new CircuitBreakerConfig(config) 86 | val metricsProvider: CircuitBreakerMetricsProvider = new CircuitBreakerMetricsProviderImpl(actorSystem) 87 | new CircuitBreakersPanelInternal(actorSystem, cbConfig, metricsProvider) 88 | } 89 | 90 | // This configuration is prepared for the tests so that it opens the Circuit Breaker 91 | // after a single failure. 92 | private def configWithWhiteList(whitelistedExceptions: String*) = ConfigFactory.parseString( 93 | s""" 94 | |lagom.circuit-breaker { 95 | | default { 96 | | 97 | | ## Set failures to '1' so a single exception trips the breaker. 98 | | max-failures = 1 99 | | 100 | | exception-whitelist = [${whitelistedExceptions.mkString(",")}] 101 | | 102 | | 103 | | enabled = on 104 | | call-timeout = 10s 105 | | reset-timeout = 15s 106 | | } 107 | |} 108 | |#//#circuit-breaker-default 109 | """.stripMargin 110 | ) 111 | } 112 | 113 | class FakeException(msg: String) extends RuntimeException(msg) 114 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/CircuitBreakersPanelInternal.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.client 6 | 7 | import java.util.concurrent.TimeUnit.MILLISECONDS 8 | import java.util.concurrent.TimeoutException 9 | 10 | import akka.actor.ActorSystem 11 | import akka.pattern.CircuitBreakerOpenException 12 | import akka.pattern.{ CircuitBreaker => AkkaCircuitBreaker } 13 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetrics 14 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetricsProvider 15 | import com.typesafe.config.Config 16 | import javax.inject.Inject 17 | import javax.inject.Singleton 18 | 19 | import scala.collection.mutable 20 | import scala.concurrent.Future 21 | import scala.concurrent.duration._ 22 | import scala.util.Failure 23 | import scala.util.Success 24 | import scala.util.Try 25 | 26 | /** 27 | * This is the internal CircuitBreakersPanel implementation. 28 | * Javadsl and Scaladsl delegates to this one. 29 | */ 30 | private[lagom] class CircuitBreakersPanelInternal( 31 | system: ActorSystem, 32 | circuitBreakerConfig: CircuitBreakerConfig, 33 | metricsProvider: CircuitBreakerMetricsProvider 34 | ) { 35 | private final case class CircuitBreakerHolder( 36 | breaker: AkkaCircuitBreaker, 37 | metrics: CircuitBreakerMetrics, 38 | failedCallDefinition: Try[_] => Boolean 39 | ) 40 | 41 | private lazy val config = circuitBreakerConfig.config 42 | private lazy val defaultBreakerConfig = circuitBreakerConfig.default 43 | 44 | private val breakers = mutable.Map.empty[String, Option[CircuitBreakerHolder]] 45 | 46 | def withCircuitBreaker[T](id: String)(body: => Future[T]): Future[T] = { 47 | breaker(id) match { 48 | case Some(CircuitBreakerHolder(b, metrics, failedCallDefinition)) => 49 | val startTime = System.nanoTime() 50 | 51 | def elapsed: Long = System.nanoTime() - startTime 52 | 53 | val result: Future[T] = b.withCircuitBreaker(body, failedCallDefinition) 54 | result.onComplete { 55 | case Success(_) => metrics.onCallSuccess(elapsed) 56 | case Failure(_: CircuitBreakerOpenException) => metrics.onCallBreakerOpenFailure() 57 | case Failure(_: TimeoutException) => metrics.onCallTimeoutFailure(elapsed) 58 | case Failure(_) => metrics.onCallFailure(elapsed) 59 | }(system.dispatcher) 60 | result 61 | case None => body 62 | } 63 | } 64 | 65 | private def createCircuitBreaker(id: String): Option[CircuitBreakerHolder] = { 66 | val allExceptionAsFailure: Try[_] => Boolean = { 67 | case _: Success[_] => false 68 | case _ => true 69 | } 70 | 71 | def failureDefinition(whitelist: Set[String]): Try[_] => Boolean = { 72 | case _: Success[_] => false 73 | case Failure(t) if whitelist.contains(t.getClass.getName) => false 74 | case _ => true 75 | } 76 | 77 | val breakerConfig = 78 | if (config.hasPath(id)) config.getConfig(id).withFallback(defaultBreakerConfig) 79 | else defaultBreakerConfig 80 | 81 | if (breakerConfig.getBoolean("enabled")) { 82 | val maxFailures = breakerConfig.getInt("max-failures") 83 | val callTimeout = breakerConfig.getDuration("call-timeout", MILLISECONDS).millis 84 | val resetTimeout = breakerConfig.getDuration("reset-timeout", MILLISECONDS).millis 85 | 86 | import scala.collection.JavaConverters.asScalaBufferConverter 87 | val exceptionWhitelist: Set[String] = breakerConfig.getStringList("exception-whitelist").asScala.toSet 88 | 89 | val definitionOfFailure = 90 | if (exceptionWhitelist.isEmpty) allExceptionAsFailure else failureDefinition(exceptionWhitelist) 91 | 92 | val breaker = 93 | new AkkaCircuitBreaker(system.scheduler, maxFailures, callTimeout, resetTimeout)(system.dispatcher) 94 | val metrics = metricsProvider.start(id) 95 | 96 | breaker.onClose(metrics.onClose()) 97 | breaker.onOpen(metrics.onOpen()) 98 | breaker.onHalfOpen(metrics.onHalfOpen()) 99 | 100 | Some(CircuitBreakerHolder(breaker, metrics, definitionOfFailure)) 101 | } else None 102 | } 103 | 104 | private def breaker(id: String): Option[CircuitBreakerHolder] = 105 | breakers.getOrElseUpdate(id, createCircuitBreaker(id)) 106 | } 107 | 108 | @Singleton 109 | class CircuitBreakerConfig @Inject() (val configuration: Config) { 110 | val config: Config = configuration.getConfig("lagom.circuit-breaker") 111 | val default: Config = config.getConfig("default") 112 | } 113 | -------------------------------------------------------------------------------- /lagomjs-client-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/client/ServiceLocators.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.scaladsl.client 6 | 7 | import java.net.URI 8 | import java.util.concurrent.atomic.AtomicInteger 9 | 10 | import akka.actor.ActorSystem 11 | import com.lightbend.lagom.internal.client.CircuitBreakerConfig 12 | import com.lightbend.lagom.internal.client.ConfigExtensions 13 | import com.lightbend.lagom.internal.scaladsl.client.CircuitBreakersPanelImpl 14 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetricsProvider 15 | import com.lightbend.lagom.scaladsl.api.Descriptor.Call 16 | import com.lightbend.lagom.scaladsl.api.CircuitBreaker 17 | import com.lightbend.lagom.scaladsl.api.Descriptor 18 | import com.lightbend.lagom.scaladsl.api.LagomConfigComponent 19 | import com.lightbend.lagom.scaladsl.api.ServiceLocator 20 | import com.typesafe.config.Config 21 | 22 | import scala.collection.immutable 23 | import scala.concurrent.ExecutionContext 24 | import scala.concurrent.Future 25 | 26 | /** 27 | * Abstract service locator that provides circuit breaking. 28 | * 29 | * Generally, only the [[ServiceLocator.locate()]] method needs to be implemented, however 30 | * [[doWithServiceImpl()]] can be overridden if the service locator wants to 31 | * handle failures in some way. 32 | */ 33 | abstract class CircuitBreakingServiceLocator(circuitBreakers: CircuitBreakersPanel)(implicit ec: ExecutionContext) 34 | extends ServiceLocator { 35 | 36 | /** 37 | * Do the given block with the given service looked up. 38 | * 39 | * This is invoked by [[doWithService()]], after wrapping the passed in block 40 | * in a circuit breaker if configured to do so. 41 | * 42 | * The default implementation just delegates to the [[locate()]] method, but this method 43 | * can be overridden if the service locator wants to inject other behaviour after the service call is complete. 44 | * 45 | * @param name The service name. 46 | * @param serviceCall The service call that needs the service lookup. 47 | * @param block A block of code that will use the looked up service, typically, to make a call on that service. 48 | * @return A future of the result of the block, if the service lookup was successful. 49 | */ 50 | protected def doWithServiceImpl[T](name: String, serviceCall: Descriptor.Call[_, _])( 51 | block: URI => Future[T] 52 | ): Future[Option[T]] = { 53 | locate(name, serviceCall).flatMap { 54 | case (Some(uri)) => block(uri).map(Some.apply) 55 | case None => Future.successful(None) 56 | } 57 | } 58 | 59 | final override def doWithService[T](name: String, serviceCall: Call[_, _])( 60 | block: (URI) => Future[T] 61 | )(implicit ec: ExecutionContext): Future[Option[T]] = { 62 | serviceCall.circuitBreaker 63 | .filter(_ != CircuitBreaker.None) 64 | .map { cb => 65 | val circuitBreakerId = cb match { 66 | case cbid: CircuitBreaker.CircuitBreakerId => cbid.id 67 | case _ => name 68 | } 69 | 70 | doWithServiceImpl(name, serviceCall) { uri => 71 | circuitBreakers.withCircuitBreaker(circuitBreakerId)(block(uri)) 72 | } 73 | } 74 | .getOrElse { 75 | doWithServiceImpl(name, serviceCall)(block) 76 | } 77 | } 78 | } 79 | 80 | /** 81 | * Components required for circuit breakers. 82 | */ 83 | trait CircuitBreakerComponents extends LagomConfigComponent { 84 | def actorSystem: ActorSystem 85 | def executionContext: ExecutionContext 86 | def circuitBreakerMetricsProvider: CircuitBreakerMetricsProvider 87 | 88 | lazy val circuitBreakerConfig: CircuitBreakerConfig = new CircuitBreakerConfig(config) 89 | 90 | lazy val circuitBreakersPanel: CircuitBreakersPanel = 91 | new CircuitBreakersPanelImpl(actorSystem, circuitBreakerConfig, circuitBreakerMetricsProvider) 92 | } 93 | 94 | /** 95 | * Components for using the configuration service locator. 96 | */ 97 | trait ConfigurationServiceLocatorComponents extends CircuitBreakerComponents { 98 | lazy val serviceLocator: ServiceLocator = 99 | new ConfigurationServiceLocator(config, circuitBreakersPanel)(executionContext) 100 | } 101 | 102 | /** 103 | * A service locator that uses static configuration. 104 | */ 105 | class ConfigurationServiceLocator(config: Config, circuitBreakers: CircuitBreakersPanel)(implicit ec: ExecutionContext) 106 | extends CircuitBreakingServiceLocator(circuitBreakers) { 107 | private val LagomServicesKey: String = "lagom.services" 108 | 109 | private val services = { 110 | if (config.hasPath(LagomServicesKey)) { 111 | val lagomServicesConfig = config.getConfig(LagomServicesKey) 112 | import scala.collection.JavaConverters._ 113 | 114 | (for { 115 | key <- lagomServicesConfig.root.keySet.asScala 116 | } yield { 117 | try { 118 | val uris = ConfigExtensions.getStringList(lagomServicesConfig, key).asScala 119 | key -> uris.map(URI.create).toList 120 | } catch { 121 | case e: IllegalArgumentException => 122 | throw new IllegalStateException( 123 | "Error loading configuration for ConfigurationServiceLocator. " + 124 | s"Expected lagom.services.$key to be a URI, but it failed to parse", 125 | e 126 | ) 127 | } 128 | }).toMap 129 | } else { 130 | Map.empty[String, List[URI]] 131 | } 132 | } 133 | 134 | override def locate(name: String, serviceCall: Call[_, _]) = 135 | locateAll(name, serviceCall).map(_.headOption) 136 | 137 | override def locateAll(name: String, serviceCall: Call[_, _]): Future[List[URI]] = 138 | Future.successful(services.getOrElse(name, Nil)) 139 | } 140 | 141 | /** 142 | * Components for using the static service locator. 143 | */ 144 | trait StaticServiceLocatorComponents extends CircuitBreakerComponents { 145 | def staticServiceUri: URI 146 | 147 | lazy val serviceLocator: ServiceLocator = 148 | new StaticServiceLocator(staticServiceUri, circuitBreakersPanel)(executionContext) 149 | } 150 | 151 | /** 152 | * A static service locator, that always resolves the same URI. 153 | */ 154 | class StaticServiceLocator(uri: URI, circuitBreakers: CircuitBreakersPanel)(implicit ec: ExecutionContext) 155 | extends CircuitBreakingServiceLocator(circuitBreakers) { 156 | override def locate(name: String, serviceCall: Call[_, _]): Future[Option[URI]] = Future.successful(Some(uri)) 157 | } 158 | 159 | /** 160 | * Components for using the round robin service locator. 161 | */ 162 | trait RoundRobinServiceLocatorComponents extends CircuitBreakerComponents { 163 | def roundRobinServiceUris: immutable.Seq[URI] 164 | 165 | lazy val serviceLocator: ServiceLocator = 166 | new RoundRobinServiceLocator(roundRobinServiceUris, circuitBreakersPanel)(executionContext) 167 | } 168 | 169 | /** 170 | * A round robin service locator, that cycles through a list of URIs. 171 | */ 172 | class RoundRobinServiceLocator(uris: immutable.Seq[URI], circuitBreakers: CircuitBreakersPanel)( 173 | implicit ec: ExecutionContext 174 | ) extends CircuitBreakingServiceLocator(circuitBreakers) { 175 | private val counter = new AtomicInteger(0) 176 | 177 | override def locate(name: String, serviceCall: Call[_, _]): Future[Option[URI]] = { 178 | val index = Math.abs(counter.getAndIncrement() % uris.size) 179 | val uri = uris(index) 180 | Future.successful(Some(uri)) 181 | } 182 | 183 | override def locateAll(name: String, serviceCall: Call[_, _]): Future[List[URI]] = 184 | Future.successful(uris.toList) 185 | } 186 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/WebSocketClient.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.client 2 | 3 | import akka.NotUsed 4 | import akka.stream.Materializer 5 | import akka.stream.scaladsl.Flow 6 | import akka.stream.scaladsl.Sink 7 | import akka.stream.scaladsl.Source 8 | import akka.util.ByteString 9 | import com.lightbend.lagom.internal.api.transport.LagomServiceApiBridge 10 | import com.typesafe.config.Config 11 | import org.reactivestreams.Publisher 12 | import org.reactivestreams.Subscriber 13 | import org.reactivestreams.Subscription 14 | import org.scalajs.dom.CloseEvent 15 | import org.scalajs.dom.WebSocket 16 | import org.scalajs.dom.raw.Event 17 | import play.api.http.Status 18 | 19 | import java.net.URI 20 | import java.nio.ByteBuffer 21 | import scala.collection.immutable._ 22 | import scala.concurrent.ExecutionContext 23 | import scala.concurrent.Future 24 | import scala.concurrent.Promise 25 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._ 26 | 27 | private[lagom] abstract class WebSocketClient(config: WebSocketClientConfig)( 28 | implicit ec: ExecutionContext, 29 | materializer: Materializer 30 | ) extends LagomServiceApiBridge { 31 | 32 | /** 33 | * Connect to the given URI 34 | */ 35 | def connect( 36 | exceptionSerializer: ExceptionSerializer, 37 | requestHeader: RequestHeader, 38 | outgoing: Source[ByteString, NotUsed] 39 | ): Future[(ResponseHeader, Source[ByteString, NotUsed])] = { 40 | // Convert http URI to ws URI 41 | val uri = requestHeaderUri(requestHeader).normalize 42 | val scheme = uri.getScheme.toLowerCase match { 43 | case "http" => "ws" 44 | case "https" => "wss" 45 | case _ => throw new RuntimeException(s"Unsupported URI scheme ${uri.getScheme}") 46 | } 47 | val url = new URI(scheme, uri.getAuthority, uri.getPath, uri.getQuery, uri.getFragment).toString 48 | 49 | // Open the socket and set its binaryType so that binary data can be parsed 50 | val socket = new WebSocket(url) 51 | socket.binaryType = "arraybuffer" 52 | 53 | // Create sink that will receive the outgoing data from the request source and send it to the socket 54 | val subscriber = new WebSocketSubscriber(socket, exceptionSerializer) 55 | val socketSink = Sink.fromSubscriber(subscriber) 56 | // Create source that will receive the incoming socket data and send it to the response source 57 | val publisher = new WebSocketPublisher(socket, exceptionSerializer, messageHeaderProtocol(requestHeader)) 58 | val socketSource = Source.fromPublisher(publisher) 59 | 60 | // Create flow that represents sending data to and receiving data from the socket 61 | // The sending side is: socketSink -> socket -> service 62 | // The receiving side is: service -> socket -> socketSource 63 | // The sink and source are not connected in Akka, the socket is the intermediary that connects them 64 | val clientConnection = Flow.fromSinkAndSource(socketSink, socketSource) 65 | 66 | val promise = Promise[(ResponseHeader, Source[ByteString, NotUsed])]() 67 | 68 | // Create artificial response header because it is not possible to get it from a WebSocket in JavaScript 69 | // Use the HTTP 101 response code to indicate switching protocols to WebSocket 70 | val protocol = messageProtocolFromContentTypeHeader(None) 71 | val responseHeader = newResponseHeader(Status.SWITCHING_PROTOCOLS, protocol, Map.empty) 72 | 73 | // Fail if the socket fails to open 74 | // Use an event listener and remove it if the socket opens successfully so that the socketSource can 75 | // use socket onError for error handling 76 | val openOnError = (event: Event) => { 77 | promise.failure(new RuntimeException(s"WebSocket error: ${event.`type`}")) 78 | } 79 | socket.addEventListener[Event]("error", openOnError) 80 | // Succeed and start the data flow if the socket opens successfully 81 | socket.onopen = (_: Event) => { 82 | socket.removeEventListener[Event]("error", openOnError) 83 | promise.success((responseHeader, outgoing.via(clientConnection))) 84 | } 85 | 86 | promise.future 87 | } 88 | 89 | /** 90 | * Subscriber that sends elements to a WebSocket 91 | * 92 | * It does not back-pressure because WebSockets do not support back-pressure. 93 | * 94 | * This is used in conjunction with [[WebSocketPublisher]] to create a flow that represents sending data to and 95 | * receiving data from the socket. Coordinating the completion or cancellation of the flow between the subscriber and 96 | * publisher is done by closing the socket. 97 | */ 98 | private class WebSocketSubscriber( 99 | socket: WebSocket, 100 | exceptionSerializer: ExceptionSerializer 101 | ) extends Subscriber[ByteString] { 102 | override def onSubscribe(s: Subscription): Unit = { 103 | // Cancel upstream when the socket closes 104 | // Use an event listener so that the WebSocketPublisher can use socket onClose 105 | socket.addEventListener[CloseEvent]("close", (_: CloseEvent) => s.cancel()) 106 | // Request unbounded demand since WebSockets do not support back-pressure 107 | s.request(Long.MaxValue) 108 | } 109 | 110 | override def onNext(t: ByteString): Unit = { 111 | // Convert the message into a JavaScript ArrayBuffer 112 | val data = t.asByteBuffer 113 | val buffer = { 114 | if (data.hasTypedArray()) { 115 | data.typedArray().subarray(data.position, data.limit).buffer 116 | } else { 117 | ByteBuffer.allocateDirect(data.remaining).put(data).typedArray().buffer 118 | } 119 | } 120 | // Send the message if the socket is open 121 | // This protects against the case when the socket is closed, the stream is cancelled, and the stream tries to 122 | // finish sending pending messages to a closed socket 123 | if (socket.readyState == WebSocket.OPEN) socket.send(buffer) 124 | } 125 | 126 | override def onError(t: Throwable): Unit = { 127 | // Close the socket in error based on the exception 128 | val rawExceptionMessage = exceptionSerializerSerialize(exceptionSerializer, t, Nil) 129 | val code = rawExceptionMessageWebSocketCode(rawExceptionMessage) 130 | val message = rawExceptionMessageMessageAsText(rawExceptionMessage) 131 | socket.close(code, message) 132 | } 133 | 134 | override def onComplete(): Unit = { 135 | socket.close() 136 | } 137 | } 138 | 139 | /** 140 | * Publisher that receives elements from a WebSocket 141 | * 142 | * It buffers elements to compensate for the delay between WebSocket connection and stream start and for the lack of 143 | * WebSocket back-pressure. 144 | * 145 | * This is used in conjunction with [[WebSocketSubscriber]] to create a flow that represents sending data to and 146 | * receiving data from the socket. Coordinating the completion or cancellation of the flow between the subscriber and 147 | * publisher is done by closing the socket. 148 | */ 149 | private class WebSocketPublisher( 150 | socket: WebSocket, 151 | exceptionSerializer: ExceptionSerializer, 152 | requestProtocol: MessageProtocol 153 | ) extends Publisher[ByteString] { 154 | // The buffer handles queueing elements, tracking subscriber demand, and sending elements in response to demand 155 | private val buffer = new WebSocketStreamBuffer(socket, config.bufferSize, deserializeException) 156 | 157 | private def deserializeException(code: Int, bytes: ByteString): Throwable = 158 | exceptionSerializerDeserializeWebSocketException(exceptionSerializer, code, requestProtocol, bytes) 159 | 160 | override def subscribe(subscriber: Subscriber[_ >: ByteString]): Unit = { 161 | if (subscriber != null) { 162 | // Attach the subscriber to the buffer, the buffer will send elements in response to requests 163 | // The publisher and buffer only support one subscriber and will fail any subsequent subscribers 164 | buffer.attach(subscriber) 165 | } else { 166 | throw new NullPointerException("Subscriber is null") 167 | } 168 | } 169 | } 170 | 171 | } 172 | 173 | private[lagom] sealed trait WebSocketClientConfig { 174 | def bufferSize: Int 175 | } 176 | 177 | private[lagom] object WebSocketClientConfig { 178 | def apply(conf: Config): WebSocketClientConfig = 179 | new WebSocketClientConfigImpl(conf.getConfig("lagom.client.websocket")) 180 | 181 | class WebSocketClientConfigImpl(conf: Config) extends WebSocketClientConfig { 182 | val bufferSize = conf.getString("bufferSize") match { 183 | case "unlimited" => Int.MaxValue 184 | case _ => conf.getInt("bufferSize") 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /lagomjs-client-scaladsl/js/src/main/scala/com/lightbend/lagom/internal/scaladsl/client/ScaladslServiceClientInvoker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.scaladsl.client 6 | 7 | import akka.NotUsed 8 | import akka.stream.Materializer 9 | import akka.stream.scaladsl.Source 10 | import akka.util.ByteString 11 | import com.lightbend.lagom.internal.api.Path 12 | import com.lightbend.lagom.internal.client.ClientServiceCallInvoker 13 | import com.lightbend.lagom.internal.scaladsl.api.ScaladslPath 14 | import com.lightbend.lagom.internal.scaladsl.api.broker.TopicFactory 15 | import com.lightbend.lagom.scaladsl.api._ 16 | import com.lightbend.lagom.scaladsl.api.Descriptor.Call 17 | import com.lightbend.lagom.scaladsl.api.Descriptor.RestCallId 18 | import com.lightbend.lagom.scaladsl.api.Descriptor.TopicCall 19 | import com.lightbend.lagom.scaladsl.api.ServiceSupport.ScalaMethodServiceCall 20 | import com.lightbend.lagom.scaladsl.api.ServiceSupport.ScalaMethodTopic 21 | import com.lightbend.lagom.scaladsl.api.broker.Topic 22 | import com.lightbend.lagom.scaladsl.api.deser._ 23 | import com.lightbend.lagom.scaladsl.api.transport.Method 24 | import com.lightbend.lagom.scaladsl.api.transport.RequestHeader 25 | import com.lightbend.lagom.scaladsl.api.transport.ResponseHeader 26 | import com.lightbend.lagom.scaladsl.client.ServiceClientConstructor 27 | import com.lightbend.lagom.scaladsl.client.ServiceClientContext 28 | import com.lightbend.lagom.scaladsl.client.ServiceClientImplementationContext 29 | import com.lightbend.lagom.scaladsl.client.ServiceResolver 30 | 31 | import scala.collection.immutable 32 | import scala.concurrent.ExecutionContext 33 | import scala.concurrent.Future 34 | 35 | private[lagom] class ScaladslServiceClient( 36 | webSocketClient: ScaladslWebSocketClient, 37 | serviceInfo: ServiceInfo, 38 | serviceLocator: ServiceLocator, 39 | serviceResolver: ServiceResolver, 40 | topicFactory: Option[TopicFactory] 41 | )(implicit ec: ExecutionContext, mat: Materializer) 42 | extends ServiceClientConstructor { 43 | private val ctx: ServiceClientImplementationContext = new ServiceClientImplementationContext { 44 | override def resolve(unresolvedDescriptor: Descriptor): ServiceClientContext = new ServiceClientContext { 45 | val descriptor = serviceResolver.resolve(unresolvedDescriptor) 46 | 47 | val serviceCalls: Map[String, ScalaServiceCall] = descriptor.calls.map { call => 48 | call.serviceCallHolder match { 49 | case methodServiceCall: ScalaMethodServiceCall[_, _] => 50 | val pathSpec = ScaladslPath.fromCallId(call.callId) 51 | methodServiceCall.method.getName -> ScalaServiceCall(call, pathSpec, methodServiceCall.pathParamSerializers) 52 | } 53 | }.toMap 54 | 55 | val topics: Map[String, TopicCall[_]] = descriptor.topics.map { topic => 56 | topic.topicHolder match { 57 | case methodTopic: ScalaMethodTopic[_] => 58 | methodTopic.method.getName -> topic 59 | } 60 | }.toMap 61 | 62 | override def createServiceCall[Request, Response]( 63 | methodName: String, 64 | params: immutable.Seq[Any] 65 | ): ServiceCall[Request, Response] = { 66 | serviceCalls.get(methodName) match { 67 | case Some(ScalaServiceCall(call, pathSpec, pathParamSerializers)) => 68 | val serializedParams = pathParamSerializers.zip(params).map { 69 | case (serializer: PathParamSerializer[Any], param) => serializer.serialize(param) 70 | } 71 | val (path, queryParams) = pathSpec.format(serializedParams) 72 | 73 | val invoker = new ScaladslClientServiceCallInvoker[Request, Response]( 74 | webSocketClient, 75 | serviceInfo, 76 | serviceLocator, 77 | descriptor, 78 | call.asInstanceOf[Call[Request, Response]], 79 | path, 80 | queryParams 81 | ) 82 | 83 | new ScaladslClientServiceCall[Request, Response, Response](invoker, identity, (header, message) => message) 84 | 85 | case None => throw new RuntimeException("No descriptor for service call method: " + methodName) 86 | } 87 | } 88 | 89 | override def createTopic[Message](methodName: String): Topic[Message] = { 90 | topicFactory match { 91 | case Some(tf) => 92 | topics.get(methodName) match { 93 | case Some(topicCall: TopicCall[Message]) => tf.create(topicCall) 94 | case None => throw new RuntimeException("No descriptor for topic method: " + methodName) 95 | } 96 | case None => 97 | throw new RuntimeException( 98 | "No message broker implementation to create topic from. Did you forget to include com.lightbend.lagom.scaladsl.broker.kafka.LagomKafkaClientComponents in your application?" 99 | ) 100 | } 101 | } 102 | } 103 | } 104 | 105 | override def construct[S <: Service](constructor: (ServiceClientImplementationContext) => S): S = constructor(ctx) 106 | 107 | private case class ScalaServiceCall( 108 | call: Call[_, _], 109 | pathSpec: Path, 110 | pathParamSerializers: immutable.Seq[PathParamSerializer[_]] 111 | ) 112 | } 113 | 114 | /** 115 | * The service call implementation. Delegates actual work to the invoker, while maintaining the handler function for 116 | * the request header and a transformer function for the response. 117 | */ 118 | private class ScaladslClientServiceCall[Request, ResponseMessage, ServiceCallResponse]( 119 | invoker: ScaladslClientServiceCallInvoker[Request, ResponseMessage], 120 | requestHeaderHandler: RequestHeader => RequestHeader, 121 | responseHandler: (ResponseHeader, ResponseMessage) => ServiceCallResponse 122 | )(implicit ec: ExecutionContext) 123 | extends ServiceCall[Request, ServiceCallResponse] { 124 | override def invoke(request: Request): Future[ServiceCallResponse] = { 125 | invoker.doInvoke(request, requestHeaderHandler).map(responseHandler.tupled) 126 | } 127 | 128 | override def handleRequestHeader( 129 | handler: RequestHeader => RequestHeader 130 | ): ServiceCall[Request, ServiceCallResponse] = { 131 | new ScaladslClientServiceCall(invoker, requestHeaderHandler.andThen(handler), responseHandler) 132 | } 133 | 134 | override def handleResponseHeader[T](handler: (ResponseHeader, ServiceCallResponse) => T): ServiceCall[Request, T] = { 135 | new ScaladslClientServiceCall[Request, ResponseMessage, T]( 136 | invoker, 137 | requestHeaderHandler, 138 | (header, message) => handler.apply(header, responseHandler(header, message)) 139 | ) 140 | } 141 | } 142 | 143 | private class ScaladslClientServiceCallInvoker[Request, Response]( 144 | webSocketClient: ScaladslWebSocketClient, 145 | serviceInfo: ServiceInfo, 146 | override val serviceLocator: ServiceLocator, 147 | override val descriptor: Descriptor, 148 | override val call: Call[Request, Response], 149 | path: String, 150 | queryParams: Map[String, Seq[String]] 151 | )(implicit ec: ExecutionContext, mat: Materializer) 152 | extends ClientServiceCallInvoker[Request, Response](serviceInfo.serviceName, path, queryParams) 153 | with ScaladslServiceApiBridge { 154 | protected override def doMakeStreamedCall( 155 | requestStream: Source[ByteString, NotUsed], 156 | requestSerializer: NegotiatedSerializer[_, _], 157 | requestHeader: RequestHeader 158 | ): Future[(ResponseHeader, Source[ByteString, NotUsed])] = 159 | webSocketClient.connect(descriptor.exceptionSerializer, requestHeader, requestStream) 160 | } 161 | 162 | private[lagom] class ScaladslServiceResolver(defaultExceptionSerializer: ExceptionSerializer) extends ServiceResolver { 163 | override def resolve(descriptor: Descriptor): Descriptor = { 164 | val withExceptionSerializer: Descriptor = 165 | if (descriptor.exceptionSerializer == DefaultExceptionSerializer.Unresolved) { 166 | descriptor.withExceptionSerializer(defaultExceptionSerializer) 167 | } else descriptor 168 | 169 | val withAcls: Descriptor = { 170 | val acls = descriptor.calls.collect { 171 | case callWithAutoAcl if callWithAutoAcl.autoAcl.getOrElse(descriptor.autoAcl) => 172 | val pathSpec = ScaladslPath.fromCallId(callWithAutoAcl.callId).regex.regex 173 | val method = calculateMethod(callWithAutoAcl) 174 | ServiceAcl(Some(method), Some(pathSpec)) 175 | } 176 | 177 | if (acls.nonEmpty) { 178 | withExceptionSerializer.addAcls(acls: _*) 179 | } else withExceptionSerializer 180 | } 181 | 182 | val withCircuitBreakers = { 183 | // iterate all calls and replace those where CB is None with their setup or the default. 184 | val callsWithCircuitBreakers: Seq[Call[_, _]] = descriptor.calls.map { call => 185 | val circuitBreaker = call.circuitBreaker.getOrElse(descriptor.circuitBreaker) 186 | call.withCircuitBreaker(circuitBreaker) 187 | } 188 | withAcls.withCalls(callsWithCircuitBreakers: _*) 189 | } 190 | 191 | withCircuitBreakers 192 | } 193 | 194 | private def calculateMethod(serviceCall: Descriptor.Call[_, _]): Method = { 195 | serviceCall.callId match { 196 | case rest: RestCallId => rest.method 197 | case _ => 198 | // If either the request or the response serializers are streamed, then WebSockets will be used, in which case 199 | // the method must be GET 200 | if (serviceCall.requestSerializer.isStreamed || serviceCall.responseSerializer.isStreamed) { 201 | Method.GET 202 | // Otherwise, if the request serializer is used, we default to POST 203 | } else if (serviceCall.requestSerializer.isUsed) { 204 | Method.POST 205 | } else { 206 | // And if not, to GET 207 | Method.GET 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lagom.js - Scala.js Client for Lagom 2 | 3 | Lagom.js is a port of the [Lagom](https://www.lagomframework.com/) service API and service client to JavaScript using Scala.js. It allows you to use and interact with your service API all in JavaScript. Eliminate the need to keep your frontend in sync with your service API, let lagom.js handle it for you. 4 | 5 | Checkout the [lagom-scalajs-example](https://github.com/mliarakos/lagom-scalajs-example) for a demo of how to use lagom.js. 6 | 7 | ## Compatibility 8 | 9 | Lagom.js is built against specific versions of Lagom, the latest are: 10 | 11 | | Lagom.js | Lagom | Scala | Scala.js | 12 | |-------------|-------|-----------------|----------| 13 | | 0.1.2-1.5.5 | 1.5.5 | 2.11
2.12 | 0.6.31 | 14 | | 0.3.2-1.6.2 | 1.6.2 | 2.12
2.13 | 0.6.33 | 15 | | 0.5.1-1.6.5 | 1.6.5 | 2.12
2.13 | 1.2.0 | 16 | 17 | Lagom.js moved to Scala.js 1.x starting with version `0.4.0-1.6.2`. Scala.js 0.6 is no longer supported, the last version to support it was `0.3.2-1.6.2`. For all past releases, see [releases](#Releases). 18 | 19 | Lagom.js does not support the Lagom Java API. It only supports the Lagom Scala API because Scala.js only supports Scala. 20 | 21 | ## Usage 22 | 23 | Lagom.js provides JavaScript versions of several Lagom artifacts. The two most important are the service API and service client. 24 | 25 | ### Service API 26 | 27 | The `lagomjs-scaladsl-api` artifact provides the JavaScript implementation of the Lagom service API: 28 | 29 | ```sbt 30 | "com.github.mliarakos.lagomjs" %%% "lagomjs-scaladsl-api" % "0.5.1-1.6.5" 31 | ``` 32 | 33 | To use it you'll need to configure your service API as a [Scala.js cross project](https://github.com/portable-scala/sbt-crossproject) for the JVM and JS platforms. Then, add the `lagomjs-scaladsl-api` dependency to the JS platform: 34 | 35 | ```scala 36 | lazy val `service-api` = crossProject(JVMPlatform, JSPlatform) 37 | .crossType(CrossType.Full) 38 | .jvmSettings( 39 | libraryDependencies += lagomScaladslApi 40 | ) 41 | .jsSettings( 42 | libraryDependencies += "com.github.mliarakos.lagomjs" %%% "lagomjs-scaladsl-api" % "0.5.1-1.6.5" 43 | ) 44 | ``` 45 | 46 | This enables your Lagom service definition to be compiled into JavaScript. In addition, your domain objects, service requests and responses, and custom exceptions are also compiled into JavaScript. This makes your entire service API available in JavaScript. 47 | 48 | ### Service Client 49 | 50 | The `lagomjs-scaladsl-client` artifact provides the JavaScript implementation of the Lagom service client: 51 | 52 | ```sbt 53 | "com.github.mliarakos.lagomjs" %%% "lagomjs-scaladsl-client" % "0.5.1-1.6.5" 54 | ``` 55 | 56 | You can use it in a Scala.js project along with your service API to generate a service client: 57 | 58 | ```scala 59 | lazy val `client-js` = project 60 | .settings( 61 | libraryDependencies += "com.github.mliarakos.lagomjs" %%% "lagomjs-scaladsl-client" % "0.5.1-1.6.5" 62 | ) 63 | .enablePlugins(ScalaJSPlugin) 64 | .dependsOn(`service-api`.js) 65 | ``` 66 | 67 | The service client can be used to interact with your service by making service calls, just as you normally would in Scala. Since the entire service API is available in JavaScript you have everything you need to create requests and understand responses. 68 | 69 | ## Features 70 | 71 | Lagom.js supports cross compiling the full Lagom service API into JavaScript. The service client supports almost all features available in Lagom: 72 | - all the service call definitions: `call`, `namedCall`, `pathCall`, `restCall` 73 | - serialization of service requests and responses using `play-json` 74 | - streaming service requests and responses using [Akka.js](https://github.com/akka-js/akka.js) and WebSockets 75 | - circuit breakers with basic metrics 76 | - all the built-in service locators: `ConfigurationServiceLocator`, `StaticServiceLocator` and `RoundRobinServiceLocator` 77 | 78 | However, the service client does not support a few the features available in Lagom: 79 | - full circuit breaker metrics: circuit breakers are fully supported, but the built-in circuit breaker metrics implementation only collects a few basic metrics 80 | - subscribing to topics: topic definitions are available in the service client, but attempting to subscribe to the topic throws an exception 81 | - advanced service locators: service locators outside the built-in service locators, such as `AkkaDiscoveryServiceLocator`, are not available 82 | 83 | ## Configuration 84 | 85 | ### Application Configuration 86 | 87 | Lagom.js uses [shocon](https://github.com/akka-js/shocon) as a Scala.js replacement for [Typesafe Config](https://github.com/lightbend/config). The library loads [default configurations](https://github.com/akka-js/shocon#loading-of-default-configuration) at compile time. If you use an application config (`application.conf`) then you need to manually load the config and fall back to the default Lagom.js config: 88 | 89 | ```scala 90 | abstract class MyApplication extends StandaloneLagomClientFactory("my-application") { 91 | // Load application config with the lagomjs default config as the fallback 92 | lazy val conf = ConfigFactory.load().withFallback(lagomjs.Config.default) 93 | override lazy val configuration: Configuration = Configuration(conf) 94 | } 95 | ``` 96 | 97 | This also applies to parsed configs: 98 | 99 | ```scala 100 | abstract class MyApplication extends StandaloneLagomClientFactory("my-application") { 101 | // Parse config with the lagomjs default config as the fallback 102 | lazy val conf = ConfigFactory.parseString(""" 103 | lagom.client.websocket { 104 | bufferSize = 128 105 | }""") 106 | .withFallback(lagomjs.Config.default) 107 | override lazy val configuration: Configuration = Configuration(conf) 108 | } 109 | ``` 110 | 111 | This approach is based on [Akka.js configurations](https://github.com/akka-js/akka.js#add-ons). 112 | 113 | ### WebSocket Stream Buffer 114 | 115 | Streaming service requests and responses are implemented using WebSockets. When starting a WebSocket connection there is a slight delay between the socket opening and the response stream being set up and ready to consume messages. To compensate for this delay the lagom.js WebSocket client uses a receive buffer to hold messages until the stream is ready. The buffer size can be set through configuration: 116 | 117 | ```yaml 118 | lagom.client.websocket.bufferSize = 16 119 | ``` 120 | 121 | The buffer is sized by default to compensate for this delay. However, the buffer can also be used for another purpose. 122 | 123 | The current WebSocket standard prevents the lagom.js WebSocket client from supporting stream back-pressure for sending or receiving WebSocket data. This can cause overflow errors and stream failure when upstream production is faster than downstream consumption. Issues are most common on the receiving side of a streaming response. Normally, standard Akka methods, such as [buffer](https://doc.akka.io/docs/akka/current/stream/stream-rate.html), could be used to mitigate this issue. Unfortunately, these methods generally do not perform well in practice because of the way operations are scheduled on the JavaScript event-loop. A fast upstream often fails the stream before the Akka buffer logic has a chance to run. 124 | 125 | The lagom.js receive buffer is implemented to schedule downstream consumption on the event-loop as soon as possible to mitigate this issue. The buffer size can be tuned to compensate for a fast streaming response. This is useful if the upstream has bursts of throughput that can overwhelm the downstream. However, depending on the use case, it may not be possible to fully compensate for stream rate differences. An upstream that is consistently faster than the downstream will eventually overflow the buffer. The buffer can be set to an unbounded size: 126 | 127 | ```yaml 128 | lagom.client.websocket.bufferSize = unlimited 129 | ``` 130 | 131 | However, an unbounded buffer can negatively impact browser performance and will eventually fail due to lack of memory. 132 | 133 | Alternatively, if the upstream logic (before data is sent over the WebSocket) can be modified, then the stream can be throttled to reduce pressure on the downstream. For example, in a `ServiceCall` implementation: 134 | 135 | ```scala 136 | override def zeros = ServerServiceCall { _ => 137 | // Throttle the source to 1 element per second 138 | val source = Source.repeat(0).throttle(elements = 1, per = 1.second) 139 | Future.successful(source) 140 | } 141 | ``` 142 | 143 | The `throttle` operator will back-pressure to achieve the desired rate and since the upstream can slow down it will not overwhelm the downstream. The throttle parameters should be tuned per use case. 144 | 145 | WebSocket back-pressure for streams may become available once the [WebSocketStream API](https://web.dev/websocketstream/) is complete and widely available. 146 | 147 | ## Releases 148 | 149 | Lagom.js tracks Lagom and generally doesn't continue development on previous Lagom releases. Since Lagom maintains a stable API within a minor release (e.g., 1.6.x) any version of Lagom.js built for that minor release should work. However, if you need Lagom.js for a specific previous Lagom release the previous Lagom.js releases are listed below. If you have an issue with a previous Lagom.js release please open an [issue](https://github.com/mliarakos/lagom-js/issues) and it will be considered on a case-by-case basis. 150 | 151 | | Lagom.js | Lagom | Scala | Scala.js | 152 | |-------------|-------|-----------------|----------| 153 | | 0.1.2-1.5.1 | 1.5.1 | 2.11
2.12 | 0.6.31 | 154 | | 0.1.2-1.5.3 | 1.5.3 | 2.11
2.12 | 0.6.31 | 155 | | 0.1.2-1.5.4 | 1.5.4 | 2.11
2.12 | 0.6.31 | 156 | | 0.1.2-1.5.5 | 1.5.5 | 2.11
2.12 | 0.6.31 | 157 | | 0.2.1-1.6.0 | 1.6.0 | 2.12 | 0.6.31 | 158 | | 0.3.1-1.6.1 | 1.6.1 | 2.12
2.13 | 0.6.32 | 159 | | 0.3.2-1.6.2 | 1.6.2 | 2.12
2.13 | 0.6.33 | 160 | | 0.4.0-1.6.2 | 1.6.2 | 2.12
2.13 | 1.0.1 | 161 | | 0.4.0-1.6.3 | 1.6.3 | 2.12
2.13 | 1.1.1 | 162 | | 0.4.0-1.6.4 | 1.6.4 | 2.12
2.13 | 1.2.0 | 163 | | 0.5.0-1.6.4 | 1.6.4 | 2.12
2.13 | 1.2.0 | 164 | | 0.5.0-1.6.5 | 1.6.5 | 2.12
2.13 | 1.2.0 | 165 | | 0.5.1-1.6.5 | 1.6.5 | 2.12
2.13 | 1.2.0 | 166 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/api/http/StandardValues.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | /* 6 | * Copy of StandardValues.scala for JS compatibility. 7 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/api/http/StandardValues.scala 8 | */ 9 | 10 | package play.api.http 11 | 12 | /** 13 | * Defines common HTTP Content-Type header values, according to the current available Codec. 14 | */ 15 | object ContentTypes extends ContentTypes 16 | 17 | /** Defines common HTTP Content-Type header values, according to the current available Codec. */ 18 | trait ContentTypes { 19 | import play.api.mvc.Codec 20 | 21 | /** 22 | * Content-Type of text. 23 | */ 24 | def TEXT(implicit codec: Codec) = withCharset(MimeTypes.TEXT) 25 | 26 | /** 27 | * Content-Type of html. 28 | */ 29 | def HTML(implicit codec: Codec) = withCharset(MimeTypes.HTML) 30 | 31 | /** 32 | * Content-Type of xhtml. 33 | */ 34 | def XHTML(implicit codec: Codec) = withCharset(MimeTypes.XHTML) 35 | 36 | /** 37 | * Content-Type of xml. 38 | */ 39 | def XML(implicit codec: Codec) = withCharset(MimeTypes.XML) 40 | 41 | /** 42 | * Content-Type of css. 43 | */ 44 | def CSS(implicit codec: Codec) = withCharset(MimeTypes.CSS) 45 | 46 | /** 47 | * Content-Type of javascript. 48 | */ 49 | def JAVASCRIPT(implicit codec: Codec) = withCharset(MimeTypes.JAVASCRIPT) 50 | 51 | /** 52 | * Content-Type of server sent events. 53 | */ 54 | def EVENT_STREAM(implicit codec: Codec) = withCharset(MimeTypes.EVENT_STREAM) 55 | 56 | /** 57 | * Content-Type of application cache. 58 | */ 59 | val CACHE_MANIFEST = withCharset(MimeTypes.CACHE_MANIFEST)(Codec.utf_8) 60 | 61 | /** 62 | * Content-Type of json. This content type does not define a charset parameter. 63 | */ 64 | val JSON = MimeTypes.JSON 65 | 66 | /** 67 | * Content-Type of form-urlencoded. This content type does not define a charset parameter. 68 | */ 69 | val FORM = MimeTypes.FORM 70 | 71 | /** 72 | * Content-Type of binary data. 73 | */ 74 | val BINARY = MimeTypes.BINARY 75 | 76 | /** 77 | * @return the `codec` charset appended to `mimeType` 78 | */ 79 | def withCharset(mimeType: String)(implicit codec: Codec) = s"$mimeType; charset=${codec.charset}" 80 | } 81 | 82 | /** 83 | * Standard HTTP Verbs 84 | */ 85 | object HttpVerbs extends HttpVerbs 86 | 87 | /** 88 | * Standard HTTP Verbs 89 | */ 90 | trait HttpVerbs { 91 | val GET = "GET" 92 | val POST = "POST" 93 | val PUT = "PUT" 94 | val PATCH = "PATCH" 95 | val DELETE = "DELETE" 96 | val HEAD = "HEAD" 97 | val OPTIONS = "OPTIONS" 98 | } 99 | 100 | /** Common HTTP MIME types */ 101 | object MimeTypes extends MimeTypes 102 | 103 | /** Common HTTP MIME types */ 104 | trait MimeTypes { 105 | 106 | /** 107 | * Content-Type of text. 108 | */ 109 | val TEXT = "text/plain" 110 | 111 | /** 112 | * Content-Type of html. 113 | */ 114 | val HTML = "text/html" 115 | 116 | /** 117 | * Content-Type of json. 118 | */ 119 | val JSON = "application/json" 120 | 121 | /** 122 | * Content-Type of xml. 123 | */ 124 | val XML = "application/xml" 125 | 126 | /** 127 | * Content-Type of xhtml. 128 | */ 129 | val XHTML = "application/xhtml+xml" 130 | 131 | /** 132 | * Content-Type of css. 133 | */ 134 | val CSS = "text/css" 135 | 136 | /** 137 | * Content-Type of javascript. 138 | */ 139 | val JAVASCRIPT = "application/javascript" 140 | 141 | /** 142 | * Content-Type of form-urlencoded. 143 | */ 144 | val FORM = "application/x-www-form-urlencoded" 145 | 146 | /** 147 | * Content-Type of server sent events. 148 | */ 149 | val EVENT_STREAM = "text/event-stream" 150 | 151 | /** 152 | * Content-Type of binary data. 153 | */ 154 | val BINARY = "application/octet-stream" 155 | 156 | /** 157 | * Content-Type of application cache. 158 | */ 159 | val CACHE_MANIFEST = "text/cache-manifest" 160 | } 161 | 162 | /** 163 | * Defines all standard HTTP status codes, with additional helpers for determining the type of status. 164 | */ 165 | object Status extends Status { 166 | def isInformational(status: Int): Boolean = status / 100 == 1 167 | def isSuccessful(status: Int): Boolean = status / 100 == 2 168 | def isRedirect(status: Int): Boolean = status / 100 == 3 169 | def isClientError(status: Int): Boolean = status / 100 == 4 170 | def isServerError(status: Int): Boolean = status / 100 == 5 171 | } 172 | 173 | /** 174 | * Defines all standard HTTP status codes. 175 | * 176 | * See RFC 7231 and RFC 6585. 177 | */ 178 | trait Status { 179 | val CONTINUE = 100 180 | val SWITCHING_PROTOCOLS = 101 181 | 182 | val OK = 200 183 | val CREATED = 201 184 | val ACCEPTED = 202 185 | val NON_AUTHORITATIVE_INFORMATION = 203 186 | val NO_CONTENT = 204 187 | val RESET_CONTENT = 205 188 | val PARTIAL_CONTENT = 206 189 | val MULTI_STATUS = 207 190 | 191 | val MULTIPLE_CHOICES = 300 192 | val MOVED_PERMANENTLY = 301 193 | val FOUND = 302 194 | val SEE_OTHER = 303 195 | val NOT_MODIFIED = 304 196 | val USE_PROXY = 305 197 | val TEMPORARY_REDIRECT = 307 198 | val PERMANENT_REDIRECT = 308 199 | 200 | val BAD_REQUEST = 400 201 | val UNAUTHORIZED = 401 202 | val PAYMENT_REQUIRED = 402 203 | val FORBIDDEN = 403 204 | val NOT_FOUND = 404 205 | val METHOD_NOT_ALLOWED = 405 206 | val NOT_ACCEPTABLE = 406 207 | val PROXY_AUTHENTICATION_REQUIRED = 407 208 | val REQUEST_TIMEOUT = 408 209 | val CONFLICT = 409 210 | val GONE = 410 211 | val LENGTH_REQUIRED = 411 212 | val PRECONDITION_FAILED = 412 213 | val REQUEST_ENTITY_TOO_LARGE = 413 214 | val REQUEST_URI_TOO_LONG = 414 215 | val UNSUPPORTED_MEDIA_TYPE = 415 216 | val REQUESTED_RANGE_NOT_SATISFIABLE = 416 217 | val EXPECTATION_FAILED = 417 218 | val IM_A_TEAPOT = 418 219 | val UNPROCESSABLE_ENTITY = 422 220 | val LOCKED = 423 221 | val FAILED_DEPENDENCY = 424 222 | val UPGRADE_REQUIRED = 426 223 | val PRECONDITION_REQUIRED = 428 224 | val TOO_MANY_REQUESTS = 429 225 | val REQUEST_HEADER_FIELDS_TOO_LARGE = 431 226 | 227 | val INTERNAL_SERVER_ERROR = 500 228 | val NOT_IMPLEMENTED = 501 229 | val BAD_GATEWAY = 502 230 | val SERVICE_UNAVAILABLE = 503 231 | val GATEWAY_TIMEOUT = 504 232 | val HTTP_VERSION_NOT_SUPPORTED = 505 233 | val INSUFFICIENT_STORAGE = 507 234 | val NETWORK_AUTHENTICATION_REQUIRED = 511 235 | } 236 | 237 | /** Defines all standard HTTP headers. */ 238 | object HeaderNames extends HeaderNames 239 | 240 | /** Defines all standard HTTP headers. */ 241 | trait HeaderNames { 242 | val ACCEPT = "Accept" 243 | val ACCEPT_CHARSET = "Accept-Charset" 244 | val ACCEPT_ENCODING = "Accept-Encoding" 245 | val ACCEPT_LANGUAGE = "Accept-Language" 246 | val ACCEPT_RANGES = "Accept-Ranges" 247 | val AGE = "Age" 248 | val ALLOW = "Allow" 249 | val AUTHORIZATION = "Authorization" 250 | 251 | val CACHE_CONTROL = "Cache-Control" 252 | val CONNECTION = "Connection" 253 | val CONTENT_DISPOSITION = "Content-Disposition" 254 | val CONTENT_ENCODING = "Content-Encoding" 255 | val CONTENT_LANGUAGE = "Content-Language" 256 | val CONTENT_LENGTH = "Content-Length" 257 | val CONTENT_LOCATION = "Content-Location" 258 | val CONTENT_MD5 = "Content-MD5" 259 | val CONTENT_RANGE = "Content-Range" 260 | val CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding" 261 | val CONTENT_TYPE = "Content-Type" 262 | val COOKIE = "Cookie" 263 | 264 | val DATE = "Date" 265 | 266 | val ETAG = "ETag" 267 | val EXPECT = "Expect" 268 | val EXPIRES = "Expires" 269 | 270 | val FROM = "From" 271 | 272 | val HOST = "Host" 273 | 274 | val IF_MATCH = "If-Match" 275 | val IF_MODIFIED_SINCE = "If-Modified-Since" 276 | val IF_NONE_MATCH = "If-None-Match" 277 | val IF_RANGE = "If-Range" 278 | val IF_UNMODIFIED_SINCE = "If-Unmodified-Since" 279 | 280 | val LAST_MODIFIED = "Last-Modified" 281 | val LINK = "Link" 282 | val LOCATION = "Location" 283 | 284 | val MAX_FORWARDS = "Max-Forwards" 285 | 286 | val PRAGMA = "Pragma" 287 | val PROXY_AUTHENTICATE = "Proxy-Authenticate" 288 | val PROXY_AUTHORIZATION = "Proxy-Authorization" 289 | 290 | val RANGE = "Range" 291 | val REFERER = "Referer" 292 | val RETRY_AFTER = "Retry-After" 293 | 294 | val SERVER = "Server" 295 | 296 | val SET_COOKIE = "Set-Cookie" 297 | val SET_COOKIE2 = "Set-Cookie2" 298 | 299 | val TE = "Te" 300 | val TRAILER = "Trailer" 301 | val TRANSFER_ENCODING = "Transfer-Encoding" 302 | 303 | val UPGRADE = "Upgrade" 304 | val USER_AGENT = "User-Agent" 305 | 306 | val VARY = "Vary" 307 | val VIA = "Via" 308 | 309 | val WARNING = "Warning" 310 | val WWW_AUTHENTICATE = "WWW-Authenticate" 311 | 312 | val FORWARDED = "Forwarded" 313 | val X_FORWARDED_FOR = "X-Forwarded-For" 314 | val X_FORWARDED_HOST = "X-Forwarded-Host" 315 | val X_FORWARDED_PORT = "X-Forwarded-Port" 316 | val X_FORWARDED_PROTO = "X-Forwarded-Proto" 317 | 318 | val X_REQUESTED_WITH = "X-Requested-With" 319 | 320 | val ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin" 321 | val ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" 322 | val ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age" 323 | val ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials" 324 | val ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods" 325 | val ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers" 326 | 327 | val ORIGIN = "Origin" 328 | val ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method" 329 | val ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers" 330 | 331 | val STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security" 332 | 333 | val X_FRAME_OPTIONS = "X-Frame-Options" 334 | val X_XSS_PROTECTION = "X-XSS-Protection" 335 | val X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options" 336 | val X_PERMITTED_CROSS_DOMAIN_POLICIES = "X-Permitted-Cross-Domain-Policies" 337 | val REFERRER_POLICY = "Referrer-Policy" 338 | 339 | val CONTENT_SECURITY_POLICY = "Content-Security-Policy" 340 | val CONTENT_SECURITY_POLICY_REPORT_ONLY: String = "Content-Security-Policy-Report-Only" 341 | val X_CONTENT_SECURITY_POLICY_NONCE_HEADER: String = "X-Content-Security-Policy-Nonce" 342 | } 343 | 344 | /** 345 | * Defines HTTP protocol constants 346 | */ 347 | object HttpProtocol extends HttpProtocol 348 | 349 | /** 350 | * Defines HTTP protocol constants 351 | */ 352 | trait HttpProtocol { 353 | // Versions 354 | val HTTP_1_0 = "HTTP/1.0" 355 | val HTTP_1_1 = "HTTP/1.1" 356 | val HTTP_2_0 = "HTTP/2.0" 357 | 358 | // Other HTTP protocol values 359 | val CHUNKED = "chunked" 360 | } 361 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lagomjs-api/js/src/main/scala/play/utils/UriEncoding.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | /* 6 | * Copy of UriEncoding.scala for JS compatibility. 7 | * https://github.com/playframework/playframework/blob/master/core/play/src/main/scala/play/utils/UriEncoding.scala 8 | */ 9 | 10 | package play.utils 11 | 12 | import java.io.ByteArrayOutputStream 13 | import java.nio.charset.Charset 14 | 15 | import play.core.utils.AsciiBitSet 16 | import play.core.utils.AsciiSet 17 | 18 | /** 19 | * Provides support for correctly encoding pieces of URIs. 20 | * 21 | * @see http://www.ietf.org/rfc/rfc3986.txt 22 | * @define javadoc http://docs.oracle.com/javase/8/docs/api 23 | */ 24 | object UriEncoding { 25 | 26 | /** 27 | * Encode a string so that it can be used safely in the "path segment" 28 | * part of a URI. A path segment is defined in RFC 3986. In a URI such 29 | * as `http://www.example.com/abc/def?a=1&b=2` both `abc` and `def` 30 | * are path segments. 31 | * 32 | * Path segment encoding differs from encoding for other parts of a URI. 33 | * For example, the "&" character is permitted in a path segment, but 34 | * has special meaning in query parameters. On the other hand, the "/" 35 | * character cannot appear in a path segment, as it is the path delimiter, 36 | * so it must be encoded as "%2F". These are just two examples of the 37 | * differences between path segment and query string encoding; there are 38 | * other differences too. 39 | * 40 | * When encoding path segments the `encodePathSegment` method should always 41 | * be used in preference to the [[$javadoc/java/net/URLEncoder.html#encode-java.lang.String-java.lang.String- java.net.URLEncoder.encode]] 42 | * method. `URLEncoder.encode`, despite its name, actually provides encoding 43 | * in the `application/x-www-form-urlencoded` MIME format which is the encoding 44 | * used for form data in HTTP GET and POST requests. This encoding is suitable 45 | * for inclusion in the query part of a URI. But `URLEncoder.encode` should not 46 | * be used for path segment encoding. (Also note that `URLEncoder.encode` is 47 | * not quite spec compliant. For example, it percent-encodes the `~` character when 48 | * really it should leave it as unencoded.) 49 | * 50 | * @param s The string to encode. 51 | * @param inputCharset The name of the encoding that the string `s` is encoded with. 52 | * The string `s` will be converted to octets (bytes) using this character encoding. 53 | * @return An encoded string in the US-ASCII character set. 54 | */ 55 | def encodePathSegment(s: String, inputCharset: String): String = { 56 | val in = s.getBytes(inputCharset) 57 | val out = new ByteArrayOutputStream() 58 | for (b <- in) { 59 | val allowed = segmentChars.get(b & 0xff) 60 | if (allowed) { 61 | out.write(b) 62 | } else { 63 | out.write('%') 64 | out.write(upperHex((b >> 4) & 0xF)) 65 | out.write(upperHex(b & 0xF)) 66 | } 67 | } 68 | out.toString("US-ASCII") 69 | } 70 | 71 | /** 72 | * Encode a string so that it can be used safely in the "path segment" part of a URI. 73 | * 74 | * @param s The string to encode. 75 | * @param inputCharset The charset of the encoding that the string `s` is encoded with. 76 | * @return An encoded string in the US-ASCII character set. 77 | */ 78 | def encodePathSegment(s: String, inputCharset: Charset): String = { 79 | encodePathSegment(s, inputCharset.name) 80 | } 81 | 82 | /** 83 | * Decode a string according to the rules for the "path segment" 84 | * part of a URI. A path segment is defined in RFC 3986. In a URI such 85 | * as `http://www.example.com/abc/def?a=1&b=2` both `abc` and `def` 86 | * are path segments. 87 | * 88 | * Path segment encoding differs from encoding for other parts of a URI. 89 | * For example, the "&" character is permitted in a path segment, but 90 | * has special meaning in query parameters. On the other hand, the "/" 91 | * character cannot appear in a path segment, as it is the path delimiter, 92 | * so it must be encoded as "%2F". These are just two examples of the 93 | * differences between path segment and query string encoding; there are 94 | * other differences too. 95 | * 96 | * When decoding path segments the `decodePathSegment` method should always 97 | * be used in preference to the [[$javadoc/java/net/URLDecoder.html java.net.URLDecoder.decode]] 98 | * method. `URLDecoder.decode`, despite its name, actually decodes 99 | * the `application/x-www-form-urlencoded` MIME format which is the encoding 100 | * used for form data in HTTP GET and POST requests. This format is suitable 101 | * for inclusion in the query part of a URI. But `URLDecoder.decoder` should not 102 | * be used for path segment encoding or decoding. 103 | * 104 | * @param s The string to decode. Must use the US-ASCII character set. 105 | * @param outputCharset The name of the encoding that the output should be encoded with. 106 | * The output string will be converted from octets (bytes) using this character encoding. 107 | * @throws play.utils.InvalidUriEncodingException If the input is not a valid encoded path segment. 108 | * @return A decoded string in the `outputCharset` character set. 109 | */ 110 | def decodePathSegment(s: String, outputCharset: String): String = { 111 | val in = s.getBytes("US-ASCII") 112 | val out = new ByteArrayOutputStream() 113 | var inPos = 0 114 | def next(): Int = { 115 | val b = in(inPos) & 0xFF 116 | inPos += 1 117 | b 118 | } 119 | while (inPos < in.length) { 120 | val b = next() 121 | if (b == '%') { 122 | // Read high digit 123 | if (inPos >= in.length) throw new InvalidUriEncodingException(s"Cannot decode $s: % at end of string") 124 | val high = fromHex(next()) 125 | if (high == -1) 126 | throw new InvalidUriEncodingException(s"Cannot decode $s: expected hex digit at position $inPos.") 127 | // Read low digit 128 | if (inPos >= in.length) 129 | throw new InvalidUriEncodingException(s"Cannot decode $s: incomplete percent encoding at end of string") 130 | val low = fromHex(next()) 131 | if (low == -1) 132 | throw new InvalidUriEncodingException(s"Cannot decode $s: expected hex digit at position $inPos.") 133 | // Write decoded byte 134 | out.write((high << 4) + low) 135 | } else if (segmentChars.get(b)) { 136 | // This character is allowed 137 | out.write(b) 138 | } else { 139 | throw new InvalidUriEncodingException(s"Cannot decode $s: illegal character at position $inPos.") 140 | } 141 | } 142 | out.toString(outputCharset) 143 | } 144 | 145 | /** 146 | * Decode a string according to the rules for the "path segment" part of a URI. 147 | * 148 | * @param s The string to decode. Must use the US-ASCII character set. 149 | * @param outputCharset The charset of the encoding that the output should be encoded with. 150 | * The output string will be converted from octets (bytes) using this character encoding. 151 | * @throws play.utils.InvalidUriEncodingException If the input is not a valid encoded path segment. 152 | * @return A decoded string in the `outputCharset` character set. 153 | */ 154 | def decodePathSegment(s: String, outputCharset: Charset): String = { 155 | decodePathSegment(s, outputCharset.name) 156 | } 157 | 158 | /** 159 | * Decode the path of a URI. Each path segment will be decoded 160 | * using the same rules as ``decodePathSegment``. No normalization is performed: 161 | * leading, trailing and duplicated slashes, if present are left as they are and 162 | * if absent remain absent; dot-segments (".." and ".") are ignored. 163 | * 164 | * Encoded slash characters are will appear as slashes in the output, thus "a/b" 165 | * will be indistinguishable from "a%2Fb". 166 | * 167 | * @param s The string to decode. Must use the US-ASCII character set. 168 | * @param outputCharset The name of the encoding that the output should be encoded with. 169 | * The output string will be converted from octets (bytes) using this character encoding. 170 | * @throws play.utils.InvalidUriEncodingException If the input is not a valid encoded path. 171 | * @return A decoded string in the `outputCharset` character set. 172 | */ 173 | def decodePath(s: String, outputCharset: String): String = { 174 | // Note: Could easily expose a method to return the decoded path as a Seq[String]. 175 | // This would allow better handling of paths segments with encoded slashes in them. 176 | // However, there is no need for this yet, so the method hasn't been added yet. 177 | splitString(s, '/').map(decodePathSegment(_, outputCharset)).mkString("/") 178 | } 179 | 180 | /** 181 | * Decode the path of a URI. Each path segment will be decoded 182 | * using the same rules as ``decodePathSegment``. 183 | * 184 | * @param s The string to decode. Must use the US-ASCII character set. 185 | * @param outputCharset The charset of the encoding that the output should be encoded with. 186 | * The output string will be converted from octets (bytes) using this character encoding. 187 | * @throws play.utils.InvalidUriEncodingException If the input is not a valid encoded path. 188 | * @return A decoded string in the `outputCharset` character set. 189 | */ 190 | def decodePath(s: String, outputCharset: Charset): String = { 191 | decodePath(s, outputCharset.name) 192 | } 193 | 194 | // RFC 3986, 3.3. Path 195 | // segment = *pchar 196 | // segment-nz = 1*pchar 197 | // segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) 198 | // ; non-zero-length segment without any colon ":" 199 | /** The set of ASCII character codes that are allowed in a URI path segment. */ 200 | private val segmentChars: AsciiBitSet = pchar.toBitSet 201 | 202 | /** The characters allowed in a path segment; defined in RFC 3986 */ 203 | private def pchar: AsciiSet = { 204 | // RFC 3986, 2.3. Unreserved Characters 205 | // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 206 | val unreserved = AsciiSet.Sets.AlphaDigit ||| AsciiSet('-', '.', '_', '~') 207 | 208 | // RFC 3986, 2.2. Reserved Characters 209 | // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 210 | // / "*" / "+" / "," / ";" / "=" 211 | val subDelims = AsciiSet('!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=') 212 | 213 | // RFC 3986, 3.3. Path 214 | // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 215 | unreserved ||| subDelims ||| AsciiSet(':', '@') 216 | } 217 | 218 | /** 219 | * Given a number from 0 to 16, return the ASCII character code corresponding 220 | * to its uppercase hexadecimal representation. 221 | */ 222 | private def upperHex(x: Int): Int = { 223 | // Assume 0 <= x < 16 224 | if (x < 10) (x + '0') else (x - 10 + 'A') 225 | } 226 | 227 | /** 228 | * Given the ASCII value of a character, return its value as a hex digit. 229 | * If the character isn't a valid hex digit, return -1 instead. 230 | */ 231 | private def fromHex(b: Int): Int = { 232 | if (b >= '0' && b <= '9') { 233 | b - '0' 234 | } else if (b >= 'A' && b <= 'Z') { 235 | 10 + b - 'A' 236 | } else if (b >= 'a' && b <= 'z') { 237 | 10 + b - 'a' 238 | } else { 239 | -1 240 | } 241 | } 242 | 243 | /** 244 | * Split a string on a character. Similar to `String.split` except, for this method, 245 | * the invariant {{{splitString(s, '/').mkString("/") == s}}} holds. 246 | * 247 | * For example: 248 | * {{{ 249 | * splitString("//a//", '/') == Seq("", "", "a", "", "") 250 | * String.split("//a//", '/') == Seq("", "", "a") 251 | * }}} 252 | */ 253 | private[utils] def splitString(s: String, c: Char): Seq[String] = { 254 | val result = scala.collection.immutable.List.newBuilder[String] 255 | import scala.annotation.tailrec 256 | @tailrec 257 | def splitLoop(start: Int): Unit = 258 | if (start < s.length) { 259 | var end = s.indexOf(c, start) 260 | if (end == -1) { 261 | result += s.substring(start) 262 | } else { 263 | result += s.substring(start, end) 264 | splitLoop(end + 1) 265 | } 266 | } else if (start == s.length) { 267 | result += "" 268 | } 269 | splitLoop(0) 270 | result.result() 271 | } 272 | } 273 | 274 | /** 275 | * An error caused by processing a value that isn't encoded correctly. 276 | */ 277 | class InvalidUriEncodingException(msg: String) extends RuntimeException(msg) 278 | -------------------------------------------------------------------------------- /lagomjs-client-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/client/ServiceClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.scaladsl.client 6 | 7 | import java.io.File 8 | 9 | import akka.actor.ActorSystem 10 | import akka.actor.CoordinatedShutdown 11 | import akka.stream.ActorMaterializer 12 | import akka.stream.Materializer 13 | import com.lightbend.lagom.internal.client.CircuitBreakerMetricsProviderImpl 14 | import com.lightbend.lagom.internal.client.WebSocketClientConfig 15 | import com.lightbend.lagom.internal.scaladsl.api.broker.TopicFactoryProvider 16 | import com.lightbend.lagom.internal.scaladsl.client.ScaladslClientMacroImpl 17 | import com.lightbend.lagom.internal.scaladsl.client.ScaladslServiceClient 18 | import com.lightbend.lagom.internal.scaladsl.client.ScaladslServiceResolver 19 | import com.lightbend.lagom.internal.scaladsl.client.ScaladslWebSocketClient 20 | import com.lightbend.lagom.internal.spi.CircuitBreakerMetricsProvider 21 | import com.lightbend.lagom.scaladsl.api._ 22 | import com.lightbend.lagom.scaladsl.api.broker.Topic 23 | import com.lightbend.lagom.scaladsl.api.deser.DefaultExceptionSerializer 24 | import com.lightbend.lagom.scaladsl.api.deser.ExceptionSerializer 25 | import play.api.Configuration 26 | import play.api.Environment 27 | import play.api.Mode 28 | 29 | import scala.collection.immutable 30 | import scala.concurrent.ExecutionContext 31 | import scala.language.experimental.macros 32 | 33 | /** 34 | * The Lagom service client implementor. 35 | * 36 | * Instances of this must also implement [[ServiceClientConstructor]], so that the `implementClient` macro can 37 | * generate code that constructs the service client. 38 | */ 39 | trait ServiceClient { self: ServiceClientConstructor => 40 | 41 | /** 42 | * Implement a client for the given service descriptor. 43 | */ 44 | def implement[S <: Service]: S = macro ScaladslClientMacroImpl.implementClient[S] 45 | } 46 | 47 | /** 48 | * Lagom service client constructor. 49 | * 50 | * This API should not be used directly, it will be invoked by the client generated by [[ServiceClient.implement]] in 51 | * order to construct the client and obtain the dependencies necessary for the client to operate. 52 | * 53 | * The reason for a separation between this interface and [[ServiceClient]] is so that the [[#construct]] method 54 | * doesn't appear on the user facing [[ServiceClient]] API. The macro it generates will cast the [[ServiceClient]] to 55 | * a [[ServiceClientConstructor]] in order to invoke it. 56 | * 57 | * Although this API should not directly be used by end users, the code generated by the [[ServiceClient]] macro does 58 | * cause end users to have a binary dependency on this class, which is why it's in the `scaladsl` package. 59 | */ 60 | trait ServiceClientConstructor extends ServiceClient { 61 | 62 | /** 63 | * Construct a service client, by invoking the passed in function that takes the implementation context. 64 | */ 65 | def construct[S <: Service](constructor: ServiceClientImplementationContext => S): S 66 | } 67 | 68 | /** 69 | * The service client implementation context. 70 | * 71 | * This API should not be used directly, it will be invoked by the client generated by [[ServiceClient.implement]] in 72 | * order to resolve the service descriptor. 73 | * 74 | * The purpose of this API is to capture the dependencies required in order to implement a service client, such as the 75 | * HTTP and WebSocket clients. 76 | * 77 | * Although this API should not directly be used by end users, the code generated by the [[ServiceClient]] macro does 78 | * cause end users to have a binary dependency on this class, which is why it's in the `scaladsl` package. 79 | */ 80 | trait ServiceClientImplementationContext { 81 | 82 | /** 83 | * Resolve the given descriptor to a service client context. 84 | */ 85 | def resolve(descriptor: Descriptor): ServiceClientContext 86 | } 87 | 88 | /** 89 | * The service client context. 90 | * 91 | * This API should not be used directly, it will be invoked by the client generated by [[ServiceClient.implement]] in 92 | * order to implement each service call and topic. 93 | * 94 | * The service client context is essentially a map of service calls and topics, constructed from a service descriptor, 95 | * that allows a [[ServiceCall]] to be easily constructed by the services methods. 96 | * 97 | * Although this API should not directly be used by end users, the code generated by the [[ServiceClient]] macro does 98 | * cause end users to have a binary dependency on this class, which is why it's in the `scaladsl` package. 99 | */ 100 | trait ServiceClientContext { 101 | 102 | /** 103 | * Create a service call for the given method name and passed in parameters. 104 | */ 105 | def createServiceCall[Request, Response]( 106 | methodName: String, 107 | params: immutable.Seq[Any] 108 | ): ServiceCall[Request, Response] 109 | 110 | /** 111 | * Create a topic for the given method name. 112 | */ 113 | def createTopic[Message](methodName: String): Topic[Message] 114 | } 115 | 116 | trait ServiceResolver { 117 | def resolve(descriptor: Descriptor): Descriptor 118 | } 119 | 120 | /** 121 | * The Lagom service client components. 122 | */ 123 | trait LagomServiceClientComponents extends TopicFactoryProvider { self: LagomConfigComponent => 124 | 125 | def serviceInfo: ServiceInfo 126 | def serviceLocator: ServiceLocator 127 | def materializer: Materializer 128 | def actorSystem: ActorSystem 129 | def executionContext: ExecutionContext 130 | def environment: Environment 131 | 132 | lazy val circuitBreakerMetricsProvider: CircuitBreakerMetricsProvider = new CircuitBreakerMetricsProviderImpl( 133 | actorSystem 134 | ) 135 | 136 | lazy val serviceResolver: ServiceResolver = new ScaladslServiceResolver(defaultExceptionSerializer) 137 | lazy val defaultExceptionSerializer: ExceptionSerializer = new DefaultExceptionSerializer(environment) 138 | 139 | lazy val scaladslWebSocketClient: ScaladslWebSocketClient = 140 | new ScaladslWebSocketClient(WebSocketClientConfig(config))(executionContext, materializer) 141 | lazy val serviceClient: ServiceClient = new ScaladslServiceClient( 142 | scaladslWebSocketClient, 143 | serviceInfo, 144 | serviceLocator, 145 | serviceResolver, 146 | optionalTopicFactory 147 | )(executionContext, materializer) 148 | } 149 | 150 | /** 151 | * Convenience for constructing service clients in a non Lagom server application. 152 | * 153 | * It is important to invoke [[#stop]] when the application is no longer needed, as this will trigger the shutdown 154 | * of all thread and connection pools. 155 | */ 156 | @deprecated(message = "Use StandaloneLagomClientFactory instead", since = "1.4.9") 157 | abstract class LagomClientApplication( 158 | clientName: String, 159 | classLoader: ClassLoader = new ClassLoader() {} 160 | ) extends StandaloneLagomClientFactory(clientName, classLoader) 161 | 162 | /** 163 | * Convenience for constructing service clients in a non Lagom server application. 164 | * 165 | * A [[StandaloneLagomClientFactory]] should be used only if your application does NOT have its own [[akka.actor.ActorSystem]], in which 166 | * this standalone factory will create and manage an [[akka.actor.ActorSystem]] and Akka Streams [[akka.stream.Materializer]]. 167 | * 168 | * It is important to invoke [[StandaloneLagomClientFactory#stop()]] when the application is no longer needed, 169 | * as this will trigger the shutdown of the underlying [[akka.actor.ActorSystem]] and Akka Streams [[akka.stream.Materializer]] 170 | * releasing all thread and connection pools in use by the clients. 171 | * 172 | * There is one more component that you’ll need to provide when creating a client application, that is a service locator. 173 | * It is up to you what service locator you use, it could be a third party service locator, or a service locator created 174 | * from static configuration. 175 | * 176 | * Lagom provides a number of built-in service locators, including a [[StaticServiceLocator]], a [[RoundRobinServiceLocator]] 177 | * and a [[ConfigurationServiceLocator]]. The easiest way to use these is to mix in their respective Components traits. 178 | * 179 | * For example, here’s a client application built using the static service locator, which uses a static URI: 180 | * {{{ 181 | * import java.net.URI 182 | * import com.lightbend.lagom.scaladsl.client._ 183 | * import play.api.libs.ws.ahc.AhcWSComponents 184 | * 185 | * val clientApplication = new StandaloneLagomClientFactory("my-client") 186 | * with StaticServiceLocatorComponents 187 | * with AhcWSComponents { 188 | * 189 | * override def staticServiceUri = URI.create("http://localhost:8080") 190 | * } 191 | * }}} 192 | * 193 | * 194 | * @param clientName The name of the service that is consuming the Lagom service. This will impact how calls made through clients 195 | * generated by this factory will identify themselves. 196 | * @param classLoader A classloader, it will be used to create the service proxy and needs to have the API for the client in it. 197 | */ 198 | abstract class StandaloneLagomClientFactory( 199 | clientName: String, 200 | classLoader: ClassLoader = new ClassLoader() {} 201 | ) extends LagomClientFactory(clientName, classLoader) { 202 | // TODO: create compatibility layer for ActorSystemProvider? 203 | override lazy val actorSystem: ActorSystem = ActorSystem("default", configuration.underlying, environment.classLoader) 204 | lazy val coordinatedShutdown: CoordinatedShutdown = CoordinatedShutdown(actorSystem) 205 | override lazy val materializer: Materializer = ActorMaterializer.create(actorSystem) 206 | 207 | /** 208 | * Stop this [[LagomClientFactory]] by shutting down the internal [[akka.actor.ActorSystem]] and Akka Streams [[akka.stream.Materializer]]. 209 | * 210 | * The stop is executed asynchronously because there is no blocking in JavaScript. Unlike standard Lagom this method 211 | * does not try to wait for the stop to complete. 212 | */ 213 | override def stop(): Unit = { 214 | coordinatedShutdown.run(ClientStoppedReason) 215 | } 216 | } 217 | 218 | case object ClientStoppedReason extends CoordinatedShutdown.Reason 219 | 220 | /** 221 | * Convenience for constructing service clients in a non Lagom server application. 222 | * 223 | * [[LagomClientFactory]] should be used only if your application DO have its own [[akka.actor.ActorSystem]] and Akka Streams [[akka.stream.Materializer]], 224 | * in which case you should reuse then when building a [[LagomClientFactory]]. 225 | * 226 | * The easiest way to reuse your existing [[akka.actor.ActorSystem]] and Akka Stream [[akka.stream.Materializer]] is to extend the [[LagomClientFactory]] 227 | * and add a constructor where you can pass them as arguments (see example below). 228 | * 229 | * There is one more component that you’ll need to provide when creating a [[LagomClientFactory]], that is a service locator. 230 | * It is up to you what service locator you use, it could be a third party service locator, or a service locator created 231 | * from static configuration. 232 | * 233 | * Lagom provides a number of built-in service locators, including a [[StaticServiceLocator]], a [[RoundRobinServiceLocator]] 234 | * and a [[ConfigurationServiceLocator]]. The easiest way to use these is to mix in their respective Components traits. 235 | * 236 | * For example, here’s a client factory built using the static service locator, which uses a static URI, 237 | * and reusing an [[akka.actor.ActorSystem]] and Akka Streams [[akka.stream.Materializer]] created outside it: 238 | * 239 | * {{{ 240 | * import java.net.URI 241 | * import com.lightbend.lagom.scaladsl.client._ 242 | * import play.api.libs.ws.ahc.AhcWSComponents 243 | * 244 | * class MyLagomClientFactory(val actorSystem: ActorSystem, val materialzer: Materializer) 245 | * extends LagomClientFactory("my-client") 246 | * with StaticServiceLocatorComponents 247 | * with AhcWSComponents { 248 | * 249 | * override def staticServiceUri = URI.create("http://localhost:8080") 250 | * } 251 | * 252 | * 253 | * val actorSystem = ActorSystem("my-app") 254 | * val materializer = ActorMaterializer()(actorSystem) 255 | * val clientFactory = new MyLagomClientFactory(actorSystem, materializer) 256 | * }}} 257 | * 258 | * @param clientName The name of the service that is consuming the Lagom service. This will impact how calls made through clients 259 | * generated by this factory will identify themselves. 260 | * @param classLoader A classloader, it will be used to create the service proxy and needs to have the API for the client in it. 261 | */ 262 | abstract class LagomClientFactory( 263 | clientName: String, 264 | classLoader: ClassLoader = new ClassLoader() {} 265 | ) extends LagomServiceClientComponents 266 | with LagomConfigComponent { 267 | override lazy val serviceInfo: ServiceInfo = ServiceInfo(clientName, immutable.Seq.empty) 268 | override lazy val environment: Environment = Environment(new File("."), classLoader, Mode.Prod) 269 | 270 | lazy val configuration: Configuration = Configuration.load(Map.empty, lagomjs.Config.default) 271 | override lazy val executionContext: ExecutionContext = actorSystem.dispatcher 272 | 273 | /** 274 | * Override this method if your [[LagomClientFactory]] implementation needs to free any resource. 275 | * 276 | * For example, when implementing your own [[LagomClientFactory]], you may choose to reuse an existing [[akka.actor.ActorSystem]], 277 | * but use a internal [[akka.stream.Materializer]]. In which case, you can use this method to only shutdown the [[akka.stream.Materializer]]. 278 | */ 279 | def stop(): Unit = {} 280 | } 281 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/ClientServiceCallInvoker.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.internal.client 6 | 7 | import java.net.URI 8 | import java.nio.ByteBuffer 9 | 10 | import akka.NotUsed 11 | import akka.stream.Materializer 12 | import akka.stream.scaladsl.Sink 13 | import akka.stream.scaladsl.Source 14 | import akka.util.ByteString 15 | import com.lightbend.lagom.internal.api.HeaderUtils 16 | import com.lightbend.lagom.internal.api.transport.LagomServiceApiBridge 17 | import org.scalajs.dom.XMLHttpRequest 18 | import org.scalajs.dom.ext.Ajax 19 | import org.scalajs.dom.ext.AjaxException 20 | import play.api.http.HeaderNames 21 | import play.api.libs.streams.AkkaStreams 22 | 23 | import scala.concurrent.ExecutionContext 24 | import scala.concurrent.Future 25 | import scala.scalajs.js.URIUtils 26 | 27 | private[lagom] abstract class ClientServiceCallInvoker[Request, Response]( 28 | serviceName: String, 29 | path: String, 30 | queryParams: Map[String, Seq[String]] 31 | )(implicit ec: ExecutionContext, mat: Materializer) 32 | extends LagomServiceApiBridge { 33 | 34 | import ClientServiceCallInvoker._ 35 | 36 | val descriptor: Descriptor 37 | val serviceLocator: ServiceLocator 38 | val call: Call[Request, Response] 39 | def headerFilter: HeaderFilter = descriptorHeaderFilter(descriptor) 40 | 41 | def doInvoke( 42 | request: Request, 43 | requestHeaderHandler: RequestHeader => RequestHeader 44 | ): Future[(ResponseHeader, Response)] = { 45 | serviceLocatorDoWithService( 46 | serviceLocator, 47 | descriptor, 48 | call, 49 | uri => { 50 | val queryString = if (queryParams.nonEmpty) { 51 | queryParams 52 | .flatMap { 53 | case (name, values) => 54 | values.map(value => URIUtils.encodeURIComponent(name) + "=" + URIUtils.encodeURIComponent(value)) 55 | } 56 | .mkString("?", "&", "") 57 | } else "" 58 | val url = s"$uri$path$queryString" 59 | 60 | val method = methodForCall(call) 61 | 62 | val requestSerializer = callRequestSerializer(call) 63 | val serializer = messageSerializerSerializerForRequest[Request, Nothing](requestSerializer) 64 | val responseSerializer = callResponseSerializer(call) 65 | 66 | val requestHeader = requestHeaderHandler( 67 | newRequestHeader( 68 | method, 69 | URI.create(url), 70 | negotiatedSerializerProtocol(serializer), 71 | messageSerializerAcceptResponseProtocols(responseSerializer), 72 | Option(newServicePrincipal(serviceName)), 73 | Map.empty 74 | ) 75 | ) 76 | 77 | val requestSerializerStreamed = messageSerializerIsStreamed(requestSerializer) 78 | val responseSerializerStreamed = messageSerializerIsStreamed(responseSerializer) 79 | 80 | val result: Future[(ResponseHeader, Response)] = 81 | (requestSerializerStreamed, responseSerializerStreamed) match { 82 | case (false, false) => 83 | makeStrictCall( 84 | headerFilterTransformClientRequest(headerFilter, requestHeader), 85 | requestSerializer.asInstanceOf[MessageSerializer[Request, ByteString]], 86 | responseSerializer.asInstanceOf[MessageSerializer[Response, ByteString]], 87 | request 88 | ) 89 | 90 | case (false, true) => 91 | makeStreamedResponseCall( 92 | headerFilterTransformClientRequest(headerFilter, requestHeader), 93 | requestSerializer.asInstanceOf[MessageSerializer[Request, ByteString]], 94 | responseSerializer.asInstanceOf[MessageSerializer[Response, AkkaStreamsSource[ByteString, NotUsed]]], 95 | request 96 | ) 97 | 98 | case (true, false) => 99 | makeStreamedRequestCall( 100 | headerFilterTransformClientRequest(headerFilter, requestHeader), 101 | requestSerializer.asInstanceOf[MessageSerializer[Request, AkkaStreamsSource[ByteString, NotUsed]]], 102 | responseSerializer.asInstanceOf[MessageSerializer[Response, ByteString]], 103 | request 104 | ) 105 | 106 | case (true, true) => 107 | makeStreamedCall( 108 | headerFilterTransformClientRequest(headerFilter, requestHeader), 109 | requestSerializer.asInstanceOf[MessageSerializer[Request, AkkaStreamsSource[ByteString, NotUsed]]], 110 | responseSerializer.asInstanceOf[MessageSerializer[Response, AkkaStreamsSource[ByteString, NotUsed]]], 111 | request 112 | ) 113 | } 114 | 115 | result 116 | } 117 | ).map { 118 | case Some(response) => response 119 | case None => 120 | throw new IllegalStateException(s"Service ${descriptorName(descriptor)} was not found by service locator") 121 | } 122 | } 123 | 124 | /** 125 | * A call that has a strict request and a streamed response. 126 | * 127 | * Currently implemented using a WebSocket, and sending the request as the first and only message. 128 | */ 129 | private def makeStreamedResponseCall( 130 | requestHeader: RequestHeader, 131 | requestSerializer: MessageSerializer[Request, ByteString], 132 | responseSerializer: MessageSerializer[_, AkkaStreamsSource[ByteString, NotUsed]], 133 | request: Request 134 | ): Future[(ResponseHeader, Response)] = { 135 | val serializer = messageSerializerSerializerForRequest[Request, ByteString](requestSerializer) 136 | 137 | // We have a single source, followed by a maybe source (that is, a source that never produces any message, and 138 | // never terminates). The maybe source is necessary because we want the response stream to stay open. 139 | val requestAsStream = 140 | if (messageSerializerIsUsed(requestSerializer)) { 141 | Source.single(negotiatedSerializerSerialize(serializer, request)).concat(Source.maybe) 142 | } else { 143 | // If it's not used, don't send any message 144 | Source.maybe[ByteString].mapMaterializedValue(_ => NotUsed) 145 | } 146 | 147 | doMakeStreamedCall(requestAsStream, serializer, requestHeader).map( 148 | (deserializeResponseStream(responseSerializer, requestHeader) _).tupled 149 | ) 150 | } 151 | 152 | /** 153 | * A call that has a streamed request and a strict response. 154 | * 155 | * Currently implemented using a WebSocket, that converts the first message received to the strict message. If no 156 | * message is received, it assumes the response is an empty message. 157 | */ 158 | private def makeStreamedRequestCall( 159 | requestHeader: RequestHeader, 160 | requestSerializer: MessageSerializer[_, AkkaStreamsSource[ByteString, NotUsed]], 161 | responseSerializer: MessageSerializer[Response, ByteString], 162 | request: Request 163 | ): Future[(ResponseHeader, Response)] = { 164 | val negotiatedSerializer = messageSerializerSerializerForRequest( 165 | requestSerializer 166 | .asInstanceOf[MessageSerializer[AkkaStreamsSource[Any, NotUsed], AkkaStreamsSource[ByteString, NotUsed]]] 167 | ) 168 | val requestStream = 169 | negotiatedSerializerSerialize(negotiatedSerializer, request.asInstanceOf[AkkaStreamsSource[Any, NotUsed]]) 170 | 171 | for { 172 | (transportResponseHeader, responseStream) <- doMakeStreamedCall( 173 | akkaStreamsSourceAsScala(requestStream), 174 | negotiatedSerializer, 175 | requestHeader 176 | ) 177 | // We want to take the first element (if it exists), and then ignore all subsequent elements. Ignoring, rather 178 | // than cancelling the stream, is important, because this is a WebSocket connection, we want the upstream to 179 | // still remain open, but if we cancel the stream, the upstream will disconnect too. 180 | maybeResponse <- responseStream.via(AkkaStreams.ignoreAfterCancellation).runWith(Sink.headOption) 181 | } yield { 182 | val bytes = maybeResponse.getOrElse(ByteString.empty) 183 | val responseHeader = headerFilterTransformClientResponse(headerFilter, transportResponseHeader, requestHeader) 184 | val deserializer = messageSerializerDeserializer(responseSerializer, messageHeaderProtocol(responseHeader)) 185 | responseHeader -> negotiatedDeserializerDeserialize(deserializer, bytes) 186 | } 187 | } 188 | 189 | /** 190 | * A call that is streamed in both directions. 191 | */ 192 | private def makeStreamedCall( 193 | requestHeader: RequestHeader, 194 | requestSerializer: MessageSerializer[_, AkkaStreamsSource[ByteString, NotUsed]], 195 | responseSerializer: MessageSerializer[_, AkkaStreamsSource[ByteString, NotUsed]], 196 | request: Request 197 | ): Future[(ResponseHeader, Response)] = { 198 | val negotiatedSerializer = messageSerializerSerializerForRequest( 199 | requestSerializer 200 | .asInstanceOf[MessageSerializer[AkkaStreamsSource[Any, NotUsed], AkkaStreamsSource[ByteString, NotUsed]]] 201 | ) 202 | val requestStream = 203 | negotiatedSerializerSerialize(negotiatedSerializer, request.asInstanceOf[AkkaStreamsSource[Any, NotUsed]]) 204 | 205 | doMakeStreamedCall(akkaStreamsSourceAsScala(requestStream), negotiatedSerializer, requestHeader).map( 206 | (deserializeResponseStream(responseSerializer, requestHeader) _).tupled 207 | ) 208 | } 209 | 210 | private def deserializeResponseStream( 211 | responseSerializer: MessageSerializer[_, AkkaStreamsSource[ByteString, NotUsed]], 212 | requestHeader: RequestHeader 213 | )(transportResponseHeader: ResponseHeader, response: Source[ByteString, NotUsed]): (ResponseHeader, Response) = { 214 | val responseHeader = headerFilterTransformClientResponse(headerFilter, transportResponseHeader, requestHeader) 215 | 216 | val deserializer = messageSerializerDeserializer( 217 | responseSerializer 218 | .asInstanceOf[MessageSerializer[AkkaStreamsSource[Any, NotUsed], AkkaStreamsSource[ByteString, NotUsed]]], 219 | messageHeaderProtocol(responseHeader) 220 | ) 221 | responseHeader -> negotiatedDeserializerDeserialize(deserializer, toAkkaStreamsSource(response)) 222 | .asInstanceOf[Response] 223 | } 224 | 225 | protected def doMakeStreamedCall( 226 | requestStream: Source[ByteString, NotUsed], 227 | requestSerializer: NegotiatedSerializer[_, _], 228 | requestHeader: RequestHeader 229 | ): Future[(ResponseHeader, Source[ByteString, NotUsed])] 230 | 231 | /** 232 | * A call that is strict in both directions. 233 | */ 234 | private def makeStrictCall( 235 | requestHeader: RequestHeader, 236 | requestSerializer: MessageSerializer[Request, ByteString], 237 | responseSerializer: MessageSerializer[Response, ByteString], 238 | request: Request 239 | ): Future[(ResponseHeader, Response)] = { 240 | 241 | val url = requestHeaderUri(requestHeader).toString 242 | val method = requestHeaderMethod(requestHeader) 243 | 244 | val headerFilter = descriptorHeaderFilter(descriptor) 245 | val transportRequestHeader = headerFilterTransformClientRequest(headerFilter, requestHeader) 246 | 247 | val contentTypeHeader = 248 | messageProtocolToContentTypeHeader(messageHeaderProtocol(transportRequestHeader)).toSeq 249 | .map(HeaderNames.CONTENT_TYPE -> _) 250 | val requestHeaders = messageHeaderHeaders(transportRequestHeader).toSeq.collect { 251 | case (_, values) if values.nonEmpty => values.head._1 -> values.map(_._2).mkString(", ") 252 | } 253 | val acceptHeader = { 254 | val accept = requestHeaderAcceptedResponseProtocols(requestHeader) 255 | .flatMap { accept => 256 | messageProtocolToContentTypeHeader(accept) 257 | } 258 | .mkString(", ") 259 | if (accept.nonEmpty) Seq(HeaderNames.ACCEPT -> accept) 260 | else Nil 261 | } 262 | val headers = (contentTypeHeader ++ requestHeaders ++ acceptHeader).toMap 263 | 264 | val body = 265 | if (messageSerializerIsUsed(requestSerializer)) { 266 | val serializer = messageSerializerSerializerForRequest(requestSerializer) 267 | val body = negotiatedSerializerSerialize(serializer, request) 268 | Some(body) 269 | } else None 270 | 271 | // Remove User-Agent header because it's not allowed by XMLHttpRequest 272 | val filteredHeaders = headers - HeaderNames.USER_AGENT 273 | val data = body.map(_.asByteBuffer).getOrElse(ByteBuffer.wrap(Array[Byte]())) 274 | 275 | Ajax 276 | .apply( 277 | method = method, 278 | url = url, 279 | data = data, 280 | timeout = 0, 281 | headers = filteredHeaders, 282 | withCredentials = false, 283 | responseType = "" 284 | ) 285 | .recover({ 286 | case e: AjaxException if !e.isTimeout => e.xhr 287 | }) 288 | .map(xhr => { 289 | val status = xhr.status 290 | val headers = parseHeaders(xhr) 291 | val body = ByteString.fromString(Option(xhr.responseText).getOrElse("")) 292 | 293 | // Create the message header 294 | val contentTypeHeader = headers.get(HeaderNames.CONTENT_TYPE).map(_.mkString(", ")) 295 | val protocol = messageProtocolFromContentTypeHeader(contentTypeHeader) 296 | val responseHeaders = headers.map { 297 | case (key, values) => HeaderUtils.normalize(key) -> values.map(key -> _).toIndexedSeq 298 | } 299 | val transportResponseHeader = newResponseHeader(status, protocol, responseHeaders) 300 | val responseHeader = headerFilterTransformClientResponse(headerFilter, transportResponseHeader, requestHeader) 301 | 302 | if (responseHeaderStatus(responseHeader) >= 400 && responseHeaderStatus(responseHeader) <= 599) { 303 | throw exceptionSerializerDeserializeHttpException( 304 | descriptorExceptionSerializer(descriptor), 305 | responseHeaderStatus(responseHeader), 306 | protocol, 307 | body 308 | ) 309 | } else { 310 | val negotiatedDeserializer = 311 | messageSerializerDeserializer(responseSerializer, messageHeaderProtocol(responseHeader)) 312 | responseHeader -> negotiatedDeserializerDeserialize(negotiatedDeserializer, body) 313 | } 314 | }) 315 | } 316 | 317 | } 318 | 319 | private object ClientServiceCallInvoker { 320 | private val header = "(.*?):(.*)".r 321 | 322 | def parseHeaders(xhr: XMLHttpRequest): Map[String, Seq[String]] = { 323 | xhr 324 | .getAllResponseHeaders() 325 | .split("\r\n") 326 | .flatMap({ 327 | case header(key, values) => Some(key.trim -> values.split(",").map(_.trim).toSeq) 328 | case _ => None 329 | }) 330 | .toMap 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /lagomjs-client/js/src/main/scala/com/lightbend/lagom/internal/client/WebSocketStreamBuffer.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.lagom.internal.client 2 | 3 | import akka.stream.BufferOverflowException 4 | import akka.util.ByteString 5 | import com.lightbend.lagom.internal.client.WebSocketStreamBuffer._ 6 | import org.reactivestreams.Subscriber 7 | import org.reactivestreams.Subscription 8 | import org.scalajs.dom.WebSocket 9 | 10 | import scala.collection.mutable 11 | import scala.scalajs.js.typedarray.ArrayBuffer 12 | import scala.scalajs.js.typedarray.TypedArrayBuffer 13 | 14 | /** 15 | * Buffers elements received from a WebSocket for consumption by a reactive stream 16 | * 17 | * The buffer is implemented as a finite state machine for use in a JavaScript runtime. It prioritizes delivering 18 | * elements downstream immediately in response to incoming messages and downstream requests. This is to mitigate the 19 | * impact of fast sockets consuming the event-loop and preventing downstream consumption of messages. It enforces a 20 | * maximum number of queued elements and will fail the socket and stream if the maximum is exceeded. 21 | * 22 | * The buffer has seven states: Queueing, Full, Streaming, Draining, Completed, Cancelled, and Failed. 23 | * 24 | * The Queueing state is when the socket is open, the buffer will receive and queue messages, and no subscriber 25 | * has been attached. The Full state is when the socket is closed, the buffer will not queue more messages, and no 26 | * subscriber has been attached. The Streaming state is when the socket is open, the buffer will receive and queue 27 | * messages, a subscriber is attached, and messages are sent to the subscriber in response to its demand signals. The 28 | * Draining state is when the socket is closed, the buffer will not queue more messages, a subscriber is attached, and 29 | * messages are sent to the subscriber in response to its demand signals. The Completed state is when the socket is 30 | * closed, a subscriber is attached, all queued messages have been sent to the subscriber, and the subscriber is 31 | * completed. The Cancelled state is when a subscriber is attached and cancelled, causing the socket to be closed and 32 | * any queued messages to be discarded. The Failed state is when the buffer has failed for any reason, causing the 33 | * socket to be closed, the subscriber to be completed with an error, and any queued messages to be discarded. 34 | */ 35 | private[lagom] class WebSocketStreamBuffer( 36 | socket: WebSocket, 37 | bufferSize: Int, 38 | deserializeException: (Int, ByteString) => Throwable 39 | ) { 40 | // Start the buffer in the Queueing state 41 | private var state: State = Queueing(bufferSize)(this, socket) 42 | 43 | // Configure the socket to interact with the buffer 44 | 45 | // Add messages from the socket to the queue 46 | socket.onmessage = { message => 47 | // The message data should be either an ArrayBuffer or a String 48 | // It should not be a Blob because the socket binaryType was set 49 | val data = message.data match { 50 | case buffer: ArrayBuffer => ByteString.apply(TypedArrayBuffer.wrap(buffer)) 51 | case data => ByteString.fromString(data.toString) 52 | } 53 | 54 | this.enqueue(data) 55 | } 56 | 57 | // Fail the buffer and ultimately the stream with an error if the socket encounters an error 58 | socket.onerror = { event => 59 | this.error(new RuntimeException(s"WebSocket error: ${event.`type`}")) 60 | } 61 | 62 | // Complete the buffer and ultimately the stream when the socket closes based on the close code 63 | // The buffer will ensure that pending elements are delivered before downstream completion 64 | socket.onclose = { event => 65 | if (event.code == NormalClosure) { 66 | this.complete() 67 | } else { 68 | // Fail because the socket closed with an error 69 | // Parse the error reason as an exception 70 | val bytes = ByteString.fromString(event.reason) 71 | val exception = deserializeException(event.code, bytes) 72 | this.error(exception) 73 | } 74 | } 75 | 76 | // Delegate all operations to the current state 77 | 78 | def attach(subscriber: Subscriber[_ >: ByteString]): Unit = state.attach(subscriber) 79 | def addDemand(n: Long): Unit = state.addDemand(n) 80 | def cancel(): Unit = state.cancel() 81 | 82 | private def enqueue(message: ByteString): Unit = state.enqueue(message) 83 | private def complete(): Unit = state.complete() 84 | private def error(exception: Throwable): Unit = state.error(exception) 85 | 86 | // Internal method used by the state to transition to a new sate 87 | private def transition(next: State): Unit = { 88 | state = next 89 | } 90 | } 91 | 92 | private object WebSocketStreamBuffer { 93 | val NormalClosure = 1000 94 | val ApplicationFailureCode = 4000 95 | 96 | sealed trait State { 97 | 98 | /** 99 | * Add the given message to the queue 100 | * 101 | * @param message the message to add 102 | */ 103 | def enqueue(message: ByteString): Unit 104 | 105 | /** 106 | * Attach a reactive streams subscriber 107 | * 108 | * @param subscriber the subscriber 109 | */ 110 | def attach(subscriber: Subscriber[_ >: ByteString]): Unit 111 | 112 | /** 113 | * Add given demand to the outstanding demand 114 | * 115 | * It is called by the reactive streams Subscriber via its Subscription `request` method 116 | * 117 | * @param n the demand 118 | */ 119 | def addDemand(n: Long): Unit 120 | 121 | /** 122 | * Complete the buffer 123 | * 124 | * This indicates that no new elements will be added to the buffer. It is called when the socket closes 125 | * successfully. 126 | */ 127 | def complete(): Unit 128 | 129 | /** 130 | * Cancel the buffer 131 | * 132 | * It is called by the reactive streams Subscriber via its Subscription `cancel` method 133 | */ 134 | def cancel(): Unit 135 | 136 | /** 137 | * Error the buffer with the given exception 138 | * 139 | * This is called in a variety of error conditions, including when the socket closes with an error. 140 | * 141 | * @param exception the exception 142 | */ 143 | def error(exception: Throwable): Unit 144 | 145 | } 146 | 147 | /** 148 | * Provides reusable state method to check if adding an element would exceed the buffer size 149 | */ 150 | sealed trait QueueSize { 151 | def bufferSize: Int 152 | def queue: mutable.Queue[ByteString] 153 | 154 | /** 155 | * Check if adding an element would exceed the buffer size, if so returns `Some` exception, otherwise `None` 156 | * 157 | * The exception is not thrown in order to avoid exception overhead and because the exception must be passed to 158 | * the subscriber onError method. 159 | * 160 | * @return optional exception if the buffer size is exceeded 161 | */ 162 | protected def checkSize: Option[Throwable] = { 163 | if (queue.size >= bufferSize) Some(BufferOverflowException(s"Exceeded buffer size of $bufferSize")) 164 | else None 165 | } 166 | } 167 | 168 | /** 169 | * Provides reusable state method to fulfill subscriber demand 170 | */ 171 | sealed trait Fulfill { 172 | def queue: mutable.Queue[ByteString] 173 | def subscriber: Subscriber[_ >: ByteString] 174 | 175 | /** 176 | * Fulfill subscriber demand if possible 177 | * 178 | * Fulfill by removing up to demand number of elements elements from the buffer and sending them to the subscriber. 179 | * The remaining outstanding demand is returned based on how many elements were sent. 180 | * 181 | * @param demand current demand 182 | * @return remaining demand 183 | */ 184 | protected def fulfill(demand: Long): Long = { 185 | val num = Math.min(queue.size, demand) 186 | for (_ <- 1L to num) subscriber.onNext(queue.dequeue) 187 | 188 | demand - num 189 | } 190 | } 191 | 192 | /** 193 | * Provides reusable state method to cancel the buffer 194 | */ 195 | sealed trait Cancelable { 196 | def socket: WebSocket 197 | def buffer: WebSocketStreamBuffer 198 | 199 | def cancel(): Unit = { 200 | val cancelled = Cancelled()(socket) 201 | buffer.transition(cancelled) 202 | } 203 | } 204 | 205 | /** 206 | * State when the socket is open, the buffer will receive and queue messages, and no subscriber has been attached 207 | * 208 | * This state allows elements to be buffered before the stream is set up and a subscriber is attached. If the socket 209 | * closes and the buffer completes before a subscriber is attached then the state will transition to Full. 210 | * Otherwise, if a subscriber is attached first, the state will transition to Streaming. 211 | */ 212 | case class Queueing(bufferSize: Int)(implicit buffer: WebSocketStreamBuffer, socket: WebSocket) 213 | extends State 214 | with QueueSize { 215 | val queue: mutable.Queue[ByteString] = mutable.Queue.empty 216 | 217 | /** 218 | * Add message to the queue 219 | * 220 | * If adding the message would exceed the buffer size then the message is discarded and the buffer transitions to 221 | * the Failed state. 222 | */ 223 | override def enqueue(message: ByteString): Unit = { 224 | checkSize.fold[Unit]({ 225 | queue.enqueue(message) 226 | })(exception => { 227 | val failed = Failed(exception) 228 | buffer.transition(failed) 229 | }) 230 | } 231 | 232 | /** 233 | * Attach the given subscriber and transition to the Streaming state 234 | */ 235 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = { 236 | val streaming = Streaming(queue, bufferSize, PreparedSubscriber(subscriber)) 237 | buffer.transition(streaming) 238 | } 239 | 240 | /** 241 | * Transition to the Failed state because this operation is not supported when no subscriber is attached 242 | */ 243 | override def addDemand(n: Long): Unit = { 244 | val exception = new UnsupportedOperationException("No subscriber is attached") 245 | val failed = Failed(exception) 246 | buffer.transition(failed) 247 | } 248 | 249 | /** 250 | * Transition to the Full state 251 | * 252 | * This indicates that no new elements will be added to the buffer. Since the buffer was completed before a 253 | * subscriber was attached the buffer contains every message. 254 | */ 255 | override def complete(): Unit = { 256 | val full = Full(queue) 257 | buffer.transition(full) 258 | } 259 | 260 | /** 261 | * Transition to the Failed state because this operation is not supported when no subscriber is attached 262 | */ 263 | override def cancel(): Unit = { 264 | val exception = new UnsupportedOperationException("No subscriber is attached") 265 | val failed = Failed(exception) 266 | buffer.transition(failed) 267 | } 268 | 269 | /** 270 | * Error the buffer by transitioning to the Failed state with the given exception 271 | */ 272 | override def error(exception: Throwable): Unit = { 273 | val failed = Failed(exception) 274 | buffer.transition(failed) 275 | } 276 | } 277 | 278 | /** 279 | * State when the socket is closed, the buffer will not queue more messages, and no subscriber has been attached 280 | * 281 | * When a subscriber is attached the state will transition to Draining to start emptying the queue by sending 282 | * elements to the subscriber in response to its demand signals. The queue contains all messages ever sent by the 283 | * socket because the socket closed and the buffer was completed before a subscriber was attached. The queue may 284 | * be empty. 285 | */ 286 | case class Full(queue: mutable.Queue[ByteString])(implicit buffer: WebSocketStreamBuffer, socket: WebSocket) 287 | extends State { 288 | 289 | /** 290 | * Transition to the Failed state because this operation is not supported after the buffer is completed 291 | */ 292 | override def enqueue(message: ByteString): Unit = { 293 | val exception = new UnsupportedOperationException("Can not add messages after the buffer is completed") 294 | val failed = Failed(exception) 295 | buffer.transition(failed) 296 | } 297 | 298 | /** 299 | * Attach the given subscriber and transition to the Draining state 300 | */ 301 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = { 302 | val draining = Draining(queue, PreparedSubscriber(subscriber)) 303 | buffer.transition(draining) 304 | } 305 | 306 | /** 307 | * Transition to the Failed state because this operation is not supported when no subscriber is attached 308 | */ 309 | override def addDemand(n: Long): Unit = { 310 | val exception = new UnsupportedOperationException("No subscriber is attached") 311 | val failed = Failed(exception) 312 | buffer.transition(failed) 313 | } 314 | 315 | /** 316 | * Ignore because the buffer is already completed 317 | */ 318 | override def complete(): Unit = {} 319 | 320 | /** 321 | * Transition to the Failed state because this operation is not supported when no subscriber is attached 322 | */ 323 | override def cancel(): Unit = { 324 | val exception = new UnsupportedOperationException("No subscriber is attached") 325 | val failed = Failed(exception) 326 | buffer.transition(failed) 327 | } 328 | 329 | /** 330 | * Error the buffer by transitioning to the Failed state with the given exception 331 | */ 332 | override def error(exception: Throwable): Unit = { 333 | val failed = Failed(exception) 334 | buffer.transition(failed) 335 | } 336 | } 337 | 338 | /** 339 | * State when the socket is open, the buffer will receive and queue messages, a subscriber is attached, and messages 340 | * are sent to the subscriber in response to its demand signals 341 | * 342 | * This state streams messages from the socket to the subscriber via the queue. Messages are sent to the subscriber 343 | * in response to its demand signals. When the socket closes and the buffer completes, then the state will transition 344 | * to Draining if there are message left in the queue, or Completed if there are no messages left. 345 | */ 346 | case class Streaming( 347 | queue: mutable.Queue[ByteString], 348 | bufferSize: Int, 349 | preparedSubscriber: PreparedSubscriber 350 | )(implicit val buffer: WebSocketStreamBuffer, val socket: WebSocket) 351 | extends State 352 | with QueueSize 353 | with Fulfill 354 | with Cancelable { 355 | // Cumulative outstanding demand starting at 0 356 | private var demand = 0L 357 | val subscriber = preparedSubscriber.subscriber 358 | 359 | /** 360 | * Add message to the queue and fulfill demand is possible 361 | * 362 | * If adding the message would exceed the buffer size then the message is discarded and the buffer transitions to 363 | * the Failed state. 364 | */ 365 | override def enqueue(message: ByteString): Unit = { 366 | checkSize.fold[Unit]({ 367 | queue.enqueue(message) 368 | demand = fulfill(demand) 369 | })(exception => { 370 | val failed = Failed(exception, subscriber) 371 | buffer.transition(failed) 372 | }) 373 | } 374 | 375 | /** 376 | * Fail the new subscriber 377 | * 378 | * Buffer operations continue for the existing subscriber. 379 | */ 380 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = { 381 | PreparedSubscriber.failSingle(subscriber) 382 | } 383 | 384 | /** 385 | * Add and fulfill demand if possible 386 | */ 387 | override def addDemand(n: Long): Unit = { 388 | if (n > 0) { 389 | demand = fulfill(demand + n) 390 | } else { 391 | val exception = new IllegalArgumentException(s"Demand of $n is not positive") 392 | val failed = Failed(exception, subscriber) 393 | buffer.transition(failed) 394 | } 395 | } 396 | 397 | /** 398 | * Transition to the Draining state if there are messages in the queue or the Completed state if not 399 | */ 400 | override def complete(): Unit = { 401 | val state: State = { 402 | if (queue.nonEmpty) Draining(queue, preparedSubscriber, demand) 403 | else Completed(subscriber) 404 | } 405 | buffer.transition(state) 406 | } 407 | 408 | /** 409 | * Error the buffer by transitioning to the Failed state with the given exception 410 | */ 411 | override def error(exception: Throwable): Unit = { 412 | val failed = Failed(exception, subscriber) 413 | buffer.transition(failed) 414 | } 415 | } 416 | 417 | /** 418 | * State when the socket is closed, the buffer will not queue more messages, a subscriber is attached, and messages 419 | * are sent to the subscriber in response to its demand signals 420 | * 421 | * This state drains the queue by sending messages to the subscriber in response to its demand signals until the 422 | * queue is empty and it then transitions to Completed. 423 | */ 424 | case class Draining( 425 | queue: mutable.Queue[ByteString], 426 | preparedSubscriber: PreparedSubscriber, 427 | initialDemand: Long = 0L 428 | )(implicit val buffer: WebSocketStreamBuffer, val socket: WebSocket) 429 | extends State 430 | with Fulfill 431 | with Cancelable { 432 | private var demand = initialDemand 433 | val subscriber = preparedSubscriber.subscriber 434 | 435 | /** 436 | * Transition to the Failed state because this operation is not supported after the buffer is completed 437 | */ 438 | override def enqueue(message: ByteString): Unit = { 439 | val exception = new UnsupportedOperationException("Can not add messages after the buffer is closed") 440 | val failed = Failed(exception, subscriber) 441 | buffer.transition(failed) 442 | } 443 | 444 | /** 445 | * Fail the new subscriber 446 | * 447 | * Buffer operations continue for the existing subscriber. 448 | */ 449 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = { 450 | PreparedSubscriber.failSingle(subscriber) 451 | } 452 | 453 | /** 454 | * Add and fulfill demand if possible, completing the buffer if the queue becomes empty 455 | */ 456 | override def addDemand(n: Long): Unit = { 457 | if (n > 0) { 458 | demand = fulfill(demand + n) 459 | if (queue.isEmpty) { 460 | val completed = Completed(subscriber) 461 | buffer.transition(completed) 462 | } 463 | } else { 464 | val exception = new IllegalArgumentException(s"Demand of $n is not positive") 465 | val failed = Failed(exception, subscriber) 466 | buffer.transition(failed) 467 | } 468 | } 469 | 470 | /** 471 | * Ignore because the buffer is already completed 472 | */ 473 | override def complete(): Unit = {} 474 | 475 | /** 476 | * Error the buffer by transitioning to the Failed state with the given exception 477 | */ 478 | override def error(exception: Throwable): Unit = { 479 | val failed = Failed(exception, subscriber) 480 | buffer.transition(failed) 481 | } 482 | } 483 | 484 | /** 485 | * State when the socket is closed, a subscriber is attached, all queued messages have been sent to the subscriber, 486 | * and the subscriber is completed 487 | * 488 | * This is the successful terminal state. Transitioning to this state closes the socket and completes the subscriber. 489 | */ 490 | case class Completed(subscriber: Subscriber[_])(implicit socket: WebSocket) extends State { 491 | socket.close() 492 | subscriber.onComplete() 493 | 494 | override def enqueue(message: ByteString): Unit = {} 495 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = PreparedSubscriber.failSingle(subscriber) 496 | override def addDemand(n: Long): Unit = {} 497 | override def complete(): Unit = {} 498 | override def cancel(): Unit = {} 499 | override def error(exception: Throwable): Unit = {} 500 | } 501 | 502 | /** 503 | * State when a subscriber is attached and cancelled, causing the socket to be closed and any queued messages to be 504 | * discarded 505 | * 506 | * This is the alternate terminal state. Transitioning to this state closes the socket. 507 | */ 508 | case class Cancelled()(implicit socket: WebSocket) extends State { 509 | socket.close() 510 | 511 | override def enqueue(message: ByteString): Unit = {} 512 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = PreparedSubscriber.failSingle(subscriber) 513 | override def addDemand(n: Long): Unit = {} 514 | override def complete(): Unit = {} 515 | override def cancel(): Unit = {} 516 | override def error(exception: Throwable): Unit = {} 517 | } 518 | 519 | /** 520 | * State when the buffer has failed for any reason, causing the socket to be closed, the subscriber to be 521 | * completed with an error, and any queued messages to be discarded 522 | * 523 | * This is the failed terminal state. Transitioning to this state closes the socket with a failure code and errors 524 | * the subscriber if one is attached. A subscriber can be attached after failure in case the buffer fails before the 525 | * stream is set up, in which case the subscriber is immediately errored with the exception. 526 | */ 527 | case class Failed(exception: Throwable, subscriber: Option[Subscriber[_]] = None)(implicit socket: WebSocket) 528 | extends State { 529 | socket.close(ApplicationFailureCode) 530 | subscriber.foreach(_.onError(exception)) 531 | 532 | override def enqueue(message: ByteString): Unit = {} 533 | override def attach(subscriber: Subscriber[_ >: ByteString]): Unit = PreparedSubscriber.fail(subscriber, exception) 534 | override def addDemand(n: Long): Unit = {} 535 | override def complete(): Unit = {} 536 | override def cancel(): Unit = {} 537 | override def error(exception: Throwable): Unit = {} 538 | } 539 | 540 | object Failed { 541 | def apply(exception: Throwable, subscriber: Subscriber[_])(implicit socket: WebSocket): Failed = 542 | Failed(exception, Some(subscriber)) 543 | } 544 | 545 | /** Subscription that interacts with the WebSocketStreamBuffer */ 546 | class BufferSubscription(buffer: WebSocketStreamBuffer) extends Subscription { 547 | // Add request to the outstanding buffer demand 548 | override def request(n: Long): Unit = buffer.addDemand(n) 549 | 550 | // Cancel the buffer 551 | // The buffer cancels immediately and does not delivery any pending elements 552 | override def cancel(): Unit = buffer.cancel() 553 | } 554 | 555 | /** Dummy subscription used to set up subscriber and in order to immediately error the subscriber */ 556 | object ErrorSubscription extends Subscription { 557 | override def request(n: Long): Unit = {} 558 | override def cancel(): Unit = {} 559 | } 560 | 561 | /** 562 | * A subscriber that has been prepared with the appropriate subscription to interact with a buffer 563 | * 564 | * The reactive streams specification requires that a subscription be provided to the subscriber prior to any other 565 | * signals. This is used to ensure that the subscriber is ready for operations before being used. 566 | */ 567 | case class PreparedSubscriber(subscriber: Subscriber[_ >: ByteString])(implicit buffer: WebSocketStreamBuffer) { 568 | subscriber.onSubscribe(new BufferSubscription(buffer)) 569 | } 570 | 571 | object PreparedSubscriber { 572 | 573 | /** 574 | * Fail the given subscriber because another subscriber is already attached 575 | * 576 | * @param subscriber the subscriber to fail 577 | */ 578 | def failSingle(subscriber: Subscriber[_]): Unit = { 579 | fail(subscriber, new IllegalStateException("This publisher only supports one subscriber")) 580 | } 581 | 582 | /** 583 | * Fail the given subscriber with the given exception 584 | * 585 | * The reactive streams specification requires that a subscription be provided to the subscriber prior to any other 586 | * signals. The subscriber is provided a dummy subscription and then immediately failed. 587 | * 588 | * @param subscriber the subscriber to fail 589 | * @param exception the exception 590 | */ 591 | def fail(subscriber: Subscriber[_], exception: Throwable): Unit = { 592 | subscriber.onSubscribe(ErrorSubscription) 593 | subscriber.onError(exception) 594 | } 595 | } 596 | 597 | } 598 | -------------------------------------------------------------------------------- /lagomjs-api-scaladsl/js/src/main/scala/com/lightbend/lagom/scaladsl/api/Service.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) Lightbend Inc. 3 | */ 4 | 5 | package com.lightbend.lagom.scaladsl.api 6 | 7 | import akka.util.ByteString 8 | import com.lightbend.lagom.scaladsl.api.Descriptor.ServiceCallHolder 9 | import com.lightbend.lagom.scaladsl.api.Descriptor.TopicHolder 10 | import com.lightbend.lagom.scaladsl.api.broker.Topic 11 | import com.lightbend.lagom.scaladsl.api.deser.MessageSerializer 12 | import com.lightbend.lagom.scaladsl.api.deser.PathParamSerializer 13 | 14 | import scala.collection.immutable 15 | import scala.reflect.macros.blackbox.Context 16 | import scala.language.experimental.macros 17 | import scala.language.implicitConversions 18 | import scala.scalajs.js 19 | 20 | /** 21 | * A Lagom service descriptor. 22 | * 23 | * This trait provides a DSL for describing a service. It is inherently constrained in its use. 24 | * 25 | * A service can describe itself by defining a trait that extends this trait, and provides an 26 | * implementation for the [[Service#descriptor]] method. 27 | */ 28 | trait Service { 29 | 30 | /** 31 | * Describe this service. 32 | * 33 | * The intended mechanism for implementing this is to implement it on the trait for the service. 34 | */ 35 | def descriptor: Descriptor 36 | } 37 | 38 | object Service { 39 | import ServiceSupport._ 40 | import Descriptor._ 41 | 42 | /** 43 | * Create a descriptor for a service with the given name. 44 | * 45 | * @param name The name of the service. 46 | * @return The descriptor. 47 | */ 48 | def named(name: String): Descriptor = Descriptor(name) 49 | 50 | /** 51 | * Create a named service call descriptor, identified by the name of the method that implements the service call. 52 | * 53 | * The `method` parameter can be provided by passing a reference to the function that provides the service call. 54 | * [[ServiceSupport]] provides a number of implicit conversions and macros for converting these references to a 55 | * [[ScalaMethodServiceCall]], which captures the actual method. 56 | * 57 | * The DSL allows the following ways to pass method references: 58 | * 59 | * ``` 60 | * // Eta-expanded method on this 61 | * call(getItem _) 62 | * // Invocation of parameterless method on this 63 | * call(getItem) 64 | * // Invocation of zero argument method on this 65 | * call(getItem()) 66 | * ``` 67 | * 68 | * The service call may or may not be qualified by `this`. These are the only forms that the DSL permits, the macro 69 | * that implements this will inspect the passed in Scala AST to extract which method is invoked. Anything else passed 70 | * will result in a compilation failure. 71 | * 72 | * @param method A reference to the service call method. 73 | * @return A named service call descriptor. 74 | */ 75 | def call[Request, Response](method: ScalaMethodServiceCall[Request, Response])( 76 | implicit requestSerializer: MessageSerializer[Request, _], 77 | responseSerializer: MessageSerializer[Response, _] 78 | ): Call[Request, Response] = 79 | namedCall(method.method.getName, method) 80 | 81 | /** 82 | * Create a named service call descriptor, identified by the given name. 83 | * 84 | * The `method` parameter can be provided by passing a reference to the function that provides the service call. 85 | * [[ServiceSupport]] provides a number of implicit conversions and macros for converting these references to a 86 | * [[ScalaMethodServiceCall]], which captures the actual method. 87 | * 88 | * The DSL allows the following ways to pass method references: 89 | * 90 | * ``` 91 | * // Eta-expanded method on this 92 | * namedCall("item", getItem _) 93 | * // Invocation of parameterless method on this 94 | * namedCall("item", getItem) 95 | * // Invocation of zero argument method on this 96 | * namedCall("item", getItem()) 97 | * ``` 98 | * 99 | * The service call may or may not be qualified by `this`. These are the only forms that the DSL permits, the macro 100 | * that implements this will inspect the passed in Scala AST to extract which method is invoked. Anything else passed 101 | * will result in a compilation failure. 102 | * 103 | * @param name The name of the service call. 104 | * @param method A reference to the service call method. 105 | * @return A named service call descriptor. 106 | */ 107 | def namedCall[Request, Response](name: String, method: ScalaMethodServiceCall[Request, Response])( 108 | implicit requestSerializer: MessageSerializer[Request, _], 109 | responseSerializer: MessageSerializer[Response, _] 110 | ): Call[Request, Response] = { 111 | CallImpl(NamedCallId(name), method, requestSerializer, responseSerializer) 112 | } 113 | 114 | /** 115 | * Create a path service call descriptor, identified by the given path pattern. 116 | * 117 | * The `method` parameter can be provided by passing a reference to the function that provides the service call. 118 | * [[ServiceSupport]] provides a number of implicit conversions and macros for converting these references to a 119 | * [[ScalaMethodServiceCall]], which captures the actual method, as well as the implicit serializers necessary to 120 | * convert the path parameters specified in the `pathPattern` to and from the arguments for the method. 121 | * 122 | * The DSL allows the following ways to pass method references: 123 | * 124 | * ``` 125 | * // Eta-expanded method on this 126 | * pathCall("/item/:id", getItem _) 127 | * // Invocation of parameterless method on this 128 | * pathCall("/items", getItem) 129 | * // Invocation of zero argument method on this 130 | * pathCall("/items", getItem()) 131 | * ``` 132 | * 133 | * The service call may or may not be qualified by `this`. These are the only forms that the DSL permits, the macro 134 | * that implements this will inspect the passed in Scala AST to extract which method is invoked. Anything else passed 135 | * will result in a compilation failure. 136 | * 137 | * @param pathPattern The path pattern. 138 | * @param method A reference to the service call method. 139 | * @return A path based service call descriptor. 140 | */ 141 | def pathCall[Request, Response](pathPattern: String, method: ScalaMethodServiceCall[Request, Response])( 142 | implicit requestSerializer: MessageSerializer[Request, _], 143 | responseSerializer: MessageSerializer[Response, _] 144 | ): Call[Request, Response] = { 145 | CallImpl(PathCallId(pathPattern), method, requestSerializer, responseSerializer) 146 | } 147 | 148 | /** 149 | * Create a REST service call descriptor, identified by the given HTTP method and path pattern. 150 | * 151 | * The `scalaMethod` parameter can be provided by passing a reference to the function that provides the service call. 152 | * [[ServiceSupport]] provides a number of implicit conversions and macros for converting these references to a 153 | * [[ScalaMethodServiceCall]], which captures the actual method, as well as the implicit serializers necessary to 154 | * convert the path parameters specified in the `pathPattern` to and from the arguments for the method. 155 | * 156 | * The DSL allows the following ways to pass method references: 157 | * 158 | * ``` 159 | * // Eta-expanded method on this 160 | * restCall(Method.GET, "/item/:id", getItem _) 161 | * // Invocation of parameterless method on this 162 | * restCall(Method.GET, "/items", getItem) 163 | * // Invocation of zero argument method on this 164 | * restCall(Method.GET, "/items", getItem()) 165 | * ``` 166 | * 167 | * The service call may or may not be qualified by `this`. These are the only forms that the DSL permits, the macro 168 | * that implements this will inspect the passed in Scala AST to extract which method is invoked. Anything else passed 169 | * will result in a compilation failure. 170 | * 171 | * @param method The HTTP method. 172 | * @param pathPattern The path pattern. 173 | * @param scalaMethod A reference to the service call method. 174 | * @return A REST service call descriptor. 175 | */ 176 | def restCall[Request, Response]( 177 | method: com.lightbend.lagom.scaladsl.api.transport.Method, 178 | pathPattern: String, 179 | scalaMethod: ScalaMethodServiceCall[Request, Response] 180 | )( 181 | implicit requestSerializer: MessageSerializer[Request, _], 182 | responseSerializer: MessageSerializer[Response, _] 183 | ): Call[Request, Response] = { 184 | CallImpl(RestCallId(method, pathPattern), scalaMethod, requestSerializer, responseSerializer) 185 | } 186 | 187 | /** 188 | * Create a topic descriptor. 189 | * 190 | * The `method` parameter can be provided by passing a reference to the function that provides the topic. 191 | * [[ServiceSupport]] provides a number of implicit conversions and macros for converting these references to a 192 | * [[ScalaMethodServiceCall]], which captures the actual method. 193 | * 194 | * The DSL allows the following ways to pass method references: 195 | * 196 | * ``` 197 | * // Eta-expanded method on this 198 | * topic("item-events", itemEventStream _) 199 | * // Invocation of parameterless method on this 200 | * topic("item-events", itemEventStream) 201 | * // Invocation of zero argument method on this 202 | * topic("item-events", itemEventStream) 203 | * ``` 204 | * 205 | * The topic may or may not be qualified by `this`. These are the only forms that the DSL permits, the macro 206 | * that implements this will inspect the passed in Scala AST to extract which method is invoked. Anything else passed 207 | * will result in a compilation failure. 208 | * 209 | * @param topicId The name of the topic. 210 | * @param method A reference to the topic method. 211 | * @return A topic call descriptor. 212 | */ 213 | def topic[Message](topicId: String, method: ScalaMethodTopic[Message])( 214 | implicit messageSerializer: MessageSerializer[Message, ByteString] 215 | ): TopicCall[Message] = { 216 | TopicCallImpl[Message](Topic.TopicId(topicId), method, messageSerializer) 217 | } 218 | } 219 | 220 | /** 221 | * Provides implicit conversions and macros to implement the service descriptor DSL. 222 | */ 223 | object ServiceSupport { 224 | import com.lightbend.lagom.scaladsl.api.deser.{ PathParamSerializer => PPS } 225 | 226 | /** 227 | * A reference to a method that implements a topic. 228 | * 229 | * @param method The actual method that implements the topic. 230 | */ 231 | class ScalaMethodTopic[Message] private[lagom] (val method: Method) extends TopicHolder 232 | 233 | /** 234 | * Provides implicit conversions to convert Scala AST that references methods to actual method references. 235 | */ 236 | object ScalaMethodTopic { 237 | implicit def topicMethodFor[Message](f: => Topic[Message]): ScalaMethodTopic[Message] = 238 | macro topicMethodForImpl[Message] 239 | implicit def topicMethodFor0[Message](f: () => Topic[Message]): ScalaMethodTopic[Message] = 240 | macro topicMethodForImpl[Message] 241 | } 242 | 243 | /** 244 | * A reference to a method that implements a service call. 245 | * 246 | * @param method The actual method that implements the service call. 247 | * @param pathParamSerializers The serializers used to convert parameters extracted from the path to and from the 248 | * arguments for the method. 249 | */ 250 | class ScalaMethodServiceCall[Request, Response] private[lagom] ( 251 | val method: Method, 252 | val pathParamSerializers: immutable.Seq[PathParamSerializer[_]] 253 | ) extends ServiceCallHolder { 254 | def invoke(service: Any, args: immutable.Seq[AnyRef]) = method.invoke(service, args: _*) 255 | } 256 | 257 | /** 258 | * Provides implicit conversions to convert Scala AST that references methods to actual method references. 259 | */ 260 | object ScalaMethodServiceCall { 261 | implicit def methodFor[Q, R](f: => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl0[Q, R] 262 | implicit def methodFor0[Q, R](f: () => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl0[Q, R] 263 | 264 | // format: off 265 | // Execute in REPL to generate: 266 | // (1 to 22).foreach { i => println(s" implicit def methodFor$i[Q, R" + (1 to i).map(p => s", P$p: PPS").mkString("") + "](f: (" + (1 to i).map(p => s"P$p").mkString(", ") + s") => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl$i[Q, R]") } 267 | implicit def methodFor1[Q, R, P1: PPS](f: (P1) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl1[Q, R] 268 | implicit def methodFor2[Q, R, P1: PPS, P2: PPS](f: (P1, P2) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl2[Q, R] 269 | implicit def methodFor3[Q, R, P1: PPS, P2: PPS, P3: PPS](f: (P1, P2, P3) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl3[Q, R] 270 | implicit def methodFor4[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS](f: (P1, P2, P3, P4) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl4[Q, R] 271 | implicit def methodFor5[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS](f: (P1, P2, P3, P4, P5) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl5[Q, R] 272 | implicit def methodFor6[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS](f: (P1, P2, P3, P4, P5, P6) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl6[Q, R] 273 | implicit def methodFor7[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS](f: (P1, P2, P3, P4, P5, P6, P7) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl7[Q, R] 274 | implicit def methodFor8[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl8[Q, R] 275 | implicit def methodFor9[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl9[Q, R] 276 | implicit def methodFor10[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl10[Q, R] 277 | implicit def methodFor11[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl11[Q, R] 278 | implicit def methodFor12[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl12[Q, R] 279 | implicit def methodFor13[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl13[Q, R] 280 | implicit def methodFor14[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl14[Q, R] 281 | implicit def methodFor15[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl15[Q, R] 282 | implicit def methodFor16[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl16[Q, R] 283 | implicit def methodFor17[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl17[Q, R] 284 | implicit def methodFor18[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS, P18: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl18[Q, R] 285 | implicit def methodFor19[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS, P18: PPS, P19: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl19[Q, R] 286 | implicit def methodFor20[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS, P18: PPS, P19: PPS, P20: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl20[Q, R] 287 | implicit def methodFor21[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS, P18: PPS, P19: PPS, P20: PPS, P21: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl21[Q, R] 288 | implicit def methodFor22[Q, R, P1: PPS, P2: PPS, P3: PPS, P4: PPS, P5: PPS, P6: PPS, P7: PPS, P8: PPS, P9: PPS, P10: PPS, P11: PPS, P12: PPS, P13: PPS, P14: PPS, P15: PPS, P16: PPS, P17: PPS, P18: PPS, P19: PPS, P20: PPS, P21: PPS, P22: PPS](f: (P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22) => ServiceCall[Q, R]): ScalaMethodServiceCall[Q, R] = macro methodForImpl22[Q, R] 289 | // format: on 290 | } 291 | 292 | /** 293 | * A JavaScript stand-in for [[java.lang.reflect.Method]]. 294 | * 295 | * @param clazz The class that declared the method. 296 | * @param name The name of the method. 297 | */ 298 | protected class Method private[lagom] (clazz: Class[_], name: String) { 299 | def getDeclaringClass: Class[_] = clazz 300 | def getName: String = name 301 | def invoke(obj: Any, args: AnyRef*): Dynamic = obj.asInstanceOf[js.Dynamic].applyDynamic(name)(args) 302 | } 303 | 304 | private def locateMethod(clazz: Class[_], name: String): Method = { 305 | new Method(clazz, name) 306 | } 307 | 308 | /** 309 | * The code generated by the service call macros uses this method to look up the Java reflection API [[Method]] for 310 | * the method that is being referenced, and uses it to create a [[ScalaMethodServiceCall]]. 311 | * 312 | * @param clazz The class that the method should be looked up from (should be the service interface). 313 | * @param name The name of the method to look up. 314 | * @param pathParamSerializers The list of path parameter serializers, (typically captured by implicit parameters), 315 | * for the arguments to the method. 316 | * @return The service call holder. 317 | */ 318 | def getServiceCallMethodWithName[Request, Response]( 319 | clazz: Class[_], 320 | name: String, 321 | pathParamSerializers: Seq[PathParamSerializer[_]] 322 | ): ScalaMethodServiceCall[Request, Response] = { 323 | new ScalaMethodServiceCall[Request, Response]( 324 | locateMethod(clazz, name), 325 | pathParamSerializers.toIndexedSeq 326 | ) 327 | } 328 | 329 | /** 330 | * The code generated by the topic macros uses this method to look up the Java reflection API [[Method]] for 331 | * the method that is being referenced, and uses it to create a [[ScalaMethodTopic]]. 332 | * 333 | * @param clazz The class that the method should be looked up from (should be the service interface). 334 | * @param name The name of the method to look up. 335 | * @return The topic holder. 336 | */ 337 | def getTopicMethodWithName[Message](clazz: Class[_], name: String): ScalaMethodTopic[Message] = { 338 | new ScalaMethodTopic[Message]( 339 | locateMethod(clazz, name) 340 | ) 341 | } 342 | 343 | // 344 | // Macro implementations 345 | // 346 | 347 | def methodForImpl0[Q, R]( 348 | c: Context 349 | )(f: c.Tree)(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = 350 | methodForImpl[Q, R](c)(f) 351 | 352 | // format: off 353 | // Execute in REPL to generate: 354 | // (1 to 22).foreach { i => println(s" def methodForImpl$i[Q, R](c: Context)(f: c.Tree)(" + (1 to i).map(p => s"p$p: c.Expr[PPS[_]]").mkString(", ") + s")(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f" + (1 to i).map(p => s", p$p").mkString("") + ")") } 355 | def methodForImpl1[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1) 356 | def methodForImpl2[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2) 357 | def methodForImpl3[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3) 358 | def methodForImpl4[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4) 359 | def methodForImpl5[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5) 360 | def methodForImpl6[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6) 361 | def methodForImpl7[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7) 362 | def methodForImpl8[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8) 363 | def methodForImpl9[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9) 364 | def methodForImpl10[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) 365 | def methodForImpl11[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) 366 | def methodForImpl12[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12) 367 | def methodForImpl13[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13) 368 | def methodForImpl14[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14) 369 | def methodForImpl15[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15) 370 | def methodForImpl16[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16) 371 | def methodForImpl17[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17) 372 | def methodForImpl18[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]], p18: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18) 373 | def methodForImpl19[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]], p18: c.Expr[PPS[_]], p19: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19) 374 | def methodForImpl20[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]], p18: c.Expr[PPS[_]], p19: c.Expr[PPS[_]], p20: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20) 375 | def methodForImpl21[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]], p18: c.Expr[PPS[_]], p19: c.Expr[PPS[_]], p20: c.Expr[PPS[_]], p21: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21) 376 | def methodForImpl22[Q, R](c: Context)(f: c.Tree)(p1: c.Expr[PPS[_]], p2: c.Expr[PPS[_]], p3: c.Expr[PPS[_]], p4: c.Expr[PPS[_]], p5: c.Expr[PPS[_]], p6: c.Expr[PPS[_]], p7: c.Expr[PPS[_]], p8: c.Expr[PPS[_]], p9: c.Expr[PPS[_]], p10: c.Expr[PPS[_]], p11: c.Expr[PPS[_]], p12: c.Expr[PPS[_]], p13: c.Expr[PPS[_]], p14: c.Expr[PPS[_]], p15: c.Expr[PPS[_]], p16: c.Expr[PPS[_]], p17: c.Expr[PPS[_]], p18: c.Expr[PPS[_]], p19: c.Expr[PPS[_]], p20: c.Expr[PPS[_]], p21: c.Expr[PPS[_]], p22: c.Expr[PPS[_]])(implicit qType: c.WeakTypeTag[Q], rType: c.WeakTypeTag[R]): c.Expr[ScalaMethodServiceCall[Q, R]] = methodForImpl[Q, R](c)(f, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16, p17, p18, p19, p20, p21, p22) 377 | // format: on 378 | 379 | private def methodForImpl[Request, Response](c: Context)(f: c.Tree, ps: c.Expr[PathParamSerializer[_]]*)( 380 | implicit requestType: c.WeakTypeTag[Request], 381 | responseType: c.WeakTypeTag[Response] 382 | ): c.Expr[ScalaMethodServiceCall[Request, Response]] = { 383 | import c.universe._ 384 | 385 | // First reify the path parameter arguments into a Seq 386 | val pathParamSerializers = c.Expr[Seq[PathParamSerializer[_]]]( 387 | Apply( 388 | Select(reify(Seq).tree, TermName("apply")), 389 | ps.map(_.tree).toList 390 | ) 391 | ) 392 | 393 | // Resolve this class and the method name. 394 | val (thisClassExpr, methodNameLiteral) = resolveThisClassExpressionAndMethodName(c)("methodFor", f) 395 | 396 | val serviceSupport = q"_root_.com.lightbend.lagom.scaladsl.api.ServiceSupport" 397 | 398 | // Generate the actual AST that the macro will output 399 | c.Expr[ScalaMethodServiceCall[Request, Response]](q""" 400 | $serviceSupport.getServiceCallMethodWithName[${requestType.tpe}, ${responseType.tpe}]( 401 | $thisClassExpr, $methodNameLiteral, $pathParamSerializers 402 | ) 403 | """) 404 | } 405 | 406 | def topicMethodForImpl[Message]( 407 | c: Context 408 | )(f: c.Tree)(implicit messageType: c.WeakTypeTag[Message]): c.Expr[ScalaMethodTopic[Message]] = { 409 | import c.universe._ 410 | 411 | // Resolve this class and the method name. 412 | val (thisClassExpr, methodNameLiteral) = resolveThisClassExpressionAndMethodName(c)("topicMethodFor", f) 413 | 414 | val serviceSupport = q"_root_.com.lightbend.lagom.scaladsl.api.ServiceSupport" 415 | 416 | // Generate the actual AST that the macro will output 417 | c.Expr[ScalaMethodTopic[Message]](q""" 418 | $serviceSupport.getTopicMethodWithName[${messageType.tpe}]($thisClassExpr, $methodNameLiteral) 419 | """) 420 | } 421 | 422 | /** 423 | * Given a passed in AST that should be a reference to a method call on this, generate AST to resolve the type of 424 | * this and the name of the method. 425 | * 426 | * @param c The macro context 427 | * @param methodDescription A description of the method that is being invoked, for error reporting. 428 | * @param f The AST. 429 | * @return A tuple of the AST to resolve the type of this, and a literal that contains the name of the method. 430 | */ 431 | private def resolveThisClassExpressionAndMethodName( 432 | c: Context 433 | )(methodDescription: String, f: c.Tree): (c.Tree, c.universe.Literal) = { 434 | import c.universe._ 435 | 436 | // Recurse on the tree so no combinations are missed (e.g. Block(_, Function(_, Select(..)))) 437 | def extract(f: Tree): (TypeName, String) = f match { 438 | case Select(This(tt), TermName(tn)) => (tt, tn) // a method, just selected b/c no parameters 439 | case Apply(t, _) => extract(t) // a method with parameter lists, applied 440 | case Function(_, t) => extract(t) // a function 441 | case Block(_, t) => extract(t) // a block 442 | case other => 443 | c.abort( 444 | c.enclosingPosition, 445 | s"$methodDescription must only be invoked with a reference to a function on this, for example, $methodDescription(this.someFunction _)" 446 | ) 447 | } 448 | val (thisType, methodName) = extract(f) 449 | 450 | val methodNameLiteral = Literal(Constant(methodName)) 451 | 452 | val thisClassExpr = if (thisType.toString.isEmpty) { 453 | // If the type is empty, it means the reference to this is unqualified, eg: 454 | // namedCall(this.someServiceCall _) 455 | q"this.getClass" 456 | } else { 457 | // Otherwise, it's a qualified reference, and we should use that type, eg: 458 | // namedCall(MyService.this.someServiceCall _) 459 | // This also happens to be what the type checker will infer when you don't explicitly refer to this, eg: 460 | // namedCall(someServiceCall _) 461 | q"_root_.scala.Predef.classOf[$thisType]" 462 | } 463 | 464 | (thisClassExpr, methodNameLiteral) 465 | } 466 | } 467 | --------------------------------------------------------------------------------