[
  {
    "path": ".github/workflows/scala.yml",
    "content": "# This workflow uses actions that are not certified by GitHub.\n# They are provided by a third-party and are governed by\n# separate terms of service, privacy policy, and support\n# documentation.\n\nname: Scala CI\n\non:\n  workflow_dispatch:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\npermissions:\n  contents: read\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v3\n    - name: Set up JDK 11\n      uses: actions/setup-java@v3\n      with:\n        java-version: '11'\n        distribution: 'temurin'\n        cache: 'sbt'\n    - name: Run tests\n      run: sbt test\n      # Optional: This step uploads information to the GitHub dependency graph and unblocking Dependabot alerts for the repository\n    - name: Upload dependency graph\n      uses: scalacenter/sbt-dependency-submission@ab086b50c947c9774b70f39fc7f6e20ca2706c91\n"
  },
  {
    "path": ".gitignore",
    "content": "project/zecret\nproject/travis-deploy-key\nproject/secrets.tar.xz\ntarget\ntest-output/\n.sbtopts\nproject/.sbt\ntest-output/\nlocal.*\n.idea\n\n# if you are here to add your IDE's files please read this instead:\n# https://stackoverflow.com/questions/7335420/global-git-ignore#22885996\nwebsite/node_modules\nwebsite/build\nwebsite/i18n/en.json\n"
  },
  {
    "path": ".scalafmt.conf",
    "content": "version = \"3.0.7\"\n\nmaxColumn = 120\nalign {\n  preset = more\n  tokens = [{code = \"->\"}, {code = \"<-\"}, {code = \"=>\", owners = [{\n      regex = \"Case\"\n  }]}, {code = \"%\"}, {code = \"%%\"}]\n  openParenCallSite = false\n  openParenDefnSite = false\n}\n\nindent {\n  significant = 2\n  defnSite = 2\n  extendSite = 2\n}\n\noptIn.annotationNewlines = true\n\nlineEndings = preserve\n\nspaces {\n  inImportCurlyBraces = true\n}\n\ndanglingParentheses.preset = true\n\ndocstrings.style = Asterisk\n\nincludeCurlyBraceInSelectChains = false\n\nassumeStandardLibraryStripMargin = true\n\nrewrite.rules = [SortImports, RedundantBraces]\n"
  },
  {
    "path": ".travis.yml",
    "content": "sudo: false\n\nlanguage: scala\n\nmatrix:\n  include:\n    # Scala 2.12, JVM\n    - scala: 2.12.17\n      jdk: openjdk8\n    - scala: 2.12.17\n      jdk: openjdk11\n    # Scala 2.13, JVM\n    - scala: 2.13.9\n      jdk: openjdk8\n    - scala: 2.13.9\n      jdk: openjdk11\n    # Scala 3.0, JVM\n    - scala: 3.1.3\n      jdk: openjdk8\n    - scala: 3.1.3\n      jdk: openjdk11\n\nbranches:\n  only:\n    - master\n    -  /^v.*/\n\nscript:\n- sbt clean coverage test coverageReport coverageOff\n\nbefore_install:\n- if [ $TRAVIS_PULL_REQUEST = 'false' ]; then\n  openssl aes-256-cbc -K $encrypted_8febf8bb1214_key -iv $encrypted_8febf8bb1214_iv -in travis/secrets.tar.enc -out travis/secrets.tar -d;\n  ls travis;\n  tar xv -C travis -f travis/secrets.tar;\n  fi\n\ncache:\n  directories:\n    - $HOME/.sbt/1.0/dependency\n    - $HOME/.sbt/boot/scala*\n    - $HOME/.sbt/launchers\n    - $HOME/.rvm\n    - website/node_modules\n\nbefore_cache:\n  - find $HOME/.sbt -name \"*.lock\" -type f -delete\n  - find $HOME/.ivy2/cache -name \"ivydata-*.properties\" -type f -delete\n  - rm -rf $HOME/.ivy2/local\n\nenv:\n  global:\n  - secure: e1WmzDGzT3JUwCWWFyLS7IBEHYJUcCcaoip/0gMcdmlhB8g5dAjWGo8Sj3ofylRcyb+0quGZZNkh//ByOSsvig0tyZBOTSUA+R62LcXfkurpxlr21fopXMhJ6CZxU6YxVgYn7QEKbvcct5QCTz8E6trp1CuuwiqMEt5joh6h5D3AESbFz4ZB8EsdMhVDJAiGK+jBaLSaoTnzbhIvXDttl4k+CuRdQ5YGUbrFyYEJsDHj2ms30NQggdvaArqSflCPtZxREDgweuWcBK9HzyZD51+sLEFG/CH0MhcYFAtmFVdD3JeaOaE33au7PXHOpyVKErhqbr7ZhhB9vBfsWitnasZXWqAS6TBWekMp/2GLDFz+QWMOgfgg6t//SvdNFe07biZO5NiJ52l+kbxm+BswA80XXZ6Nrbs2VIjdvasxUUJ9ud1hvwMtMuIrwJcG3Jg0sxOaXdwmLahpn6BIw8nYyOhDDvtr0utbUkhMtNWv23MzWnwU1+BVCcFglH0KFyG30i3cSe6S73CRJv6ILE0xRjXVCGstZAYq0we9lA5NcQtPYQlAtsunTfyd6XR1RFn4cQaCtMVrGwG2XUz5Stq3y0Hd9P+wBaZdstl17dEgjHMdP758N8dsR/8saOt3w4i5ktsS8f9LACvGfW5a09VnQWkowDgKokSF8lmn0CPxDa0=\n  - secure: kWcc2SnYmlbZAGUYifr1nb1m1i+iITUcxWELzokSkKFIIc870WFXyvu3VK9pxShiTDehg4shRnp+Ygfynbn44WI4guuzMQLuSlhxt0uDZ6UmLnSOLOpCOfpT6WuVQkiaxoETyNZ7u8bnZqmFSQWfSoH8PWdWBBdON8hD7DsAmvXj6hgIn8jmstq8dgLqqboDH7RGEIpgWIXB2a203Hoe/gYOXtXOm9LyRK1/RCOnGNxkAowNhKVpEvZKjp5P2is7i7bgQEKzagBtj977WnFe6TNJjhB49yrSDGJ2W7Nf0L1BWKtUiTGqeyDuaBSBMxUyzQaMn4zcVHHNMcWogbYPnsACBzmbd3ZfJu8WM6JLtLFIwMtdj9sl7UDc/hin/KlUOfi/FvJZSNYqGiIxj5dwVlmZg9g+oaz/9L4bMOZ9p6PmIYSHOApQg0v6MJMQfeKrlKcpMLA0FcZ0597nTJEy6UOBlMYlVxTgWiCUNDFALsWqOblNgoB1Z9aAQ8RT3GwOaMj4UXFXJbUK9Q2CqdYEi/DFi8Hhjyh0Th3a6cgLIq+18G5OrHASX8odecAFq6J8+eZO+SW+WUJFB6D3loLbM/S436SKB8VH7JeVBlVjPA+HOA6eAfC0mHVupKm0sPTNW4s00rNSQfQvBlmKe8v5sQtAKROu3rJ7rHn1uNAOQOE=\n  - secure: ctGTO3DWvc1tG3mGWg+ffb+/Cw8zl6QNjtCzDaSv/KY5jo0a/+hBOMQjjhn+oSvFNhYfV1Ykc+Ym+mdrJRuVvi2QuABvI6gKhT3c9HOt7Al6/4Q1gEnSf0YdEhF/ZRVp4+4CmG9luk/OtHwRBtXOWSS7+b7RKCs5+Homy645oNuB3OJs9N0G3rAQ58riuLTtNTPQcQvNbzULc8CkP056mdGxjMBykp85X2mRqVDaEp0Gg7WaVxJnKIZt5eFJy1dll2xcPyJVojYVRnsObUHXcUgeEa9aNA+Vcwrpy9pL37jR18rExLWkf/zRYCi2fK3C48/rFmIqTg+q8O6h5MlupS5uZeQmLoJzr2l9okZg9FF22ONDNWJc614U2M3VzbDGOJ4Hw5rEpaFECHdZZ9omMRsuGambGMStcrt5GFwLZ5ANJOY+kB2od8C60Bk4kVSUJpFlsGUb3a/kSlvYE+tjlL0rMS2abQcLoEOypILm7vxNPd4NBDXPOX2tRKnKE+bv1gxmoQYmEVNofF8taKxHFd6K1o2H7FmEvNOt27iqXtTrI8TTOudP39raZwjWFeTNvxeIPewjAhbeRX/tB+QwVT1cuEgc/zPwq58K3aYU1dewZn3acepp00qrA8xWL1de+t73yiCDBj0bwKKMRA0BuawO3Al7Pugv1Po/4AmBbNQ=\n\nafter_success:\n- bash <(curl -s https://codecov.io/bash)\n- if [ $TRAVIS_PULL_REQUEST = 'false' ]; then sbt ++$TRAVIS_SCALA_VERSION releaseEarly;\n  fi"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Kopaniev Vladyslav\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ZIO-SAGA\n\n> [!WARNING]\n> This project is no longer supported. For implementing real world sagas consider workflow orchestration tools like Temporal that has available libraries for Scala e.g. [zio-temporal](https://github.com/vitaliihonta/zio-temporal). Also feel free to fork this repository and modify it for your own needs.\n\n[![Support Ukraine](https://img.shields.io/static/v1?label=United24&message=Support%20Ukraine&color=lightgrey&link=https%3A%2F%2Fu24.gov.ua&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAASwAAADICAYAAABS39xVAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAANKSURBVHhe7dZBThRhFEbRnx1IgvtFiIoxbgemOHLAhAoJ1QyaBahroKxqE%2BMS6iZncPKSbwE3b4yr6W58en4Z148zwC5tjbqabrdgvZ59PS5nn2eAfVobtbbquAXrcBquJ4B9Wht1%2BrQEC9g9wQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8gQLCBDsIAMwQIyBAvIECwgQ7CADMECMgQLyBAsIEOwgAzBAjIEC8j4F6zL%2BTA%2BHpfxYR0A9mhr1OXzPC5u7g%2Fvv%2F1YLr58B9ilU6Nu7ufx6%2BH88Hs6X9YLsEtbo34%2BvJvH29M4LC9jWZ4Admpt1NqqNVjTGqz5bFkmgJ1aG%2FX2KFhAgWABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVkCBaQIVhAhmABGYIFZAgWkCFYQIZgARmCBWQIFpAhWECGYAEZggVk%2FBes1%2BX4dwDYpbVRa6uOW7Du3p7Hy1YvgF3aGjWN2z9qCgwkg1n6XwAAAABJRU5ErkJggg%3D%3D)](https://u24.gov.ua)\n[![badge-scala-ukraine](https://img.shields.io/badge/Scala-Ukraine-EBD038?labelColor=4172CC)](https://t.me/scala_ukraine)\n| CI | Coverage | Release |  |\n| --- | --- | --- | --- |\n| [![Build Status][Badge-Travis]][Link-Travis] | [![Coverage Status][Badge-Codecov]][Link-Codecov] | [![Release Artifacts][Badge-SonatypeReleases]][Link-SonatypeReleases] | [![Scala Steward badge][Badge-ScalaSteward]][Link-ScalaSteward] |\n\nBuild your transactions in purely functional way.\n\nzio-saga allows you to compose your requests and compensating actions from Saga pattern in one transaction\nwithout any boilerplate.\n\n\nBacked by ZIO it adds a simple abstraction called Saga that takes the responsibility of\nproper composition of effects and associated compensating actions.\n\n# Getting started\n\nAdd zio-saga dependency to your `build.sbt`:\n\n`libraryDependencies += \"com.vladkopanev\" %% \"zio-saga-core\" % \"0.4.0\"`\n\n# Example of usage:\n\nConsider the following case, we have built our food delivery system in microservices fashion, so\nwe have `Order` service, `Payment` service, `LoyaltyProgram` service, etc. \nAnd now we need to implement a closing order method, that collects *payment*, assigns *loyalty* points \nand closes the *order*. This method should run transactionally so if e.g. *closing order* fails we will \nrollback the state for user and *refund payments*, *cancel loyalty points*.\n\nApplying Saga pattern we need a compensating action for each call to particular microservice, those \nactions needs to be run for each completed request in case some of the requests fails.\n\n![Order Saga Flow](./images/diagrams/Order%20Saga%20Flow.jpeg)\n\nLet's think for a moment about how we could implement this pattern without any specific libraries.\n\nThe naive implementation could look like this:\n\n```scala\ndef orderSaga(): IO[SagaError, Unit] = {\n    for {\n      _ <- collectPayments(2d, 2) orElse refundPayments(2d, 2)\n      _ <- assignLoyaltyPoints(1d, 1) orElse cancelLoyaltyPoints(1d, 1)\n      _ <- closeOrder(1) orElse reopenOrder(1)\n    } yield ()\n  }\n```\n\nLooks pretty simple and straightforward, `orElse` function tries to recover the original request if it fails.\nWe have covered every request with a compensating action. But what if last request fails? We know for sure that corresponding \ncompensation `reopenOrder` will be executed, but when other compensations would be run? Right, they would not be triggered, \nbecause the error would not be propagated higher, thus not triggering compensating actions. That is not what we want, we want \nfull rollback logic to be triggered in Saga, whatever error occurred.\n \nSecond try, this time let's somehow trigger all compensating actions.\n  \n```scala\ndef orderSaga(): IO[SagaError, Unit] = {\n    collectPayments(2d, 2).flatMap { _ = >\n        assignLoyaltyPoints(1d, 1).flatMap { _ => \n            closeOrder(1) orElse(reopenOrder(1)  *> IO.fail(new SagaError))\n        } orElse (cancelLoyaltyPoints(1d, 1)  *> IO.fail(new SagaError))\n    } orElse(refundPayments(2d, 2) *> IO.fail(new SagaError))\n  }\n```\n\nThis works, we trigger all rollback actions by failing after each. \nBut the implementation itself looks awful, we lost expressiveness in the call-back hell, imagine 15 saga steps implemented in such manner,\nand we also lost the original error that we wanted to show to the user.\n\nYou can solve this problems in different ways, but you will encounter a number of difficulties, and your code still would \nlook pretty much the same as we did in our last try. \n\nAchieve a generic solution is not that simple, so you will end up\nrepeating the same boilerplate code from service to service.\n\n`zio-saga` tries to address this concerns and provide you with simple syntax to compose your Sagas.\n\nWith `zio-saga` we could do it like so:\n\n```scala\ndef orderSaga(): IO[SagaError, Unit] = {\n    import com.vladkopanev.zio.saga.Saga._\n\n    (for {\n      _ <- collectPayments(2d, 2) compensate refundPayments(2d, 2)\n      _ <- assignLoyaltyPoints(1d, 1) compensate cancelLoyaltyPoints(1d, 1)\n      _ <- closeOrder(1) compensate reopenOrder(1)\n    } yield ()).transact\n  }\n```\n\n`compensate` pairs request IO with compensating action IO and returns a new `Saga` object which then you can compose with other\n`Sagas`.\nTo materialize `Saga` object to `ZIO` when it's complete it is required to use `transact` method.\n\nAs you can see with `zio-saga` the process of building your Sagas is greatly simplified comparably to ad-hoc solutions. \nZIO-Sagas are composable, boilerplate-free and intuitively understandable for people that aware of Saga pattern.\nThis library let you compose transaction steps both in sequence and in parallel, this feature gives you more powerful control \nover transaction execution.\n\n# Advanced\n\nAdvanced example of working application that stores saga state in DB (journaling) could be found \nhere [examples](/examples).\n\n### Retrying \n`zio-saga` provides you with functions for retrying your compensating actions, so you could \nwrite:\n\n ```scala\ncollectPayments(2d, 2) retryableCompensate (refundPayments(2d, 2), Schedule.exponential(1.second))\n```\n\nIn this example your Saga will retry compensating action `refundPayments` after exponentially \nincreasing timeouts (based on `ZIO#retry` and `ZSchedule`).\n\n\n### Parallel execution\nSaga pattern does not limit transactional requests to run only in sequence.\nBecause of that `zio-saga` contains methods for parallel execution of requests. \n\n```scala\n    val flight          = bookFlight compensate cancelFlight\n    val hotel           = bookHotel compensate cancelHotel\n    val bookingSaga     = flight zipPar hotel\n```\n\nNote that in this case two compensations would run in sequence, one after another by default.\nIf you need to execute compensations in parallel consider using `Saga#zipWithParAll` function, it allows arbitrary \ncombinations of compensating actions.\n\n### Result dependent compensations\n\nDepending on the result of compensable effect you may want to execute specific compensation, for such cases `zio-saga`\ncontains specific functions:\n- `compensate(compensation: Either[E, A] => Compensator[R, E])` this function makes compensation dependent on the result \nof corresponding effect that either fails or succeeds.\n- `compensateIfFail(compensation: E => Compensator[R, E])` this function makes compensation dependent only on error type \nhence compensation will only be triggered if corresponding effect fails.\n- `compensateIfSuccess(compensation: A => Compensator[R, E])` this function makes compensation dependent only on\nsuccessful result type hence compensation can only occur if corresponding effect succeeds.\n\n### Notes on compensation action failures\n\nBy default, if some compensation action fails no other compensation would run and therefore user has the ability to \nchoose what to do: stop compensation (by default), retry failed compensation step until it succeeds or proceed to next \ncompensation steps ignoring the failure.\n\n### Cats Compatible Sagas\n\n[cats-saga](https://github.com/VladKopanev/cats-saga)\n\n[Link-Codecov]: https://codecov.io/gh/VladKopanev/zio-saga?branch=master \"Codecov\"\n[Link-Travis]: https://travis-ci.com/VladKopanev/zio-saga \"circleci\"\n[Link-SonatypeReleases]: https://oss.sonatype.org/content/repositories/releases/com/vladkopanev/zio-saga-core_2.12/ \"Sonatype Releases\"\n[Link-ScalaSteward]: https://scala-steward.org\n\n[Badge-Codecov]: https://codecov.io/gh/VladKopanev/zio-saga/branch/master/graph/badge.svg \"Codecov\" \n[Badge-Travis]: https://travis-ci.com/VladKopanev/zio-saga.svg?branch=master \"Codecov\" \n[Badge-SonatypeReleases]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.vladkopanev/zio-saga-core_2.11.svg \"Sonatype Releases\"\n[Badge-ScalaSteward]: https://img.shields.io/badge/Scala_Steward-helping-brightgreen.svg?style=flat&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAQCAMAAAARSr4IAAAAVFBMVEUAAACHjojlOy5NWlrKzcYRKjGFjIbp293YycuLa3pYY2LSqql4f3pCUFTgSjNodYRmcXUsPD/NTTbjRS+2jomhgnzNc223cGvZS0HaSD0XLjbaSjElhIr+AAAAAXRSTlMAQObYZgAAAHlJREFUCNdNyosOwyAIhWHAQS1Vt7a77/3fcxxdmv0xwmckutAR1nkm4ggbyEcg/wWmlGLDAA3oL50xi6fk5ffZ3E2E3QfZDCcCN2YtbEWZt+Drc6u6rlqv7Uk0LdKqqr5rk2UCRXOk0vmQKGfc94nOJyQjouF9H/wCc9gECEYfONoAAAAASUVORK5CYII=\n"
  },
  {
    "path": "build.sbt",
    "content": "import com.typesafe.sbt.SbtPgp.autoImportImpl.pgpSecretRing\nimport sbt.file\n\nname := \"zio-saga\"\n\nval mainScala = \"2.13.9\"\nval allScala = Seq(\"2.12.17\", mainScala, \"3.1.3\")\n\ninThisBuild(\n  List(\n    organization := \"com.vladkopanev\",\n    homepage := Some(url(\"https://github.com/VladKopanev/zio-saga\")),\n    licenses := List(\"MIT License\" -> url(\"https://opensource.org/licenses/MIT\")),\n    developers := List(\n      Developer(\n        \"VladKopanev\",\n        \"Vladislav Kopanev\",\n        \"ivengo53@gmail.com\",\n        url(\"http://vladkopanev.com\")\n      )\n    ),\n    scmInfo := Some(\n      ScmInfo(url(\"https://github.com/VladKopanev/zio-saga\"), \"scm:git:git@github.com/VladKopanev/zio-saga.git\")\n    ),\n    pgpPublicRing := file(\"./travis/local.pubring.asc\"),\n    pgpSecretRing := file(\"./travis/local.secring.asc\"),\n    releaseEarlyWith := SonatypePublisher\n  )\n)\n\nlazy val commonSettings = Seq(\n  scalaVersion := mainScala,\n  scalacOptions ++= Seq(\n    \"-deprecation\",\n    \"-encoding\",\n    \"UTF-8\",\n    \"-explaintypes\",\n    \"-Yrangepos\",\n    \"-feature\",\n    \"-Xfuture\",\n    \"-language:higherKinds\",\n    \"-language:existentials\",\n    \"-language:implicitConversions\",\n    \"-unchecked\",\n    \"-Xlint:_,-type-parameter-shadow\",\n    \"-Ywarn-numeric-widen\",\n    \"-Ywarn-unused\",\n    \"-Ywarn-value-discard\"\n  ) ++ (CrossVersion.partialVersion(scalaVersion.value) match {\n    case Some((3, _)) =>\n      Seq(\"-Ykind-projector\", \"-unchecked\")\n    case Some((2, 12)) =>\n      Seq(\n        \"-Xsource:2.13\",\n        \"-Yno-adapted-args\",\n        \"-Ypartial-unification\",\n        \"-Ywarn-extra-implicit\",\n        \"-Ywarn-inaccessible\",\n        \"-Ywarn-infer-any\",\n        \"-Ywarn-nullary-override\",\n        \"-Ywarn-nullary-unit\",\n        \"-opt-inline-from:<source>\",\n        \"-opt-warnings\",\n        \"-opt:l:inline\"\n      )\n    case _ => Nil\n  }),\n  resolvers ++= Resolver.sonatypeOssRepos(\"snapshots\") ++ Resolver.sonatypeOssRepos(\"releases\")\n)\n\nlazy val root = project\n  .in(file(\".\"))\n  .aggregate(core)\n\nlazy val core = project\n  .in(file(\"core\"))\n  .settings(\n    commonSettings,\n    name := \"zio-saga-core\",\n    crossScalaVersions := allScala,\n    libraryDependencies ++= Seq(\n      \"dev.zio\" %% \"zio\"          % Versions.Zio,\n      \"dev.zio\" %% \"zio-test\"     % Versions.Zio % \"test\",\n      \"dev.zio\" %% \"zio-test-sbt\" % Versions.Zio % \"test\"\n    ),\n    testFrameworks := Seq(new TestFramework(\"zio.test.sbt.ZTestFramework\"))\n  )\n\nlazy val examples = project\n  .in(file(\"examples\"))\n  .settings(\n    commonSettings,\n    scalaVersion := mainScala,\n    coverageEnabled := false,\n    libraryDependencies ++= Seq(\n      \"ch.qos.logback\" % \"logback-classic\"     % \"1.2.10\",\n      \"dev.zio\"       %% \"zio-interop-cats\"    % \"3.2.9.0\",\n      \"org.typelevel\" %% \"log4cats-core\"       % Versions.Log4Cats,\n      \"org.typelevel\" %% \"log4cats-slf4j\"      % Versions.Log4Cats,\n      \"io.circe\"      %% \"circe-generic\"       % Versions.Circe,\n      \"io.circe\"      %% \"circe-parser\"        % Versions.Circe,\n      \"org.http4s\"    %% \"http4s-circe\"        % Versions.Http4s,\n      \"org.http4s\"    %% \"http4s-dsl\"          % Versions.Http4s,\n      \"org.http4s\"    %% \"http4s-blaze-server\" % Versions.Http4s,\n      \"org.tpolecat\"  %% \"doobie-core\"         % Versions.Doobie,\n      \"org.tpolecat\"  %% \"doobie-hikari\"       % Versions.Doobie,\n      \"org.tpolecat\"  %% \"doobie-postgres\"     % Versions.Doobie,\n      // compilerPlugin(\"org.scalamacros\"  %% \"paradise\"           % \"2.1.0\"),\n      compilerPlugin(\"org.typelevel\" %% \"kind-projector\"     % \"0.13.2\" cross CrossVersion.full),\n      compilerPlugin(\"com.olegpy\"    %% \"better-monadic-for\" % \"0.3.1\")\n    )\n  )\n  .dependsOn(core % \"compile->compile\")\n"
  },
  {
    "path": "core/src/main/scala/com/vladkopanev/zio/saga/Saga.scala",
    "content": "package com.vladkopanev.zio.saga\n\nimport com.vladkopanev.zio.saga.Saga.Compensator\nimport zio.Clock\nimport zio.{Cause, Exit, Fiber, IO, RIO, Schedule, Task, UIO, ZIO}\n\n/**\n * A Saga is an immutable structure that models a distributed transaction.\n *\n * @see [[https://blog.couchbase.com/saga-pattern-implement-business-transactions-using-microservices-part/ Saga pattern]]\n *\n * Saga class has three type parameters - R for environment, E for errors and A for successful result.\n * Saga wraps a ZIO that carries the compensating action in both error and result channels and enables a composition\n * with another Sagas in for-comprehensions.\n * If error occurs Saga will execute compensating actions starting from action that corresponds to failed request\n * till the first already completed request.\n * */\nfinal class Saga[-R, +E, +A] private (\n  private val request: ZIO[R, (E, Compensator[R, E]), (A, Compensator[R, E])]\n) extends AnyVal {\n  self =>\n\n  /**\n   * Maps the resulting value `A` of this Saga to value `B` with function `f`.\n   * */\n  def map[B](f: A => B): Saga[R, E, B] =\n    new Saga(request.map { case (a, comp) => (f(a), comp) })\n\n  /**\n   * Sequences the result of this Saga to the next Saga.\n   * */\n  def flatMap[R1 <: R, E1 >: E, B](f: A => Saga[R1, E1, B]): Saga[R1, E1, B] =\n    new Saga(request.flatMap {\n      case (a, compA) =>\n        f(a).request.mapBoth(\n          { case (e, compB) => (e, compB *> compA) },\n          { case (r, compB) => (r, compB *> compA) }\n        )\n    })\n\n  /**\n   * Flattens the structure of this Saga by executing outer Saga first and then executes inner Saga.\n   * */\n  def flatten[R1 <: R, E1 >: E, B](implicit ev: A <:< Saga[R1, E1, B]): Saga[R1, E1, B] =\n    self.flatMap(r => ev(r))\n\n  /**\n   * Returns Saga that will execute this Saga in parallel with other, combining the result in a tuple.\n   * Both compensating actions would be executed in case of failure.\n   * */\n  def zipPar[R1 <: R, E1 >: E, B](that: Saga[R1, E1, B]): Saga[R1, E1, (A, B)] =\n    zipWithPar(that)((a, b) => (a, b))\n\n  /**\n   * Returns Saga that will execute this Saga in sequence with other, combining the result in a tuple.\n   * Only failed effect would be compensated.\n   * */\n  def zip[R1 <: R, E1 >: E, B](that: Saga[R1, E1, B]): Saga[R1, E1, (A, B)] =\n    zipWith(that)((a, b) => (a, b))\n\n  /**\n   * Returns Saga that will execute this Saga in parallel with other, combining the result with specified function `f`.\n   * Both compensating actions would be executed in case of failure.\n   * */\n  def zipWithPar[R1 <: R, E1 >: E, B, C](that: Saga[R1, E1, B])(f: (A, B) => C): Saga[R1, E1, C] =\n    zipWithParAll(that)(f)((a, b) => a *> b)\n\n  /**\n   * Returns Saga that will execute this Saga in sequence with other, combining the result with specified function `f`.\n   * Only failed effect would be compensated.\n   * */\n  def zipWith[R1 <: R, E1 >: E, B, C](that: Saga[R1, E1, B])(f: (A, B) => C): Saga[R1, E1, C] = (for {\n    thisResult <- this\n    thatResult <- that\n  } yield f(thisResult, thatResult))\n\n  /**\n   * Returns Saga that will execute this Saga in parallel with other, combining the result with specified function `f`\n   * and combining the compensating actions with function `g` (this allows user to choose a strategy of running both\n   * compensating actions e.g. in sequence or in parallel).\n   * */\n  def zipWithParAll[R1 <: R, E1 >: E, B, C](\n    that: Saga[R1, E1, B]\n  )(f: (A, B) => C)(g: (Compensator[R1, E1], Compensator[R1, E1]) => Compensator[R1, E1]): Saga[R1, E1, C] = {\n    def coordinate[A1, B1, C1](f: (A1, B1) => C1)(\n      fasterSaga: Exit[(E1, Compensator[R1, E1]), (A1, Compensator[R1, E1])],\n      slowerSaga: Fiber[(E1, Compensator[R1, E1]), (B1, Compensator[R1, E1])]\n    ): ZIO[R1, (E1, Compensator[R1, E1]), (C1, Compensator[R1, E1])] =\n      fasterSaga.foldZIO(\n        { cause =>\n          def extractCompensatorFrom(c: Cause[(E1, Compensator[R1, E1])]): Compensator[R1, E1] =\n            c.failures.headOption.map[Compensator[R1, E1]](_._2).getOrElse(ZIO.dieMessage(\"Compensator was lost\"))\n          /* we can't use interrupt here because we won't get a compensation action in case\n           IO was still running and interrupted */\n          slowerSaga.await.flatMap(\n            _.foldZIO(\n              { loserCause =>\n                val compA = extractCompensatorFrom(cause)\n                val compB = extractCompensatorFrom(loserCause)\n                ZIO.failCause((cause && loserCause).map { case (e, _) => (e, g(compB, compA)) })\n              }, { case (_, compB) => ZIO.failCause(cause.map { case (e, compA) => (e, g(compB, compA)) }) }\n            )\n          )\n        }, {\n          case (a, compA) =>\n            slowerSaga.join.mapBoth(\n              { case (e, compB) => (e, g(compB, compA)) },\n              { case (b, compB) => (f(a, b), g(compB, compA)) }\n            )\n        }\n      )\n\n    val h = (b: B, a: A) => f(a, b)\n    new Saga(request.raceWith(that.request)(coordinate(f), coordinate(h)))\n  }\n\n  /**\n   * Materializes this Saga to ZIO effect.\n   * */\n  def transact: ZIO[R, E, A] =\n    request.tapError({ case (e, compA) => compA.orElseFail((e, IO.unit)) }).mapBoth(_._1, _._1)\n}\n\nobject Saga {\n\n  type Compensator[-R, +E] = ZIO[R, E, Unit]\n\n  /**\n   * Constructs new Saga from action and compensating action.\n   * */\n  def compensate[R, E, A](request: ZIO[R, E, A], compensator: Compensator[R, E]): Saga[R, E, A] =\n    compensate(request, (_: Either[E, A]) => compensator)\n\n  /**\n   * Constructs new Saga from action and compensation function that will be applied the result of this request.\n   * */\n  def compensate[R, E, A](action: ZIO[R, E, A], compensation: Either[E, A] => Compensator[R, E]): Saga[R, E, A] =\n    new Saga(action.mapBoth(e => (e, compensation(Left(e))), a => (a, compensation(Right(a)))))\n\n  /**\n   * Constructs new Saga from action and compensation function that will be applied only to failed result of this request.\n   * If given action succeeds associated compensating action would not be executed during the compensation phase.\n   * */\n  def compensateIfFail[R, E, A](action: ZIO[R, E, A], compensation: E => Compensator[R, E]): Saga[R, E, A] =\n    compensate(action, (result: Either[E, A]) => result.fold(compensation, _ => ZIO.unit))\n\n  /**\n   * Constructs new Saga from action and compensation function that will be applied only to successful result of this request.\n   * If given action fails associated compensating action would not be executed during the compensation phase.\n   * */\n  def compensateIfSuccess[R, E, A](action: ZIO[R, E, A], compensation: A => Compensator[R, E]): Saga[R, E, A] =\n    compensate(action, (result: Either[E, A]) => result.fold(_ => ZIO.unit, compensation))\n\n  /**\n   * Runs all Sagas in iterable in parallel and collects\n   * the results.\n   */\n  def collectAllPar[R, E, A](sagas: Iterable[Saga[R, E, A]]): Saga[R, E, List[A]] =\n    foreachPar[R, E, Saga[R, E, A], A](sagas)(identity)\n\n  /**\n   * Runs all Sagas in iterable in sequence and collects the results.\n   */\n  def collectAll[R, E, A](sagas: Iterable[Saga[R, E, A]]): Saga[R, E, List[A]] =\n    foreach(sagas)(identity)\n\n  /**\n   * Runs all Sagas in iterable in parallel, and collect\n   * the results.\n   */\n  def collectAllPar[R, E, A](sagas: Saga[R, E, A]*): Saga[R, E, List[A]] =\n    collectAllPar(sagas)\n\n  /**\n   * Constructs Saga without compensation that fails with an error.\n   * */\n  def fail[R, E](error: E): Saga[R, E, Nothing] = noCompensate(ZIO.fail(error))\n\n  /**\n   * Constructs a Saga that applies the function `f` to each element of the `Iterable[A]` in parallel,\n   * and returns the results in a new `List[B]`.\n   *\n   */\n  def foreachPar[R, E, A, B](as: Iterable[A])(fn: A => Saga[R, E, B]): Saga[R, E, List[B]] =\n    as.foldRight[Saga[R, E, List[B]]](Saga.noCompensate(IO.succeed(Nil))) { (a, io) =>\n      fn(a).zipWithPar(io)((b, bs) => b :: bs)\n    }\n\n  /**\n   * Constructs a Saga that applies the function `f` to each element of the `Iterable[A]` in sequence, and returns the\n   * results in a new `List[B]`.\n   */\n  def foreach[R, E, A, B](as: Iterable[A])(fn: A => Saga[R, E, B]): Saga[R, E, List[B]] =\n    as.foldRight[Saga[R, E, List[B]]](Saga.noCompensate(IO.succeed(Nil))) { (a, io) =>\n      fn(a).zipWith(io)((b, bs) => b :: bs)\n    }\n\n  /**\n   * Constructs new `no-op` Saga that will do nothing on error.\n   * */\n  def noCompensate[R, E, A](request: ZIO[R, E, A]): Saga[R, E, A] = compensate(request, ZIO.unit)\n\n  /**\n   * Constructs new Saga from action, compensating action and a scheduling policy for retrying compensation.\n   * */\n  def retryableCompensate[R, SR, E, A](\n    request: ZIO[R, E, A],\n    compensator: Compensator[R, E],\n    schedule: Schedule[SR, E, Any]\n  ): Saga[R with SR with Clock, E, A] = {\n    val retry: Compensator[R with SR with Clock, E] = compensator.retry(schedule.unit)\n    compensate(request, retry)\n  }\n\n  /**\n   * Constructs Saga without compensation that succeeds with a strict value.\n   * */\n  def succeed[R, A](value: A): Saga[R, Nothing, A] = noCompensate(ZIO.succeed(value))\n\n  // $COVERAGE-OFF$\n  implicit def IOtoCompensable[E, A](io: IO[E, A]): Compensable[Any, E, A] = new Compensable(io)\n\n  implicit def ZIOtoCompensable[R, E, A](zio: ZIO[R, E, A]): Compensable[R, E, A] = new Compensable(zio)\n\n  implicit def UIOtoCompensable[A](uio: UIO[A]): Compensable[Any, Nothing, A] = new Compensable(uio)\n\n  implicit def RIOtoCompensable[R, A](rio: RIO[R, A]): Compensable[R, Throwable, A] = new Compensable(rio)\n\n  implicit def TaskToCompensable[A](task: Task[A]): Compensable[Any, Throwable, A] = new Compensable(task)\n  // $COVERAGE-ON$\n\n  /**\n   * Extension methods for IO requests.\n   * */\n  class Compensable[-R, +E, +A](val request: ZIO[R, E, A]) extends AnyVal {\n\n    def compensate[R1 <: R, E1 >: E](c: Compensator[R1, E1]): Saga[R1, E1, A] =\n      Saga.compensate(request, c)\n\n    def compensate[R1 <: R, E1 >: E](compensation: Either[E1, A] => Compensator[R1, E1]): Saga[R1, E1, A] =\n      Saga.compensate(request, compensation)\n\n    def compensateIfFail[R1 <: R, E1 >: E](compensation: E1 => Compensator[R1, E1]): Saga[R1, E1, A] =\n      Saga.compensateIfFail(request, compensation)\n\n    def compensateIfSuccess[R1 <: R, E1 >: E](compensation: A => Compensator[R1, E1]): Saga[R1, E1, A] =\n      Saga.compensateIfSuccess(request, compensation)\n\n    def retryableCompensate[R1 <: R, SR, E1 >: E](\n      c: Compensator[R1, E1],\n      schedule: Schedule[SR, E1, Any]\n    ): Saga[R1 with SR with Clock, E1, A] =\n      Saga.retryableCompensate(request, c, schedule)\n\n    def noCompensate: Saga[R, E, A] = Saga.noCompensate(request)\n  }\n\n}\n"
  },
  {
    "path": "core/src/test/scala/com/vladkopanev/zio/saga/SagaSpec.scala",
    "content": "package com.vladkopanev.zio.saga\n\nimport java.util.concurrent.TimeUnit\nimport com.vladkopanev.zio.saga.Saga.Compensator\nimport zio._\nimport zio.Clock\nimport zio.test._\nimport zio.test.Assertion._\nimport zio.test.TestEnvironment\nimport Saga._\nimport SagaSpecUtil._\nimport zio.Clock.currentTime\n\nobject SagaSpec\n    extends DefaultRunnableSpec {\n      override val spec: ZSpec[TestEnvironment, Any] = suite(\"SagaSpec\")(\n        suite(\"Saga#map\")(test(\"should change the result value with provided function\") {\n          assertM(Saga.compensate(ZIO.succeed(1), ZIO.unit).map(_.toString).transact)(equalTo(\"1\"))\n        }),\n        suite(\"Saga#zipPar\")(test(\"should successfully run two Sagas in parallel\") {\n          assertM((bookFlight compensate cancelFlight zipPar (bookHotel compensate cancelHotel)).transact)(equalTo((FlightPayment, HotelPayment)))\n        }),\n        suite(\"Saga#zip\")(test(\"should successfully run two Sagas in sequence\") {\n          assertM((bookFlight compensate cancelFlight zip (bookHotel compensate cancelHotel)).transact)(equalTo((FlightPayment, HotelPayment)))\n        }),\n        suite(\"Saga#zipWithPar\")(\n          test(\"should successfully run two Sagas in parallel\") {\n            for {\n              startTime <- currentTime(TimeUnit.MILLISECONDS).provideLayer(Clock.live)\n              _ <- (sleep(1000.millis) *> bookFlight compensate cancelFlight)\n                    .zipWithPar(sleep(1000.millis) *> bookHotel compensate cancelHotel)((_, _) => ())\n                    .transact\n              endTime <- currentTime(TimeUnit.MILLISECONDS).provideLayer(Clock.live)\n            } yield assert(endTime - startTime)(isLessThanEqualTo(3000L))\n          },\n          test(\"should run both compensating actions in case right request fails\") {\n            val bookFlightS = sleep(1000.millis) *> bookFlight\n            val failHotel   = sleep(100.millis) *> IO.fail(HotelBookingError())\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (bookFlightS compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))\n                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))((_, _) => ())\n                    .transact\n                    .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\")))\n          },\n          test(\"should run both compensating actions in case left request fails\") {\n            val bookFlightS = sleep(1000.millis) *> bookFlight\n            val failHotel   = sleep(100.millis) *> IO.fail(HotelBookingError())\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (failHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))\n                    .zipWithPar(bookFlightS compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))((_, _) =>\n                      ()\n                    )\n                    .transact\n                    .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\")))\n          },\n          test(\"should run both compensating actions in case both requests fails\") {\n            val failFlight = sleep(1000.millis) *> IO.fail(FlightBookingError())\n            val failHotel  = sleep(1000.millis) *> IO.fail(HotelBookingError())\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (failFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))\n                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))((_, _) => ())\n                    .transact\n                    .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assert(log)(hasSameElements(Vector(\"flight canceled\", \"hotel canceled\")))\n          },\n          test(\"should run compensating actions in order that is opposite to which requests finished\") {\n            val failFlight = sleep(1000.millis) *> IO.fail(FlightBookingError())\n            val failHotel  = sleep(100.millis) *> IO.fail(HotelBookingError())\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (failFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))\n                    .zipWithPar(failHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))((_, _) => ())\n                    .transact\n                    .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\")))\n          }\n        ),\n        suite(\"Saga#zipWith\")(\n          test(\"should successfully run two Sagas in sequence\") {\n            (for {\n              actionLog <- Ref.make(Vector.empty[String]).noCompensate\n              hotelBooked = bookHotel <* actionLog.update(_ :+ \"hotel booked\") compensate cancelHotel\n              flightBooked = bookFlight <* actionLog.update(_ :+ \"flight booked\") compensate cancelFlight\n              _       <- hotelBooked.zipWith(flightBooked)((_, _) => ())\n              actions <- actionLog.get.noCompensate\n            } yield assertTrue(actions == Vector(\"hotel booked\", \"flight booked\"))).transact\n          },\n          test(\"when right failed then should compensate right before left\") {\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))\n                .zipWith(IO.fail(HotelBookingError()) compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))((_, _) => ())\n                .transact\n                .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assertTrue(log == Vector(\"hotel canceled\", \"flight canceled\"))\n          },\n          test(\"when left failed should compensate left without calling right\") {\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (IO.fail(HotelBookingError()) compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\")))\n                .zipWith(actionLog.update(_ :+ \"flight booked\") *> bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\")))((_, _) =>\n                  ()\n                )\n                .transact\n                .orElse(IO.unit)\n              log <- actionLog.get\n            } yield assertTrue(log == Vector(\"hotel canceled\"))\n          }\n        ),\n        suite(\"Saga#zipWithParAll\")(test(\"should allow combining compensations in parallel\") {\n          val failFlight = IO.fail(FlightBookingError())\n          val failHotel  = IO.fail(HotelBookingError())\n\n          def cancelFlightC(actionLog: Ref[Vector[String]]) =\n            sleep(100.millis) *>\n              cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n          def cancelHotelC(actionLog: Ref[Vector[String]]) =\n            sleep(100.millis) *>\n              cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n\n          for {\n            actionLog <- Ref.make(Vector.empty[String])\n            _ <- (failFlight compensate cancelFlightC(actionLog))\n                  .zipWithParAll(failHotel compensate cancelHotelC(actionLog))((_, _) => ())((a, b) => a.zipPar(b).unit)\n                  .transact\n                  .orElse(IO.unit)\n            log <- actionLog.get\n          } yield assert(log)(hasSameElements(Vector(\"flight canceled\", \"hotel canceled\")))\n        }),\n        suite(\"Saga#retryableCompensate\")(\n          test(\"should construct Saga that repeats compensating action once\") {\n            val failFlight: ZIO[Clock, FlightBookingError, PaymentInfo] = sleep(1000.millis) *> IO.fail(\n              FlightBookingError()\n            )\n\n            def failCompensator(log: Ref[Vector[String]]): Compensator[Any, FlightBookingError] =\n              cancelFlight(log.update(_ :+ \"Compensation failed\")) *> IO.fail(FlightBookingError())\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (failFlight retryableCompensate (failCompensator(actionLog), Schedule.once)).transact\n                    .orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector.fill(2)(\"Compensation failed\")))\n          },\n          test(\"should work with other combinators\") {\n            val saga = for {\n              _ <- bookFlight.noCompensate\n              _ <- bookHotel retryableCompensate (cancelHotel, Schedule.once)\n              _ <- bookCar compensate cancelCar\n            } yield ()\n\n            assertM(saga.transact)(anything)\n          }\n        ),\n        suite(\"Saga#collectAllPar\")(\n          test(\"should construct a Saga that runs several requests in parallel\") {\n            def bookFlightS(log: Ref[Vector[String]]): ZIO[Clock, FlightBookingError, PaymentInfo] =\n              sleep(1000.millis) *> bookFlight <* log.update(_ :+ \"flight is booked\")\n            def bookHotelS(log: Ref[Vector[String]]): ZIO[Clock, HotelBookingError, PaymentInfo] =\n              sleep(600.millis) *> bookHotel <* log.update(_ :+ \"hotel is booked\")\n            def bookCarS(log: Ref[Vector[String]]): ZIO[Clock, CarBookingError, PaymentInfo] =\n              sleep(300.millis) *> bookCar <* log.update(_ :+ \"car is booked\")\n            def bookCarS2(log: Ref[Vector[String]]): ZIO[Clock, CarBookingError, PaymentInfo] =\n              sleep(100.millis) *> bookCar <* log.update(_ :+ \"car2 is booked\")\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              flight    = bookFlightS(actionLog) compensate cancelFlight\n              hotel     = bookHotelS(actionLog) compensate cancelHotel\n              car       = bookCarS(actionLog) compensate cancelCar\n              car2      = bookCarS2(actionLog) compensate cancelCar\n              _         <- Saga.collectAllPar(flight, hotel, car, car2).transact\n              log       <- actionLog.get\n            } yield assert(log)(hasSameElements(Vector(\"car2 is booked\", \"car is booked\", \"hotel is booked\", \"flight is booked\")))\n          },\n          test(\"should run all compensating actions in case of error\") {\n            val failFlightBooking = sleep(1000.millis) *> IO.fail(FlightBookingError())\n            val bookHotelS        = sleep(600.millis) *> bookHotel\n            val bookCarS          = sleep(300.millis) *> bookCar\n            val bookCarS2         = sleep(100.millis) *> bookCar\n\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              flight    = failFlightBooking compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n              hotel     = bookHotelS compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n              car       = bookCarS compensate cancelCar(actionLog.update(_ :+ \"car canceled\"))\n              car2      = bookCarS2 compensate cancelCar(actionLog.update(_ :+ \"car2 canceled\"))\n              _         <- Saga.collectAllPar(List(flight, hotel, car, car2)).transact.orElse(IO.unit)\n              log       <- actionLog.get\n            } yield assert(log)(hasSameElements(Vector(\"flight canceled\", \"hotel canceled\", \"car canceled\", \"car2 canceled\")))\n          }\n        ),\n        suite(\"Saga#succeed\")(test(\"should construct saga that will succeed\") {\n          val failFlightBooking = IO.fail(FlightBookingError())\n          val stub              = 1\n\n          for {\n            actionLog <- Ref.make(Vector.empty[String])\n            _ <- (for {\n                  i <- Saga.succeed(stub)\n                  _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ s\"flight canceled $i\"))\n                } yield ()).transact.orElse(ZIO.unit)\n            log <- actionLog.get\n          } yield assert(log)(equalTo(Vector(s\"flight canceled $stub\")))\n        }),\n        suite(\"Saga#fail\")(test(\"should construct saga that will fail\") {\n          val failFlightBooking = IO.fail(FlightBookingError())\n\n          for {\n            actionLog <- Ref.make(Vector.empty[String])\n            _ <- (for {\n                  i <- Saga.fail(FlightBookingError())\n                  _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ s\"flight canceled $i\"))\n                } yield ()).transact.orElse(ZIO.unit)\n            log <- actionLog.get\n          } yield assert(log)(equalTo(Vector.empty))\n        }),\n        suite(\"Saga#compensateIfFail\")(\n          test(\"should construct saga step that executes it's compensation if it's requests fails\") {\n            val failCar = IO.fail(CarBookingError())\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (for {\n                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n                    _ <- failCar compensateIfFail ((_: SagaError) => cancelCar(actionLog.update(_ :+ \"car canceled\")))\n                  } yield ()).transact.orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"car canceled\", \"hotel canceled\", \"flight canceled\")))\n          },\n          test(\"should construct saga step that do not executes it's compensation if it's request succeeds\") {\n            val failFlightBooking = IO.fail(FlightBookingError())\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (for {\n                    _ <- bookCar compensateIfFail ((_: SagaError) => cancelCar(actionLog.update(_ :+ \"car canceled\")))\n                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n                    _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n                  } yield ()).transact.orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\")))\n          }\n        ),\n        suite(\"Saga#compensateIfSuccess\")(\n          test(\n            \"should construct saga step that executes it's compensation if it's requests succeeds\"\n          ) {\n            val failFlightBooking = IO.fail(FlightBookingError())\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (for {\n                    _ <- bookCar compensateIfSuccess (\n                          (_: PaymentInfo) => cancelCar(actionLog.update(_ :+ \"car canceled\"))\n                        )\n                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n                    _ <- failFlightBooking compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n                  } yield ()).transact.orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\", \"car canceled\")))\n          },\n          test(\"should construct saga step that do not executes it's compensation if it's request fails\") {\n            val failCar = IO.fail(CarBookingError())\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (for {\n                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n                    _ <- failCar compensateIfSuccess (\n                          (_: PaymentInfo) => cancelCar(actionLog.update(_ :+ \"car canceled\"))\n                        )\n                  } yield ()).transact.orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"flight canceled\", \"hotel canceled\")))\n          }\n        ),\n        suite(\"Saga#compensate\")(\n          test(\"should allow compensation to be dependent on the result of corresponding effect\") {\n            val failCar = IO.fail(CarBookingError())\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              _ <- (for {\n                    _ <- bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n                    _ <- bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n                    _ <- failCar compensate (\n                          (_: Either[SagaError, PaymentInfo]) => cancelCar(actionLog.update(_ :+ \"car canceled\"))\n                        )\n                  } yield ()).transact.orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"car canceled\", \"flight canceled\", \"hotel canceled\")))\n          }\n        ),\n        suite(\"Saga#flatten\")(\n          test(\"should execute outer effect first and then the inner one producing the result of it\") {\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n              outer     = bookFlight compensate cancelFlight(actionLog.update(_ :+ \"flight canceled\"))\n              inner     = bookHotel compensate cancelHotel(actionLog.update(_ :+ \"hotel canceled\"))\n              failCar   = IO.fail(CarBookingError()) compensate cancelCar(actionLog.update(_ :+ \"car canceled\"))\n\n              _ <- outer\n                    .map(_ => inner)\n                    .flatten[Any, SagaError, PaymentInfo]\n                    .flatMap(_ => failCar)\n                    .transact\n                    .orElse(ZIO.unit)\n              log <- actionLog.get\n            } yield assert(log)(equalTo(Vector(\"car canceled\", \"hotel canceled\", \"flight canceled\")))\n          }\n        ),\n        suite(\"Saga#transact\")(\n          test(\"should return original error in case compensator also fails\") {\n            val expectedError                                   = FlightBookingError()\n            val failFlight: ZIO[Clock, FlightBookingError, Any] = sleep(1000.millis) *> IO.fail(expectedError)\n\n            val failCompensator = cancelFlight *> IO.fail(CarBookingError())\n\n            val saga = (failFlight compensate failCompensator).transact.catchAll(e => IO.succeed(e))\n\n            assertM(saga)(equalTo(expectedError))\n          },\n          test(\"should return original error in case compensator also fails 2\") {\n            val expectedError                                   = FlightBookingError()\n            val failFlight: ZIO[Clock, FlightBookingError, Any] = sleep(1000.millis) *> IO.fail(expectedError)\n\n            val failCompensator = cancelFlight *> IO.fail(new RuntimeException())\n\n            val saga = (for {\n              _ <- bookHotel compensate cancelHotel\n              _ <- failFlight compensate failCompensator\n              _ <- bookCar compensate cancelCar\n            } yield ()).transact.catchAll[Clock, Any, Any](e => IO.succeed(e))\n\n            assertM(saga)(equalTo(expectedError))\n          }\n        ),\n        suite(\"Saga#collectAll\")(\n          test(\"should collect all effects in one collection in sequential way\") {\n            for {\n              actionLog <- Ref.make(Vector.empty[String])\n\n              payments <- Saga\n                .collectAll(\n                  (bookFlight <* actionLog.update(_ :+ \"flight booked\")).noCompensate ::\n                    (bookHotel <* actionLog.update(_ :+ \"hotel booked\")).noCompensate ::\n                    (bookCar <* actionLog.update(_ :+ \"car booked\")).noCompensate ::\n                    Nil\n                )\n                .transact\n\n              actionsOrder <- actionLog.get\n            } yield assert(payments)(hasSameElements(FlightPayment :: HotelPayment :: CarPayment :: Nil)) &&\n              assertTrue(actionsOrder == Vector(\"flight booked\", \"hotel booked\", \"car booked\"))\n          },\n          test(\"should compensate made effects when one of them failed\") {\n            for {\n              log <- Ref.make(Vector.empty[String])\n\n              successfullyBookFlight = bookFlight.compensate(cancelFlight(log.update(_ :+ \"flight canceled\")))\n\n              failOnHotelBook = IO.fail(HotelBookingError)\n                .compensate(cancelHotel(log.update(_ :+ \"hotel canceled\")))\n\n              successfullyBookCar = (bookCar <* log.update(_ :+ \"car booked\"))\n                .compensate(cancelCar(log.update(_ :+ \"car canceled\")))\n\n              _ <- Saga\n                .collectAll(successfullyBookFlight :: failOnHotelBook :: successfullyBookCar :: Nil)\n                .transact\n                .fold(\n                  _ => (),\n                  _ => ()\n                )\n\n              actionsOrder <- log.get\n            } yield assertTrue(actionsOrder == Vector(\"hotel canceled\", \"flight canceled\"))\n          }\n        )\n      )\n}\n\nobject SagaSpecUtil {\n\n  def sleep(d: Duration): URIO[Clock, Unit] = ZIO.sleep(d).provide(Clock.live)\n\n  sealed trait SagaError extends Product with Serializable {\n    def message: String\n  }\n  case class FlightBookingError(message: String = \"Can't book a flight\")        extends SagaError\n  case class HotelBookingError(message: String = \"Can't book a hotel room\")     extends SagaError\n  case class CarBookingError(message: String = \"Can't book a car\")              extends SagaError\n  case class PaymentFailedError(message: String = \"Can't collect the payments\") extends SagaError\n\n  case class PaymentInfo(amount: Double)\n\n  val FlightPayment = PaymentInfo(420d)\n  val HotelPayment  = PaymentInfo(1448d)\n  val CarPayment    = PaymentInfo(42d)\n\n  def bookFlight: IO[FlightBookingError, PaymentInfo] = IO.succeed(FlightPayment)\n\n  def bookHotel: IO[HotelBookingError, PaymentInfo] = IO.succeed(HotelPayment)\n\n  def bookCar: IO[CarBookingError, PaymentInfo] = IO.succeed(CarPayment)\n\n  def collectPayments(paymentInfo: PaymentInfo*): IO[PaymentFailedError, Unit] = IO.succeed(paymentInfo).unit\n\n  def cancelFlight: Compensator[Any, FlightBookingError] = IO.unit\n\n  def cancelFlight(postAction: UIO[Any]): Compensator[Any, FlightBookingError] =\n    postAction *> IO.unit\n\n  def cancelHotel: Compensator[Any, HotelBookingError] = IO.unit\n\n  def cancelHotel(postAction: UIO[Any]): Compensator[Any, HotelBookingError] =\n    postAction *> IO.unit\n\n  def cancelCar: Compensator[Any, CarBookingError] = IO.unit\n\n  def cancelCar(postAction: UIO[Any]): Compensator[Any, CarBookingError] = postAction *> IO.unit\n\n  def refundPayments(paymentInfo: PaymentInfo*): Compensator[Any, PaymentFailedError] = IO.succeed(paymentInfo).unit\n\n}\n"
  },
  {
    "path": "examples/README.md",
    "content": "This is the example that extends one provided in the parent REAMDE file.\n\nThis projects shows one of the implementation variants of Saga Executor Coordinator\nthat also writes transaction log to database and knows how to recover from failures. \n\nTODO:\n- pack project to docker image\n- minimal GUI, SSE for saga execution tracking\n- in case of coordinator scaling we need to differentiate saga that is ongoing from failed saga that we need to restore"
  },
  {
    "path": "examples/src/main/resources/application.conf",
    "content": ""
  },
  {
    "path": "examples/src/main/resources/logback.xml",
    "content": "<configuration>\n\n    <appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\">\n        <encoder>\n            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n [%X{userId} %X{orderId} %X{sagaId}]</pattern>\n        </encoder>\n    </appender>\n\n    <root level=\"debug\">\n        <appender-ref ref=\"STDOUT\" />\n    </root>\n</configuration>"
  },
  {
    "path": "examples/src/main/resources/sql/saga.ddl",
    "content": "CREATE TABLE public.saga\n(\n    id sequence,\n    initiator uuid NOT NULL,\n    \"createdAt\" timestamp without time zone NOT NULL,\n    \"finishedAt\" timestamp without time zone,\n    data jsonb NOT NULL,\n    type text,\n    CONSTRAINT \"Saga_pkey\" PRIMARY KEY (id)\n)\n\nCREATE TABLE public.saga_step\n(\n    \"sagaId\" integer NOT NULL,\n    name text NOT NULL,\n    result jsonb,\n    \"finishedAt\" timestamp without time zone,\n    failure text,\n    CONSTRAINT saga_step_pkey PRIMARY KEY (\"sagaId\", name),\n    CONSTRAINT saga_id_fk FOREIGN KEY (\"sagaId\")\n        REFERENCES public.saga (id) MATCH SIMPLE\n        ON UPDATE NO ACTION\n        ON DELETE CASCADE\n)"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/OrderSagaCoordinator.scala",
    "content": "package com.vladkopanev.zio.saga.example\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.client.{\n    LoyaltyPointsServiceClient,\n    OrderServiceClient,\n    PaymentServiceClient\n  }\nimport com.vladkopanev.zio.saga.example.dao.SagaLogDao\nimport com.vladkopanev.zio.saga.example.model.{ OrderSagaData, OrderSagaError, SagaStep }\nimport io.chrisdavenport.log4cats.StructuredLogger\nimport io.chrisdavenport.log4cats.slf4j.Slf4jLogger\nimport zio.{ Schedule, Task, ZIO }\n\nimport scala.concurrent.TimeoutException\n\ntrait OrderSagaCoordinator {\n  def runSaga(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double, sagaIdOpt: Option[Long]): TaskC[Unit]\n\n  def recoverSagas: TaskC[Unit]\n}\n\nclass OrderSagaCoordinatorImpl(\n  paymentServiceClient: PaymentServiceClient,\n  loyaltyPointsServiceClient: LoyaltyPointsServiceClient,\n  orderServiceClient: OrderServiceClient,\n  sagaLogDao: SagaLogDao,\n  maxRequestTimeout: Int,\n  logger: StructuredLogger[Task]\n) extends OrderSagaCoordinator {\n\n  import com.vladkopanev.zio.saga.Saga._\n\n  def runSaga(\n    userId: UUID,\n    orderId: BigInt,\n    money: BigDecimal,\n    bonuses: Double,\n    sagaIdOpt: Option[Long]\n  ): TaskC[Unit] = {\n\n    import zio.duration._\n\n    def mkSagaRequest(\n      request: TaskC[Unit],\n      sagaId: Long,\n      stepName: String,\n      executedSteps: List[SagaStep],\n      compensating: Boolean = false\n    ) =\n      ZIO.fromOption(\n          executedSteps.find(step => step.name == stepName && !compensating).flatMap(_.failure).map(new OrderSagaError(_))\n        ).flip *> request\n          .timeoutFail(new TimeoutException(s\"Execution Timeout occurred for $stepName step\"))(maxRequestTimeout.seconds)\n          .tapBoth(\n            e => sagaLogDao.createSagaStep(stepName, sagaId, result = None, failure = Some(e.getMessage)),\n            _ => sagaLogDao.createSagaStep(stepName, sagaId, result = None)\n          )\n          .when(!executedSteps.exists(_.name == stepName))\n\n    def collectPayments(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(\n      paymentServiceClient.collectPayments(userId, money, sagaId.toString),\n      sagaId,\n      \"collectPayments\",\n      executed\n    )\n\n    def assignLoyaltyPoints(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(\n      loyaltyPointsServiceClient.assignLoyaltyPoints(userId, bonuses, sagaId.toString),\n      sagaId,\n      \"assignLoyaltyPoints\",\n      executed\n    )\n\n    def closeOrder(executed: List[SagaStep], sagaId: Long) =\n      mkSagaRequest(orderServiceClient.closeOrder(userId, orderId, sagaId.toString), sagaId, \"closeOrder\", executed)\n\n    def refundPayments(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(\n      paymentServiceClient.refundPayments(userId, money, sagaId.toString),\n      sagaId,\n      \"refundPayments\",\n      executed,\n      compensating = true\n    )\n\n    def cancelLoyaltyPoints(executed: List[SagaStep], sagaId: Long) = mkSagaRequest(\n      loyaltyPointsServiceClient.cancelLoyaltyPoints(userId, bonuses, sagaId.toString),\n      sagaId,\n      \"cancelLoyaltyPoints\",\n      executed,\n      compensating = true\n    )\n\n    def reopenOrder(executed: List[SagaStep], sagaId: Long) =\n      mkSagaRequest(\n        orderServiceClient.reopenOrder(userId, orderId, sagaId.toString),\n        sagaId,\n        \"reopenOrder\",\n        executed,\n        compensating = true\n      )\n\n    val expSchedule = Schedule.exponential(1.second)\n    def buildSaga(sagaId: Long, executedSteps: List[SagaStep]) =\n      for {\n        _ <- collectPayments(executedSteps, sagaId) retryableCompensate (refundPayments(executedSteps, sagaId), expSchedule)\n        _ <- assignLoyaltyPoints(executedSteps, sagaId) retryableCompensate (cancelLoyaltyPoints(executedSteps, sagaId), expSchedule)\n        _ <- closeOrder(executedSteps, sagaId) retryableCompensate (reopenOrder(executedSteps, sagaId), expSchedule)\n      } yield ()\n\n    import io.circe.syntax._\n\n    val mdcLog = wrapMDC(logger, userId, orderId, sagaIdOpt)\n    val data   = OrderSagaData(userId, orderId, money, bonuses).asJson\n\n    for {\n      _        <- mdcLog.info(\"Saga execution started\")\n      sagaId   <- sagaIdOpt.fold(sagaLogDao.startSaga(userId, data))(i => Task.succeed(i))\n      executed <- sagaLogDao.listExecutedSteps(sagaId)\n      _ <- buildSaga(sagaId, executed).transact.tapBoth({\n            case _: OrderSagaError => sagaLogDao.finishSaga(sagaId)\n            case _                 => ZIO.unit\n          }, _ => sagaLogDao.finishSaga(sagaId))\n      _ <- mdcLog.info(\"Saga execution finished\")\n    } yield ()\n\n  }\n\n  override def recoverSagas: TaskC[Unit] =\n    for {\n      _     <- logger.info(\"Sagas recovery stared\")\n      sagas <- sagaLogDao.listUnfinishedSagas\n      _     <- logger.info(s\"Found unfinished sagas: $sagas\")\n      _ <- ZIO.foreachParN_(100)(sagas) { sagaInfo =>\n            ZIO.fromEither(sagaInfo.data.as[OrderSagaData]).flatMap {\n              case OrderSagaData(userId, orderId, money, bonuses) =>\n                runSaga(userId, orderId, money, bonuses, Some(sagaInfo.id)).catchSome {\n                  case _: OrderSagaError => ZIO.unit\n                }\n            }\n          }\n      _ <- logger.info(\"Sagas recovery finished\")\n    } yield ()\n\n  private def wrapMDC(logger: StructuredLogger[Task], userId: UUID, orderId: BigInt, sagaIdOpt: Option[Long]) =\n    StructuredLogger.withContext(logger)(\n      Map(\"userId\" -> userId.toString, \"orderId\" -> orderId.toString, \"sagaId\" -> sagaIdOpt.toString)\n    )\n}\n\nobject OrderSagaCoordinatorImpl {\n  import zio.interop.catz._\n\n  def apply(\n    paymentServiceClient: PaymentServiceClient,\n    loyaltyPointsServiceClient: LoyaltyPointsServiceClient,\n    orderServiceClient: OrderServiceClient,\n    sagaLogDao: SagaLogDao,\n    maxRequestTimeout: Int\n  ): Task[OrderSagaCoordinatorImpl] =\n    Slf4jLogger\n      .create[Task]\n      .map(\n        new OrderSagaCoordinatorImpl(\n          paymentServiceClient,\n          loyaltyPointsServiceClient,\n          orderServiceClient,\n          sagaLogDao,\n          maxRequestTimeout,\n          _\n        )\n      )\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/SagaApp.scala",
    "content": "package com.vladkopanev.zio.saga.example\nimport com.vladkopanev.zio.saga.example.client.{LoyaltyPointsServiceClientStub, OrderServiceClientStub, PaymentServiceClientStub}\nimport com.vladkopanev.zio.saga.example.dao.SagaLogDaoImpl\nimport com.vladkopanev.zio.saga.example.endpoint.SagaEndpoint\nimport zio.interop.catz._\nimport zio.console.putStrLn\nimport zio.{App, ExitCode, ZEnv, ZIO}\n\nimport scala.concurrent.ExecutionContext\n\nobject SagaApp extends App {\n\n  import org.http4s.server.blaze._\n\n  implicit val runtime = this\n\n  override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {\n    val flakyClient         = sys.env.getOrElse(\"FLAKY_CLIENT\", \"false\").toBoolean\n    val clientMaxReqTimeout = sys.env.getOrElse(\"CLIENT_MAX_REQUEST_TIMEOUT_SEC\", \"10\").toInt\n    val sagaMaxReqTimeout   = sys.env.getOrElse(\"SAGA_MAX_REQUEST_TIMEOUT_SEC\", \"12\").toInt\n\n    (for {\n      paymentService <- PaymentServiceClientStub(clientMaxReqTimeout, flakyClient)\n      loyaltyPoints  <- LoyaltyPointsServiceClientStub(clientMaxReqTimeout, flakyClient)\n      orderService   <- OrderServiceClientStub(clientMaxReqTimeout, flakyClient)\n      logDao         = new SagaLogDaoImpl\n      orderSEC       <- OrderSagaCoordinatorImpl(paymentService, loyaltyPoints, orderService, logDao, sagaMaxReqTimeout)\n      app            = new SagaEndpoint(orderSEC).service\n      _              <- orderSEC.recoverSagas.fork\n      _              <- BlazeServerBuilder[TaskC](ExecutionContext.global).bindHttp(8042).withHttpApp(app).serve.compile.drain\n    } yield ()).foldM(\n      e => putStrLn(s\"Saga Coordinator fails with error $e, stopping server...\").as(ExitCode.failure),\n      _ => putStrLn(s\"Saga Coordinator finished successfully, stopping server...\").as(ExitCode.success)\n    )\n  }\n\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/LoyaltyPointsServiceClient.scala",
    "content": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nimport io.chrisdavenport.log4cats.Logger\nimport io.chrisdavenport.log4cats.slf4j.Slf4jLogger\nimport zio.Task\n\ntrait LoyaltyPointsServiceClient {\n\n  def assignLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit]\n\n  def cancelLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit]\n}\n\nclass LoyaltyPointsServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean)\n    extends LoyaltyPointsServiceClient {\n\n  override def assignLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"assignLoyaltyPoints\").when(flaky)\n      _ <- logger.info(s\"Loyalty points assigned to user $userId\")\n    } yield ()\n\n  override def cancelLoyaltyPoints(userId: UUID, amount: Double, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"cancelLoyaltyPoints\").when(flaky)\n      _ <- logger.info(s\"Loyalty points canceled for user $userId\")\n    } yield ()\n\n}\n\nobject LoyaltyPointsServiceClientStub {\n\n  import zio.interop.catz._\n\n  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[LoyaltyPointsServiceClientStub] =\n    Slf4jLogger.create[Task].map(new LoyaltyPointsServiceClientStub(_, maxRequestTimeout, flaky))\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/OrderServiceClient.scala",
    "content": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nimport io.chrisdavenport.log4cats.Logger\nimport io.chrisdavenport.log4cats.slf4j.Slf4jLogger\nimport zio.Task\n\ntrait OrderServiceClient {\n\n  def closeOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit]\n\n  def reopenOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit]\n}\n\nclass OrderServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean) extends OrderServiceClient {\n\n  override def closeOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"closeOrder\").when(flaky)\n      _ <- logger.info(s\"Order #$orderId closed\")\n    } yield ()\n\n  override def reopenOrder(userId: UUID, orderId: BigInt, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"reopenOrder\").when(flaky)\n      _ <- logger.info(s\"Order #$orderId reopened\")\n    } yield ()\n}\n\nobject OrderServiceClientStub {\n\n  import zio.interop.catz._\n\n  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[OrderServiceClientStub] =\n    Slf4jLogger.create[Task].map(new OrderServiceClientStub(_, maxRequestTimeout, flaky))\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/PaymentServiceClient.scala",
    "content": "package com.vladkopanev.zio.saga.example.client\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.TaskC\nimport io.chrisdavenport.log4cats.Logger\nimport io.chrisdavenport.log4cats.slf4j.Slf4jLogger\nimport zio.Task\n\ntrait PaymentServiceClient {\n\n  def collectPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit]\n\n  def refundPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit]\n}\n\nclass PaymentServiceClientStub(logger: Logger[Task], maxRequestTimeout: Int, flaky: Boolean)\n    extends PaymentServiceClient {\n\n  override def collectPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"collectPayments\").when(flaky)\n      _ <- logger.info(s\"Payments collected from user #$userId\")\n    } yield ()\n\n  override def refundPayments(userId: UUID, amount: BigDecimal, traceId: String): TaskC[Unit] =\n    for {\n      _ <- randomSleep(maxRequestTimeout)\n      _ <- randomFail(\"refundPayments\").when(flaky)\n      _ <- logger.info(s\"Payments refunded to user #$userId\")\n    } yield ()\n}\n\nobject PaymentServiceClientStub {\n\n  import zio.interop.catz._\n\n  def apply(maxRequestTimeout: Int, flaky: Boolean): Task[PaymentServiceClient] =\n    Slf4jLogger.create[Task].map(new PaymentServiceClientStub(_, maxRequestTimeout, flaky))\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/client/client.scala",
    "content": "package com.vladkopanev.zio.saga.example\n\nimport zio.{ Task, ZIO }\n\nimport scala.util.Random\n\npackage object client {\n\n  import zio.duration._\n\n  def randomSleep(maxTimeout: Int): TaskC[Unit] =\n    for {\n      randomSeconds <- ZIO.effectTotal(Random.nextInt(maxTimeout))\n      _             <- ZIO.sleep(randomSeconds.seconds)\n    } yield ()\n\n  def randomFail(operationName: String): Task[Unit] =\n    for {\n      randomInt <- ZIO.effectTotal(Random.nextInt(100))\n      _         <- if (randomInt % 10 == 0) ZIO.fail(new RuntimeException(s\"Failed to execute $operationName\")) else ZIO.unit\n    } yield ()\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/dao/SagaLogDao.scala",
    "content": "package com.vladkopanev.zio.saga.example.dao\n\nimport java.util.UUID\n\nimport com.vladkopanev.zio.saga.example.model.{ SagaInfo, SagaStep }\nimport doobie.implicits.javatime.JavaTimeInstantMeta\nimport io.circe.Json\nimport org.postgresql.util.PGobject\nimport zio.{ Task, ZIO }\n\ntrait SagaLogDao {\n  def finishSaga(sagaId: Long): ZIO[Any, Throwable, Unit]\n\n  def startSaga(initiator: UUID, data: Json): ZIO[Any, Throwable, Long]\n\n  def createSagaStep(\n    name: String,\n    sagaId: Long,\n    result: Option[Json],\n    failure: Option[String] = None\n  ): ZIO[Any, Throwable, Unit]\n\n  def listExecutedSteps(sagaId: Long): ZIO[Any, Throwable, List[SagaStep]]\n\n  def listUnfinishedSagas: ZIO[Any, Throwable, List[SagaInfo]]\n}\n\nclass SagaLogDaoImpl extends SagaLogDao {\n  import doobie._\n  import doobie.implicits._\n  import doobie.postgres.implicits._\n  import zio.interop.catz._\n\n  val xa = Transactor.fromDriverManager[Task](\n    \"org.postgresql.Driver\",\n    \"jdbc:postgresql:Saga\",\n    \"postgres\",\n    \"root\"\n  )\n  implicit val han = LogHandler.jdkLogHandler\n\n  override def finishSaga(sagaId: Long): ZIO[Any, Throwable, Unit] =\n    sql\"\"\"UPDATE saga SET \"finishedAt\" = now() WHERE id = $sagaId\"\"\".update.run.transact(xa).unit\n\n  override def startSaga(initiator: UUID, data: Json): ZIO[Any, Throwable, Long] =\n    sql\"\"\"INSERT INTO saga(\"initiator\", \"createdAt\", \"finishedAt\", \"data\", \"type\") \n          VALUES ($initiator, now(), null, $data, 'order')\"\"\".update\n      .withUniqueGeneratedKeys[Long](\"id\")\n      .transact(xa)\n\n  override def createSagaStep(\n    name: String,\n    sagaId: Long,\n    result: Option[Json],\n    failure: Option[String]\n  ): ZIO[Any, Throwable, Unit] =\n    sql\"\"\"INSERT INTO saga_step(\"sagaId\", \"name\", \"result\", \"finishedAt\", \"failure\")\n          VALUES ($sagaId, $name, $result, now(), $failure)\"\"\".update.run\n      .transact(xa)\n      .unit\n\n  override def listExecutedSteps(sagaId: Long): ZIO[Any, Throwable, List[SagaStep]] =\n    sql\"\"\"SELECT \"sagaId\", \"name\", \"finishedAt\", \"result\", \"failure\"\n          from saga_step WHERE \"sagaId\" = $sagaId\"\"\".query[SagaStep].to[List].transact(xa)\n\n  override def listUnfinishedSagas: ZIO[Any, Throwable, List[SagaInfo]] =\n    sql\"\"\"SELECT \"id\", \"initiator\", \"createdAt\", \"finishedAt\", \"data\", \"type\"\n          from saga s WHERE \"finishedAt\" IS NULL\"\"\".query[SagaInfo].to[List].transact(xa)\n\n  implicit lazy val JsonMeta: Meta[Json] = {\n    import io.circe.parser._\n    Meta.Advanced\n      .other[PGobject](\"jsonb\")\n      .timap[Json](pgObj => parse(pgObj.getValue).fold(e => sys.error(e.message), identity)) { json =>\n        val pgObj = new PGobject\n        pgObj.setType(\"jsonb\")\n        pgObj.setValue(json.noSpaces)\n        pgObj\n      }\n  }\n\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/endpoint/SagaEndpoint.scala",
    "content": "package com.vladkopanev.zio.saga.example.endpoint\n\nimport com.vladkopanev.zio.saga.example.{ OrderSagaCoordinator, TaskC }\nimport com.vladkopanev.zio.saga.example.model.OrderInfo\nimport org.http4s.circe._\nimport org.http4s.dsl.Http4sDsl\nimport org.http4s.implicits._\nimport org.http4s.{ HttpApp, HttpRoutes }\nimport zio.interop.catz._\n\nfinal class SagaEndpoint(orderSagaCoordinator: OrderSagaCoordinator) extends Http4sDsl[TaskC] {\n\n  private implicit val decoder = jsonOf[TaskC, OrderInfo]\n\n  val service: HttpApp[TaskC] = HttpRoutes\n    .of[TaskC] {\n      case req @ POST -> Root / \"saga\" / \"finishOrder\" =>\n        for {\n          OrderInfo(userId, orderId, money, bonuses) <- req.as[OrderInfo]\n          resp <- orderSagaCoordinator\n                   .runSaga(userId, orderId, money, bonuses, None)\n                   .foldM(fail => InternalServerError(fail.getMessage), _ => Ok(\"Saga submitted\"))\n        } yield resp\n    }\n    .orNotFound\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/example.scala",
    "content": "package com.vladkopanev.zio.saga\nimport zio.RIO\nimport zio.clock.Clock\n\npackage object example {\n  type TaskC[+A] = RIO[Clock, A]\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderInfo.scala",
    "content": "package com.vladkopanev.zio.saga.example.model\n\nimport java.util.UUID\n\n\ncase class OrderInfo(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double)\n\nobject OrderInfo {\n  import io.circe._\n  import io.circe.generic.semiauto._\n  implicit val OrderInfoDecoder: Decoder[OrderInfo] = deriveDecoder[OrderInfo]\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/OrderSagaError.scala",
    "content": "package com.vladkopanev.zio.saga.example.model\n\nclass OrderSagaError(message: String) extends RuntimeException(s\"Saga failed with message: $message\")\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaInfo.scala",
    "content": "package com.vladkopanev.zio.saga.example.model\n\nimport java.time.Instant\nimport java.util.UUID\n\nimport io.circe.Json\n\ncase class SagaInfo(id: Long,\n                    initiator: UUID,\n                    createdAt: Instant,\n                    finishedAt: Option[Instant],\n                    data: Json,\n                    `type`: String)\n\ncase class OrderSagaData(userId: UUID, orderId: BigInt, money: BigDecimal, bonuses: Double)\n\nobject OrderSagaData {\n  import io.circe._, io.circe.generic.semiauto._\n  implicit val decoder: Decoder[OrderSagaData] = deriveDecoder[OrderSagaData]\n  implicit val encoder: Encoder[OrderSagaData] = deriveEncoder[OrderSagaData]\n}\n"
  },
  {
    "path": "examples/src/main/scala/com/vladkopanev/zio/saga/example/model/SagaStep.scala",
    "content": "package com.vladkopanev.zio.saga.example.model\n\nimport java.time.Instant\n\nimport io.circe.Json\n\ncase class SagaStep(sagaId: Long, name: String, finishedAt: Instant, result: Option[Json], failure: Option[String])\n"
  },
  {
    "path": "project/Versions.scala",
    "content": "object Versions {\n\n  val Circe = \"0.14.1\"\n  val Doobie = \"1.0.0-RC1\"\n  val Http4s = \"0.23.6\"\n  val Log4Cats = \"2.1.1\"\n  val Zio = \"2.0.0-RC1\"\n}\n"
  },
  {
    "path": "project/build.properties",
    "content": "sbt.version = 1.8.3"
  },
  {
    "path": "project/plugins.sbt",
    "content": "addSbtPlugin(\"org.scalameta\" % \"sbt-scalafmt\"      % \"2.3.0\")\naddSbtPlugin(\"org.scoverage\" % \"sbt-scoverage\"     % \"2.0.7\")\naddSbtPlugin(\"ch.epfl.scala\" % \"sbt-release-early\" % \"2.1.1\")\n\n// from https://github.com/sbt/sbt/issues/6745#issuecomment-1442315151\nlibraryDependencySchemes += \"org.scala-lang.modules\" %% \"scala-xml\" % VersionScheme.Always"
  }
]