{"id":21849650,"url":"https://github.com/frees-io/frees-rpc-lambda-world-workshop","last_synced_at":"2026-05-21T05:31:46.091Z","repository":{"id":73456200,"uuid":"149020011","full_name":"frees-io/frees-rpc-lambda-world-workshop","owner":"frees-io","description":" Building Purely Functional Microservices with Freestyle RPC","archived":false,"fork":false,"pushed_at":"2018-09-18T00:18:35.000Z","size":33,"stargazers_count":1,"open_issues_count":0,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-21T17:54:57.608Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/frees-io.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-09-16T17:40:40.000Z","updated_at":"2018-12-07T01:20:31.000Z","dependencies_parsed_at":null,"dependency_job_id":"a18ffb05-f382-45d4-99af-4760fdc87504","html_url":"https://github.com/frees-io/frees-rpc-lambda-world-workshop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/frees-io/frees-rpc-lambda-world-workshop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frees-io%2Ffrees-rpc-lambda-world-workshop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frees-io%2Ffrees-rpc-lambda-world-workshop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frees-io%2Ffrees-rpc-lambda-world-workshop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frees-io%2Ffrees-rpc-lambda-world-workshop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/frees-io","download_url":"https://codeload.github.com/frees-io/frees-rpc-lambda-world-workshop/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/frees-io%2Ffrees-rpc-lambda-world-workshop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33289825,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-21T02:57:32.698Z","status":"ssl_error","status_checked_at":"2026-05-21T02:57:31.990Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-28T00:13:57.559Z","updated_at":"2026-05-21T05:31:46.085Z","avatar_url":"https://github.com/frees-io.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# My Smart Home workshop\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n- [What are we going to build?](#what-are-we-going-to-build)\n- [Basic Freestyle-RPC Structure](#basic-freestyle-rpc-structure)\n  - [How to run it](#how-to-run-it)\n  - [Project structure](#project-structure)\n    - [Protocol](#protocol)\n    - [Server](#server)\n    - [Client](#client)\n- [Evolving the Avro schema](#evolving-the-avro-schema)\n  - [Protocol](#protocol-1)\n- [Unary RPC service: `IsEmpty`](#unary-rpc-service-isempty)\n  - [Protocol](#protocol-2)\n  - [Server](#server-1)\n  - [Client](#client-1)\n  - [Result](#result)\n- [Server-streaming RPC service: `GetTemperature`](#server-streaming-rpc-service-gettemperature)\n  - [Protocol](#protocol-3)\n  - [Server](#server-2)\n  - [Client](#client-2)\n  - [Result](#result-1)\n- [Bidirectional streaming RPC service: `comingBackMode`](#bidirectional-streaming-rpc-service-comingbackmode)\n  - [Protocol](#protocol-4)\n  - [Server](#server-3)\n  - [Client](#client-3)\n  - [Result](#result-2)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n\n## What are we going to build?\n\nDuring the course of this workshop, we are going to build a couple of purely functional microservices, which are going to interact with each other in different ways but always via the RPC protocol. One as a server will play the role of a smart home and the other will be a client, a mobile app for instance, and the interactions will be:\n\n- `IsEmpty`: Will be a unary RPC, that means that the smart home will return a single response to each request from the mobile, to let it know if there is anybody inside the home or there isn't.\n\n- `getTemperature`: Will be a unidirectional streaming service from the server, where the smart home will return a stream of temperature values in real-time after a single request from the mobile.\n\n- `comingBackMode`: Will be a bidirectional streaming service, where the mobile app sends a stream of location coordinates and the smart home emits in streaming a list of operations that are being triggered. For instance:\n   - If the client is about 30 minutes to come back, the home can start heating the living room and increase the power of the hot water heater.\n   - If the client is only a 2-minute walk away, the home can turn some lights on and turn the irrigation system off.\n   - If the client is in front of the main door, this can be unlocked and the alarms disabled.\n\n## Basic Freestyle-RPC Structure\n\nWe are going to use the `rpc-server-client-pb` giter8 template to create the basic project structure, which provides a good basis to build upon. In this case, the template creates a multimodule project, with:\n- The RPC protocol, which is very simple. It exposes a service to lookup a person given a name.\n- The server, which with implements an interpreter of the service defined by the protocol and it runs an RPC server.\n- The client, which consumes the RPC endpoint against the server, and it uses the protocol to know the schema.\n\nTo start:\n\n```bash\nsbt new frees-io/rpc-server-client-pb.g8\n...\nname [Project Name]: SmartHome\nprojectDescription [Project Description]: My SmartHome app\nproject [project-name]: smarthome\npackage [org.mycompany]: com.fortyseven\nfreesRPCVersion [0.14.1]:\n\nTemplate applied in ./smarthome\n```\n\n### How to run it\n\nRun the server:\n\n```bash\nsbt runServer\n```\n\nAnd the log will show:\n\n```bash\nINFO  - ServiceName(seedServer) - Starting app.server at Host(localhost):Port(19683)\n```\n\nthen, run the client:\n\n```bash\nsbt runClient\n```\n\nThe client should log:\n\n```bash\nINFO  - Created new RPC client for (localhost,19683)\nINFO  - Request: foo\nINFO  - Result: PeopleResponse(Person(foo,24))\nINFO  - Request: bar\nINFO  - Result: PeopleResponse(Person(bar,9))\nINFO  - Request: baz\nINFO  - Result: PeopleResponse(Person(baz,17))\nINFO  - Removed 1 RPC clients from cache.\n```\n\nAnd the server:\n\n```bash\nINFO  - PeopleService - Request: PeopleRequest(foo)\nINFO  - PeopleService - Sending response: Person(foo,24)\nINFO  - PeopleService - Request: PeopleRequest(bar)\nINFO  - PeopleService - Sending response: Person(bar,9)\nINFO  - PeopleService - Request: PeopleRequest(baz)\nINFO  - PeopleService - Sending response: Person(baz,17)\n```\n\n### Project structure\n\n```bash\n.\n├── LICENSE\n├── NOTICE.md\n├── README.md\n├── build.sbt\n├── version.sbt\n├── commons\n├── project\n├── client\n│   └── src\n│       └── main\n│           ├── resources\n│           │   └── logback.xml\n│           └── scala\n│               ├── ClientApp.scala\n│               ├── ClientRPC.scala\n│               └── PeopleServiceApi.scala\n├── protocol\n│   └── src\n│       └── main\n│           └── resources\n│               ├── People.avdl\n│               └── PeopleService.avdl\n└── server\n    └── src\n        └── main\n            └── scala\n                ├── PeopleRepository.scala\n                ├── PeopleServiceHandler.scala\n                └── ServerApp.scala\n```\n\n#### Protocol\n\nThe protocol module includes the definition of the service and the messages that will be used both by the server and the client:\n\n```bash\n├── protocol\n│   └── src\n│       └── main\n│           └── resources\n│               ├── People.avdl\n│               └── PeopleService.avdl\n```\n\n**_People.avdl_**\n\nIn this initial example, which the app only exposes a service to retrieve persons by a given name, we need to define the models that are going to \"flow through the wire\", and in this case we are using Avro Schema Definition:\n\n```scala\nprotocol People {\n\n  record Person {\n    string name;\n    int age;\n  }\n\n  record PeopleRequest {\n    string name;\n  }\n\n  record PeopleResponse {\n    Person person;\n  }\n\n}\n```\n\n**_PeopleService.avdl_**\n\nAnd finally, we have to define the protocol. In this case is just an operation called `getPerson` that accepts a `PeopleRequest` and returns a `PeopleResponse`:\n\n```scala\nprotocol PeopleService {\n  import idl \"People.avdl\";\n  PeopleResponse getPerson(PeopleRequest request);\n}\n```\n\n#### Server\n\nThe server tackles mainly a couple of purposes: To run the RPC server and provide an interpreter to the service defined in the protocol.\n\n```scala\n└── server\n    └── src\n        └── main\n            └── scala\n                ├── PeopleRepository.scala\n                ├── PeopleServiceHandler.scala\n                └── ServerApp.scala\n```\n\n**_PoepleServiceHandler.scala_**\n\nThis is the interpretation of the protocol `PeopleService`. In this case, the `getPerson` operation returns the person retrieved by the `PeopleRepository`, which represents a database interaction.\n\n```scala\nclass PeopleServiceHandler[F[_]: Sync: Logger: PeopleRepository] extends PeopleService[F] {\n  val serviceName = \"PeopleService\"\n\n  override def getPerson(request: PeopleRequest): F[PeopleResponse] =\n    for {\n      _      \u003c- Logger[F].info(s\"$serviceName - Request: $request\")\n      person \u003c- PeopleRepository[F].getPerson(request.name)\n      _      \u003c- Logger[F].info(s\"$serviceName - Sending response: $person\")\n    } yield PeopleResponse(person)\n}\n```\n\n**_ServerApp.scala_**\n\nThe implementation of the `serverStream` leverages the features of **GrpcServer** to deal with servers.\n\n```scala\nimplicit val PS: PeopleService[F] = new PeopleServiceHandler[F]\n\nval grpcConfigs: List[GrpcConfig] = List(AddService(PeopleService.bindService[F]))\n\nStream.eval(\n  for {\n    server \u003c- GrpcServer.default[F](config.port.value, grpcConfigs)\n    _ \u003c- Logger[F].info(s\"${config.name} - Starting app.server at ${config.host}:${config.port}\")\n    exitCode \u003c- GrpcServer.server(server).as(StreamApp.ExitCode.Success)\n  } yield exitCode\n)\n```\n\n#### Client\n\nIn this initial version of the client, it just runs a client for the `PeopleService` and it injects it in the streaming flow of the app.\n\n```scala\n├── client\n│   └── src\n│       └── main\n│           ├── resources\n│           │   └── logback.xml\n│           └── scala\n│               ├── ClientApp.scala\n│               ├── ClientRPC.scala\n│               └── PeopleServiceApi.scala\n```\n\n**_PeopleServiceApi.scala_**\n\nThis algebra is the via to connect to the server through the RPC client, using some Freestyle-RPC magic.\n\n```scala\ntrait PeopleServiceApi[F[_]] {\n  def getPersonByName(name: String): F[Person]\n}\n\nobject PeopleServiceApi {\n\n  def apply[F[_]: Effect](clientRPCF: F[PeopleService.Client[F]])(implicit L: Logger[F]):PeopleServiceApi[F] =\n    new PeopleServiceApi[F] {\n      override def getPersonByName(name: String): F[Person] =\n        for {\n          clientRPC \u003c- clientRPCF\n          _         \u003c- L.info(s\"Request: $name\")\n          result    \u003c- clientRPC.getPerson(PeopleRequest(name))\n          _         \u003c- L.info(s\"Result: $result\")\n        } yield result.person\n    }\n\n  def createInstance[F[_]: Effect](\n      hostname: String,\n      port: Int,\n      sslEnabled: Boolean = false,\n      tryToRemoveUnusedEvery: FiniteDuration = 30.minutes,\n      removeUnusedAfter: FiniteDuration = 1.hour)(\n      implicit L: Logger[F],\n      TM: Timer[F],\n      S: Scheduler): fs2.Stream[F, PeopleServiceApi[F]] = ???\n}\n```\n\n**_ClientRPC.scala_**\n\nThis object provides an RPC client for a given tuple of host and port. It's used in `PeopleServiceApi`.\n\n**_ClientApp.scala_**\n\nSimilar to `ServerApp`, this app instantiates the logger, the RPC client and it calls to `getPersonByName` as soon as it starts running.\n\n```scala\nfor {\n  peopleApi \u003c- PeopleServiceApi.createInstance(config.host.value, config.port.value)\n  exitCode \u003c- Stream\n    .eval(List(\"foo\", \"bar\", \"baz\").traverse[F, Person](peopleApi.getPersonByName))\n    .as(StreamApp.ExitCode.Success)\n} yield exitCode\n```\n\n## Evolving the Avro schema\n\nAs we have seen before, both client and server are using the same common protocol defined via Avro schema, which is an ideal scenario but realistically speaking the server side might need to add certainly changes in the model. Then how the server can preserve the compatibility with clients that are still using the old model?\n\nThanks to the Avro definitions we can add evolutions to the models in a safety way, keeping all the clients fully compatible but obviously, there are some limited operations that can't be done, like removing a field in a response model or adding a new required field to a request object.\n\nTo illustrate that non-updated clients are able to keep interacting with evolved servers, we'll just add a new field `phone` to `Person`.\n\n### Protocol\n\nLet's add a new evolution to the models described in the protocol\n\n**_People.avdl_**\n\n\n```scala\nprotocol People {\n\n  record Person {\n    string name;\n    int age;\n    string phone;\n  }\n\n  record PeopleRequest {\n    string name;\n  }\n\n  record PeopleResponse {\n    Person person;\n  }\n\n}\n```\n\nWe can now run the server app using this new version, and the client app with the previous one, and the requests should have been processed properly on both sides.\n\nAs we can see, the client digests `Person`s instances included in the responses as expected:\n\n```scala\nINFO  - Created new RPC client for (localhost,19683)\nINFO  - Request: foo\nINFO  - Result: PeopleResponse(Person(foo,24))\nINFO  - Request: bar\nINFO  - Result: PeopleResponse(Person(bar,9))\nINFO  - Request: baz\nINFO  - Result: PeopleResponse(Person(baz,17))\nINFO  - Removed 1 RPC clients from cache.\n```\n\nEven when actually the server is including the telephone numbers at them:\n\n```scala\nINFO  - PeopleService - Request: PeopleRequest(foo)\nINFO  - PeopleService - Sending response: Person(foo,24,(206) 198-8396)\nINFO  - PeopleService - Request: PeopleRequest(bar)\nINFO  - PeopleService - Sending response: Person(bar,9,(206) 740-2096)\nINFO  - PeopleService - Request: PeopleRequest(baz)\nINFO  - PeopleService - Sending response: Person(baz,17,(206) 812-1984)\n```\n\n## Unary RPC service: `IsEmpty`\n\nHaving said this, now it's the right moment to get started to develop the features of the SmartHome, and discard the `People` stuff. As we said above, we want also to build a unary RPC service to let clients know if there is somebody in the home or there is not.\n\n### Protocol\n\nIn order to show another way to define protocols, we are going to express our models and services using directly Scala code, and using **ProtocolBuffer** as serialiser instead of **Avro**.\n\nSo the protocol module can adopt know this shape (of course we should also discard the `idlgens` references at `ProjectPlugin.scala` and `plugins.sbt`):\n\n```scala\n├── protocol\n│   └── src\n│       └── main\n│           └── scala\n│               └── protocol\n│                   ├── Messages.scala\n│                   └── SmartHomeService.scala\n```\n\n**_Messages.scala_**\n\nWhere we defined the messages flowing through the wire:\n\n```scala\n@message\nfinal case class IsEmptyRequest()\n\n@message\nfinal case class IsEmptyResponse(result: Boolean)\n```\n\n**_SmartHomeService.scala_**\n\nWhere we defined interface of the RPC service:\n\n```scala\n@service(Protobuf) trait SmartHomeService[F[_]] {\n\n  def isEmpty(request: IsEmptyRequest): F[IsEmptyResponse]\n\n}\n```\n\n### Server\n\nNow, we have to implement an interpreter for the new service `SmartHomeService`:\n\n```scala\nclass SmartHomeServiceHandler[F[_]: Sync: Logger] extends SmartHomeService[F] {\n  val serviceName = \"SmartHomeService\"\n\n  override def isEmpty(request: IsEmptyRequest): F[IsEmptyResponse] =\n    Logger[F].info(s\"$serviceName - Request: $request\").as(IsEmptyResponse(true))\n\n}\n```\n\nAnd bind it to the gRPC server:\n\n```scala\nval grpcConfigs: List[GrpcConfig] = List(AddService(SmartHomeService.bindService[F]))\n```\n\n### Client\n\nAnd the client, of course, needs an algebra to describe the same operation:\n\n```scala\ntrait SmartHomeServiceApi[F[_]] {\n  def isEmpty(): F[Boolean]\n}\n```\n\nThat will be called when the app is running\n\n```scala\nfor {\n  serviceApi \u003c- SmartHomeServiceApi.createInstance(config.host.value, config.port.value)\n  _          \u003c- Stream.eval(serviceApi.isEmpty)\n} yield StreamApp.ExitCode.Success\n```\n\n### Result\n\nWhen we run the client now with `sbt runClient` we get:\n\n```bash\nINFO  - Created new RPC client for (localhost,19683)\nINFO  - Result: IsEmptyResponse(true)\nINFO  - Removed 1 RPC clients from cache.\n```\n\nAnd the server log the request as expected:\n\n```bash\nINFO  - SmartHomeService - Request: IsEmptyRequest()\n```\n\n\n## Server-streaming RPC service: `GetTemperature`\n\nFollowing the established plan, the next step is building the service that returns a stream of temperature values, to let clients subscribe to collect real-time info.\n\n### Protocol\n\nAs usual we should add this operation in the protocol.\n\n**_Messages.scala_**\n\nAdding new models:\n\n```scala\ncase class TemperatureUnit(value: String) extends AnyVal\ncase class Temperature(value: Double, unit: TemperatureUnit)\n```\n\n**_SmartHomeService.scala_**\n\nAnd the `getTemperature` operation:\n\n```scala\n@service(Protobuf) trait SmartHomeService[F[_]] {\n\n  def isEmpty(request: IsEmptyRequest): F[IsEmptyResponse]\n\n  def getTemperature(empty: Empty.type): Stream[F, Temperature]\n}\n```\n\n### Server\n\nIf we want to emit a stream of `Temperature` values,  we would be well advised to develop a producer of `Temperature` in the server side. For instance:\n\n```scala\ntrait TemperatureReader[F[_]] {\n  def sendSamples: Stream[F, Temperature]\n}\n\nobject TemperatureReader {\n  implicit def instance[F[_]: Sync: Logger: Timer]: TemperatureReader[F] =\n    new TemperatureReader[F] {\n      val seed = Temperature(77d, TemperatureUnit(\"Fahrenheit\"))\n\n      def readTemperature(current: Temperature): F[Temperature] =\n        Timer[F]\n          .sleep(1.second)\n          .flatMap(_ =\u003e\n            Sync[F].delay {\n              val increment: Double = Random.nextDouble() / 2d\n              val signal            = if (Random.nextBoolean()) 1 else -1\n              val currentValue      = current.value\n\n              current.copy(\n                value = BigDecimal(currentValue + (signal * increment))\n                  .setScale(2, RoundingMode.HALF_UP)\n                  .doubleValue)\n          })\n\n      override def sendSamples: Stream[F, Temperature] =\n        Stream.iterateEval(seed) { t =\u003e\n          Logger[F].info(s\"* New Temperature 👍  --\u003e $t\").flatMap(_ =\u003e readTemperature(t))\n        }\n    }\n\n  def apply[F[_]](implicit ev: TemperatureReader[F]): TemperatureReader[F] = ev\n}\n```\n\nAnd this can be returned as response of the new service, in the interpreter.\n\n```scala\noverride def getTemperature(empty: Empty.type): Stream[F, Temperature] = for {\n  _            \u003c- Stream.eval(Logger[F].info(s\"$serviceName - getTemperature Request\"))\n  temperatures \u003c- TemperatureReader[F].sendSamples.take(20)\n} yield temperatures\n```\n\n### Client\n\nWe have nothing less than adapt the client to consume the new service when it starting up. To this, a couple of changes are needed:\n\nFirstly we should enrich the algebra\n\n```scala\ntrait SmartHomeServiceApi[F[_]] {\n\n  def isEmpty(): F[Boolean]\n\n  def getTemperature(): Stream[F, Temperature]\n\n}\n```\n\nWhose interpretation could be:\n\n```scala\ndef getTemperature: Stream[F, TemperaturesSummary] = {\n  for {\n    client      \u003c- Stream.eval(clientF)\n    temperature \u003c- client.getTemperature(Empty)\n    _           \u003c- Stream.eval(L.info(s\"* Received new temperature: 👍 --\u003e $temperature\"))\n  } yield temperature\n}.fold(TemperaturesSummary.empty)((summary, temperature) =\u003e summary.append(temperature))\n```\n\nBasically, we are logging the incoming values and at the end we calculate the average of those values.\n\nNow, the client app calls to both services: `isEmpty` and `getTemperature`.\nAnd finally, to call it:\n\n```scala\nfor {\n  serviceApi  \u003c- SmartHomeServiceApi.createInstance(config.host.value, config.port.value)\n  _           \u003c- Stream.eval(serviceApi.isEmpty)\n  summary     \u003c- serviceApi.getTemperature\n  _           \u003c- Stream.eval(Logger[F].info(s\"The average temperature is: ${summary.averageTemperature}\"))\n} yield StreamApp.ExitCode.Success\n```\n\n### Result\n\nWhen we run the client now with `sbt runClient` we get:\n\n```bash\nINFO  - Created new RPC client for (localhost,19683)\nINFO  - Result: IsEmptyResponse(true)\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.0,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.25,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.58,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(78.02,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.67,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.5,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.58,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(77.15,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.66,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.45,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.77,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.74,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.41,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.59,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.77,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.49,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.04,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(76.42,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(75.95,TemperatureUnit(Fahrenheit))\nINFO  - * Received new temperature: 👍  --\u003e Temperature(75.97,TemperatureUnit(Fahrenheit))\nINFO  - The average temperature is: Temperature(76.85,TemperatureUnit(Fahrenheit))\nINFO  - Removed 1 RPC clients from cache.\n```\n\nAnd the server log the request as expected:\n\n```bash\nINFO  - ServiceName(seedServer) - Starting app.server at Host(localhost):Port(19683)\nINFO  - SmartHomeService - Request: IsEmptyRequest()\nINFO  - SmartHomeService - getTemperature Request\nINFO  - * New Temperature 👍  --\u003e Temperature(77.0,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.25,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.58,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(78.02,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.67,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.5,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.58,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(77.15,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.66,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.45,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.77,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.74,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.41,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.59,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.77,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.49,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.04,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(76.42,TemperatureUnit(Fahrenheit))\nINFO  - * New Temperature 👍  --\u003e Temperature(75.95,TemperatureUnit(Fahrenheit))\n```\n\n## Bidirectional streaming RPC service: `comingBackMode`\n\nTo illustrate the bidirectional streaming, we are going to build a new service that makes the server react to real-time info provided by the client. In this case, as we said above, the client (the mobile app) will emit a stream of coordinates (latitude and longitude), and the server (the smart home) will trigger some actions according to the distance.\n\n### Protocol\n\nSo let's add this service to the protocol.\n\n**_Messages.scala_**\n\nAdding new models:\n\n```scala\ncase class Point(lat: Double, long: Double)\ncase class Location(currentLocation: Point, destination: Point, distanceToDestination: Double)\ncase class SmartHomeAction(description: String, isDone: Boolean)\n@message\nfinal case class ComingBackModeResponse(actions: List[SmartHomeAction])\n```\n\n**_SmartHomeService.scala_**\n\nAnd the `comingBackMode` operation:\n\n```scala\n@service(Protobuf) trait SmartHomeService[F[_]] {\n\n  def isEmpty(request: IsEmptyRequest): F[IsEmptyResponse]\n\n  def getTemperature(empty: Empty.type): Stream[F, Temperature]\n\n  def comingBackMode(request: Stream[F, Location]): Stream[F, ComingBackModeResponse]\n}\n```\n\n### Server\n\nSo there is a new function to be implemented in the interpreter:\n\n```scala\noverride def comingBackMode(request: Stream[F, Location]): Stream[F, ComingBackModeResponse] =\n  for {\n    _        \u003c- Stream.eval(Logger[F].info(s\"$serviceName - Enabling Coming Back Home mode\"))\n    location \u003c- request\n    _ \u003c- Stream.eval(\n      if (location.distanceToDestination \u003e 0.0d) Logger[F].info(s\"$serviceName - Distance to destination: ${location.distanceToDestination} mi\")\n      else Logger[F].info(s\"$serviceName - You have reached your destination 🏡\"))\n    response \u003c- Stream.eval(SmartHomeSupervisor[F].performAction(location))\n  } yield response\n```\n\n### Client\n\nAgain, if the client will emit a stream of locations, we should develop a producer, which as been created at `LocationGenerators`. No big deal, so far. But we have to add this operation in the `SmartHomeServiceApi`:\n\n```scala\ntrait SmartHomeServiceApi[F[_]] {\n  def isEmpty(): F[Boolean]\n  def getTemperature(): Stream[F, Temperature]\n  def comingBackMode(locations: Stream[F, Location]): F[Boolean]\n}\n```\n\nWhose interpretation could be:\n\n```scala\ndef comingBackMode(locations: Stream[F, Location]): Stream[F, ComingBackModeResponse] = for {\n  client   \u003c- Stream.eval(clientF)\n  response \u003c- client.comingBackMode(locations)\n} yield response\n```\n\nNow, we have all the ingredients to proceed in the ClientApp:\n\n```scala\nfor {\n  serviceApi \u003c- SmartHomeServiceApi.createInstance(config.host.value, config.port.value)\n  _          \u003c- Stream.eval(serviceApi.isEmpty)\n  summary    \u003c- serviceApi.getTemperature\n  _          \u003c- Stream.eval(Logger[F].info(s\"The average temperature is: ${summary.averageTemperature}\"))\n  response   \u003c- serviceApi.comingBackMode(LocationsGenerator.get[F])\n} yield response.actions\n```\n\n### Result\n\nWhen we run the client now with `sbt runClient` we get:\n\n```bash\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👮 - Enable security cameras\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 💦 - Disable irrigation system\n\nINFO  - 🔌 - Send Rumba to the charging dock\n\nINFO  - 🛋 - Start heating the living room\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 🔥 - Fireplace in ambient mode\n\nINFO  - 🗞 - Get news summary\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 💧 - Increase the power of the hot water heater\n\nINFO  - 🛁 - Turn the towel heaters on\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 😎 - Low the blinds\n\nINFO  - 💡 - Turn on the lights\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👩 - Connect Alexa\n\nINFO  - 📺 - Turn on the TV\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 🔦 - Turn exterior lights on\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 👀 - Waiting for a new location...\n\nINFO  - 🚪 - Unlock doors\n\nINFO  - Removed 1 RPC clients from cache.\n```\n\nAnd the server log the request as expected:\n\n```bash\nINFO  - SmartHomeService - Enabling Coming Back Home mode\nINFO  - SmartHomeService - Distance to destination: 6.39 mi\nINFO  - SmartHomeService - Distance to destination: 6.26 mi\nINFO  - SmartHomeService - Distance to destination: 6.13 mi\nINFO  - SmartHomeService - Distance to destination: 6.0 mi\nINFO  - SmartHomeService - Distance to destination: 5.87 mi\nINFO  - SmartHomeService - Distance to destination: 5.74 mi\nINFO  - SmartHomeService - Distance to destination: 5.61 mi\nINFO  - SmartHomeService - Distance to destination: 5.48 mi\nINFO  - SmartHomeService - Distance to destination: 5.35 mi\nINFO  - SmartHomeService - Distance to destination: 5.22 mi\nINFO  - SmartHomeService - Distance to destination: 5.09 mi\nINFO  - SmartHomeService - Distance to destination: 4.96 mi\nINFO  - SmartHomeService - Distance to destination: 4.83 mi\nINFO  - SmartHomeService - Distance to destination: 4.7 mi\nINFO  - SmartHomeService - Distance to destination: 4.57 mi\nINFO  - SmartHomeService - Distance to destination: 4.44 mi\nINFO  - SmartHomeService - Distance to destination: 4.31 mi\nINFO  - SmartHomeService - Distance to destination: 4.18 mi\nINFO  - SmartHomeService - Distance to destination: 4.05 mi\nINFO  - SmartHomeService - Distance to destination: 3.91 mi\nINFO  - SmartHomeService - Distance to destination: 3.78 mi\nINFO  - SmartHomeService - Distance to destination: 3.65 mi\nINFO  - SmartHomeService - Distance to destination: 3.52 mi\nINFO  - SmartHomeService - Distance to destination: 3.39 mi\nINFO  - SmartHomeService - Distance to destination: 3.26 mi\nINFO  - SmartHomeService - Distance to destination: 3.13 mi\nINFO  - SmartHomeService - Distance to destination: 3.0 mi\nINFO  - SmartHomeService - Distance to destination: 2.87 mi\nINFO  - SmartHomeService - Distance to destination: 2.74 mi\nINFO  - SmartHomeService - Distance to destination: 2.61 mi\nINFO  - SmartHomeService - Distance to destination: 2.48 mi\nINFO  - SmartHomeService - Distance to destination: 2.35 mi\nINFO  - SmartHomeService - Distance to destination: 2.22 mi\nINFO  - SmartHomeService - Distance to destination: 2.09 mi\nINFO  - SmartHomeService - Distance to destination: 1.96 mi\nINFO  - SmartHomeService - Distance to destination: 1.83 mi\nINFO  - SmartHomeService - Distance to destination: 1.7 mi\nINFO  - SmartHomeService - Distance to destination: 1.57 mi\nINFO  - SmartHomeService - Distance to destination: 1.44 mi\nINFO  - SmartHomeService - Distance to destination: 1.3 mi\nINFO  - SmartHomeService - Distance to destination: 1.17 mi\nINFO  - SmartHomeService - Distance to destination: 1.04 mi\nINFO  - SmartHomeService - Distance to destination: 0.91 mi\nINFO  - SmartHomeService - Distance to destination: 0.78 mi\nINFO  - SmartHomeService - Distance to destination: 0.65 mi\nINFO  - SmartHomeService - Distance to destination: 0.52 mi\nINFO  - SmartHomeService - Distance to destination: 0.39 mi\nINFO  - SmartHomeService - Distance to destination: 0.26 mi\nINFO  - SmartHomeService - Distance to destination: 0.13 mi\nINFO  - SmartHomeService - You have reached your destination 🏡\n```\n\n\u003c!-- DOCTOC SKIP --\u003e\n# Copyright\n\nFreestyle-RPC is designed and developed by 47 Degrees\n\nCopyright (C) 2017 47 Degrees. \u003chttp://47deg.com\u003e\n\n[comment]: # (End Copyright)","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrees-io%2Ffrees-rpc-lambda-world-workshop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffrees-io%2Ffrees-rpc-lambda-world-workshop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffrees-io%2Ffrees-rpc-lambda-world-workshop/lists"}