{"id":21399393,"url":"https://github.com/jcouyang/jujiu","last_synced_at":"2025-10-31T14:02:13.793Z","repository":{"id":39924092,"uuid":"188653627","full_name":"jcouyang/jujiu","owner":"jcouyang","description":"Functional Scala Cache","archived":false,"fork":false,"pushed_at":"2024-12-11T17:14:13.000Z","size":537,"stargazers_count":58,"open_issues_count":17,"forks_count":6,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-27T01:11:20.810Z","etag":null,"topics":["birds","cache","caffeine","functional-programming","scala"],"latest_commit_sha":null,"homepage":"","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/jcouyang.png","metadata":{"files":{"readme":"README.org","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":"2019-05-26T07:36:30.000Z","updated_at":"2024-12-11T17:14:19.000Z","dependencies_parsed_at":"2023-02-16T21:46:07.435Z","dependency_job_id":"32595abc-d66b-46eb-abc8-1821ed85dfa8","html_url":"https://github.com/jcouyang/jujiu","commit_stats":{"total_commits":311,"total_committers":3,"mean_commits":"103.66666666666667","dds":0.4951768488745981,"last_synced_commit":"0a397de12fa69c2a1549667e8c7ec5628130c1da"},"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fjujiu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fjujiu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fjujiu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcouyang%2Fjujiu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcouyang","download_url":"https://codeload.github.com/jcouyang/jujiu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248693953,"owners_count":21146897,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["birds","cache","caffeine","functional-programming","scala"],"created_at":"2024-11-22T15:14:22.804Z","updated_at":"2025-10-31T14:02:08.746Z","avatar_url":"https://github.com/jcouyang.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"#+HTML: \u003ch1\u003e\u003cruby\u003e雎鳩\u003crt\u003eju jiu\u003c/rt\u003e\u003c/ruby\u003e\u003c/h1\u003e\n\nFunctional Scala Caching\n\n[[https://github.com/jcouyang/jujiu/actions][https://github.com/jcouyang/jujiu/workflows/Build%20and%20Test/badge.svg]]\n[[https://index.scala-lang.org/jcouyang/jujiu][https://index.scala-lang.org/jcouyang/jujiu/latest.svg?v=1]]\n[[https://www.javadoc.io/doc/us.oyanglul/jujiu_0.23][https://www.javadoc.io/badge/us.oyanglul/jujiu_2.13.svg?label=document]]\n[[https://codecov.io/gh/jcouyang/jujiu][https://codecov.io/gh/jcouyang/jujiu/branch/master/graph/badge.svg]]\n\n#+HTML: \u003cimg src=https://upload.wikimedia.org/wikipedia/commons/7/7e/Imperial_Encyclopaedia_-_Animal_Kingdom_-_pic009_-_%E9%9B%8E%E9%B3%A9%E5%9C%96.svg width=40%/\u003e\n\n#+BEGIN_QUOTE\n*Do one thing and do it well* micro [[https://github.com/search?q=org:jcouyang+topic:birds\u0026type=Repositories][birds library]] series\n#+END_QUOTE\n\n~val version =~ [[https://index.scala-lang.org/jcouyang/jujiu][https://index.scala-lang.org/jcouyang/jujiu/latest.svg?v=1]]\n#+BEGIN_EXAMPLE\nlibraryDependencies += \"us.oyanglul\" %% \"jujiu\" % version\n#+END_EXAMPLE\n\n* Quick Started in Scala 3^{=new=}\n  :PROPERTIES:\n  :header-args: :tangle src/test/scala-3.0.0-M3/us/oyanglul/JujiuSpec.scala :exports code\n  :CUSTOM_ID: scala-3-example\n  :END:\n  #+begin_src scala :exports none\n    package us.oyanglul.jujiu\n\n    import us.oyanglul.jujiu.syntax.caffeine._\n    import us.oyanglul.jujiu.syntax.cache._\n    import scala.concurrent.ExecutionContext\n    import org.specs2.mutable.Specification\n    import cats.effect._\n    import com.github.benmanes.caffeine.cache\n  #+end_src\n\n  There are only two simple steps to use cache:\n1. Initiate a protocol-agnostic Cache DSL, which means the DSL only aware of what to operate, not how to\n2. using the DSL syntax =fetchF=, =parFetchAllF= etc to describe how to use the Cache in the program\n3. =given= a instance of =Cache[Key, Val]=, in the example we created a Caffeine instance, which tells *how* exactly how to actually do the cache.\n\n  #+begin_src scala\n    class JujiuScala3Spec extends Specification:\n      given ContextShift[IO] = IO.contextShift(ExecutionContext.global)\n      \"works with IO\" \u003e\u003e {\n        \"normal cache\" \u003e\u003e {\n          val dsl: Cache[IO, cache.Cache, String, String] = new CaffeineCache[IO, String, String]{}\n\n          def program(using cache.Cache[String, String]) =\n            for\n              _ \u003c- IO(println(\"something\"))\n              _ \u003c- dsl.putF(\"key1\", \"value1\")\n              r1 \u003c- dsl.fetchF(\"key1\")\n              r2 \u003c- dsl.fetchF(\"key2\", _ =\u003e IO(\"value2\"))\n              r3 \u003c- dsl.fetchAllF(List(\"key1\", \"key2\"))\n              r4 \u003c- dsl.parFetchAllF[List, IO.Par](List(\"key1\", \"key2\"))\n              _ \u003c- dsl.clearF(\"key1\")\n            yield (r1, r2, r3, r4)\n\n          given cache.Cache[String, String] = Caffeine().sync[String, String]\n          program.unsafeRunSync() must_== (\n            (\n              Some(\"value1\"),\n              \"value2\",\n              List(Some(\"value1\"), Some(\"value2\")),\n              List(Some(\"value1\"), Some(\"value2\"))\n            )\n          )\n        }\n      }\n    end JujiuScala3Spec\n  #+end_src\n\n* [[https://typelevel.org/cats/img/cats-badge-tiny.png]] [[https://github.com/ben-manes/caffeine][Caffeine]]\n  :PROPERTIES:\n  :header-args: :tangle no :exports code\n  :CUSTOM_ID: making-caffeine-cats-friendly-badge\n  :END:\n#+BEGIN_SRC scala :exports none :noweb yes :tangle src/test/scala/us/oyanglul/JujiuSpec.scala\n  package us.oyanglul.jujiu\n  import us.oyanglul.jujiu.syntax.caffeine._\n  import us.oyanglul.jujiu.syntax.cache._\n  import cats.{Applicative}\n  import cats.data.Kleisli\n  import java.util.concurrent.CompletableFuture\n  import scala.concurrent.ExecutionContext\n  import org.specs2.mutable.Specification\n  import cats.instances.list._\n  import cats.syntax.all._\n  import cats.effect._\n  import scala.concurrent.ExecutionContext.Implicits.global\n  import scala.concurrent.duration._\n  import com.github.benmanes.caffeine.cache\n  import us.oyanglul.jujiu.syntax.CaffeineSyntax\n\n  class JujiuSpec extends Specification with org.specs2.mock.Mockito {\n    implicit val cs: ContextShift[IO] = IO.contextShift(ExecutionContext.global)\n    \u003c\u003cget_set_cache\u003e\u003e\n    \u003c\u003casync_load_failure\u003e\u003e\n    \u003c\u003ccatsio_cache\u003e\u003e\n    \u003c\u003ccatsio_loading_cache\u003e\u003e\n    \u003c\u003ccaffeine_builder\u003e\u003e\n    \u003c\u003ctagless_final\u003e\u003e\n    \u003c\u003creaderT\u003e\u003e\n    \u003c\u003credis\u003e\u003e\n  }\n#+END_SRC\n\n#+BEGIN_SRC scala -n :noweb-ref get_set_cache\n  \"it should able to get and set cache\" \u003e\u003e {\n    object cacheDsl extends CaffeineCache[IO, String, String]   // \u003c- (ref:dsl)\n    val program = for {\n      r1 \u003c- cacheDsl.fetch(\"not exist yet\")                     // \u003c- (ref:fetch)\n      r2 \u003c- cacheDsl.fetch(\"not exist yet\", _ =\u003e IO(\"default\")) // \u003c- (ref:fetchOr)\n      _ \u003c- cacheDsl.put(\"not exist yet\", \"now exist\")           // \u003c- (ref:put)\n      r3 \u003c- cacheDsl.fetch(\"not exist yet\")\n      _ \u003c- cacheDsl.clear(\"not exist yet\")\n      r4 \u003c- cacheDsl.fetch(\"not exist yet\")\n    } yield (r1, r2, r3, r4)\n    program(Caffeine().sync)                                    // \u003c- (ref:run)\n      .unsafeRunSync() must_== ((None, \"default\", Some(\"now exist\"), None))\n  }\n#+END_SRC\n\n#+BEGIN_SRC scala :exports none :noweb-ref async_load_failure\n    \"it should IO error when async load failure\" \u003e\u003e {\n      object dsl extends CaffeineAsyncCache[IO, String, String] {\n        implicit val executionContext = global\n      }\n      val program = for {\n        r1 \u003c- dsl.fetch(\"not exist yet\")\n        r2 \u003c- dsl.fetch(\"not exist yet\", _ =\u003e IO(\"default\"))\n      } yield (r1, r2)\n  \n      val failCache = mock[cache.AsyncCache[String, String]]\n      failCache.getIfPresent(\"not exist yet\") returns CompletableFuture.supplyAsync(() =\u003e IO.raiseError[String](new Exception(\"cache load error\")).unsafeRunSync())\n  \n      program(\n        failCache\n      ).unsafeRunSync() must throwA[Exception](message = \"cache load error\")\n    }\n#+END_SRC\n\n#+BEGIN_QUOTE\nThis README is a *literal programming* file, all code here will generate the [[https://github.com/jcouyang/jujiu/blob/master/src/test/scala/us/oyanglul/JujiuSpec.scala][test]] file\n#+END_QUOTE\n\nI can walk you through line by line though\n\n- [[(dsl)][line-(dsl)]] creates an instance of =CaffeineCache= which has side effect =IO=,\n  key is =String= and value is =String= as well\n- [[(fetch)][line-(fetch)]]  won't acutally trigger any effect, it just returns a\n  DSL, represent as type =Klesili[IO, Cache, String]= which in English,\n \"give me a =Cache= and I can provide you an\n  =IO[String]=\"\n\n- [[(fetchOr)][line-(fetchOr)]] is new =fetch= DSL, the second parameter is a function\n  =K =\u003e IO[V]=, if cache not exist, it will run the function can put the\n  result into the cache, and return the value\n\n- [[(put)][line-(put)]] will update the value of key \"not exist yet\" to \"overrided\"\n\n- [[(run)][line-(run)]] is the Scala idiomatic syntax to build synchronize\n  Caffeine Cache\\\\\n  if you still recall that the =program= is actually\n  =Klesili[IO, Cache, String]= so now\\\\\n  I provide it a =Cache= by =program(Caffeine().sync)=\\\\\n  it shall return me a =IO[String]= =.unsafeRunSync()= the IO and all\n  effects you described before in =program= will be triggered\\\\\n  and you will get the actual result\n\n** works with Cats IO\n   :PROPERTIES:\n   :CUSTOM_ID: and-cats-io\n   :END:\nJujiu has very flexible DSL, If you don't like Kleisli, it works with IO(technically you IO type just need to be a =Async=) as well\n\nwhat you'll need to import some syntax\n#+BEGIN_SRC scala :exports none :tangle no\nimport us.oyanglul.jujiu.syntax.cache._\n#+END_SRC\n\n#+BEGIN_SRC scala :exports none :noweb-ref catsio_cache\n  \"works with IO\" \u003e\u003e {\n    \"normal cache\" \u003e\u003e {\n      val c: Cache[IO, cache.Cache, String, String] = new CaffeineCache[IO, String, String] {}\n      implicit val cacheProvider: cache.Cache[String, String] = Caffeine().sync[String, String]\n      def program =\n        for {\n          _ \u003c- IO(println(\"something\"))\n          _ \u003c- c.putF(\"key1\", \"value1\")\n          r1 \u003c- c.fetchF(\"key1\")\n          r2 \u003c- c.fetchF(\"key2\", _ =\u003e IO(\"value2\"))\n          r3 \u003c- c.fetchAllF(List(\"key1\", \"key2\"))\n          r4 \u003c- c.parFetchAllF[List, IO.Par](List(\"key1\", \"key2\"))\n          _ \u003c- c.clearF(\"key1\")\n        } yield (r1, r2, r3, r4)\n      program.unsafeRunSync() must_== (\n        (\n          Some(\"value1\"),\n          \"value2\",\n          List(Some(\"value1\"), Some(\"value2\")),\n          List(Some(\"value1\"), Some(\"value2\"))\n        )\n      )\n    }\n\n#+END_SRC\n\nand provide =cacheProvider= implicitly, since you are not using Kleisli, you need to tell what cache\nthese DSLs will run on\n\n#+BEGIN_SRC scala :noweb-ref catsio_loading_cache\n    \"loading cache\" \u003e\u003e {\n      val c: LoadingCache[IO, cache.LoadingCache, String, String] = new CaffeineLoadingCache[IO, String, String] {}\n      implicit val cacheProvider: cache.LoadingCache[String, String] = Caffeine().sync(identity)\n      def program =\n        for {\n          _ \u003c- IO(println(\"something\"))\n          r1 \u003c- c.fetchF(\"1\")\n          r2 \u003c- c.fetchAllF(List(\"2\", \"3\"))\n          r3 \u003c- c.parFetchAllF[List, IO.Par](List(\"4\", \"5\"))\n        } yield (r1, r2, r3)\n      program.unsafeRunSync() must_== ((\"1\", List(\"2\", \"3\"), List(\"4\", \"5\")))\n    }\n  }\n#+END_SRC\n\n#+BEGIN_QUOTE\nsimilar to =ExecutionContext=, you need to provide context the thread can run on\n#+END_QUOTE\n\nand all dsl suffix with =F=\n** idiomatic syntax for Caffeine builder\n\nDealing with Java DSL and Java Future is too verbose and painful in\nScala project\n\nLet's see how Jiujiu makes Caffeine friendly to Cats IO as well\n\nA good example is the Async Loading Cache\n\nFirst you will need caffeine builder syntax\n#+BEGIN_SRC scala :export none :tangle no\nimport us.oyanglul.jujiu.syntax.caffeine._\n#+END_SRC\n\n#+BEGIN_SRC scala :noweb-ref caffeine_builder\n  \"it should able to get and set async loading cache\" \u003e\u003e {\n    object cache extends CaffeineAsyncLoadingCache[IO, Integer, String] {\n      implicit val executionContext = global // \u003c-- (ref:executionContext)\n    }\n\n    val program = for {\n      r1 \u003c- cache.fetch(1)\n      r2 \u003c- cache.fetch(2)\n      r3 \u003c- cache.fetchAll(List[Integer](1, 2, 3))\n    } yield (r1, r2, r3)\n\n    val caffeineA: com.github.benmanes.caffeine.cache.AsyncLoadingCache[Integer, String] = Caffeine()\n      .executionContext(global) // \u003c-- (ref:global)\n      .withExpire( // \u003c-- (ref:expire)\n        (_: Integer, _: String) =\u003e 1.second,\n        (_: Integer, _: String, currentDuration: FiniteDuration) =\u003e currentDuration,\n        (_: Integer, _: String, currentDuration: FiniteDuration) =\u003e currentDuration\n      )\n      .async((key: Integer) =\u003e IO(\"async string\" + key)) // \u003c-- (ref:async)\n\n    val caffeineB = Caffeine()\n      .withExpireAfterAccess(1.second)\n      .withExpireAfterWrite(2.seconds)\n      .withRefreshAfterWrite(3.seconds)\n      .async((key: Integer) =\u003e IO(\"async string\" + key))\n\n    val expected = (\n      \"async string1\",\n      \"async string2\",\n      List(\"async string1\", \"async string2\", \"async string3\")\n    )\n    program(caffeineA).unsafeRunSync() must_== expected\n    program(caffeineB).unsafeRunSync() must_== expected\n    program(Caffeine().async(_ =\u003e IO.raiseError(new Exception(\"something wrong\"))))\n      .unsafeRunSync() must throwA[Exception]\n  }\n#+END_SRC\n\n- [[(executionContext)][line-(executionContext)]] Async Loading Cache need an Execution Context to execute the Java\n  Future things\n\n- [[(global)][line-(global)]] =.executionContext(global)= will make sure the cache using Scala\n  execution context as default to execute java future, otherwise its default java folk join pool.\n  alternatively you can also use Akka's execution context.\n\n- [[(expire)][line-(expire)]] default the expiring policy, here it's more Scala idiomatic\n  lambda and =Duration=\n\n- [[(async)][line-(async)]] will create an\n  async loading cache.\n  the async loading function that it will use is =K =\u003e IO[V]= so you\n  don't need to deal with awful Java Future.\n\n** Works with Tagless Final\nNo matter what style of effect abstraction you project is using, Jujiu can easily fit in\n\ni.e. Tagless Final\n#+BEGIN_SRC scala :noweb-ref tagless_final\n  \"works with tagless final\" \u003e\u003e {\n    trait LogDsl[F[_]] {\n      def log(msg: String): F[Unit]\n    }\n\n    type ProgramDsl[F[_]] = CaffeineCache[F, String, String] with LogDsl[F]\n\n    def program[F[_]: Async](dsl: ProgramDsl[F])\n    (implicit ev: cache.Cache[String, String]): F[Option[String]] =\n      for {\n        value \u003c- dsl.fetchF(\"key\")\n        _ \u003c- dsl.log(\"something\")\n      } yield value\n\n    {\n      object dsl extends CaffeineCache[IO, String, String] with LogDsl[IO] {\n        def log(msg: String) = IO(println(msg))\n      }\n\n      implicit val cacheProvider: cache.Cache[String, String] = Caffeine().sync[String, String]\n\n      program[IO](dsl).unsafeRunSync() must_== None\n    }\n  }\n#+END_SRC\n\njust =extends CaffeineCache[F, K, V]= and provide =cacheProvider=\n\n** ReaderT Pattern\nif your code is in ReaderT pattern, good, it will fit in more naturally\n#+BEGIN_SRC scala :noweb-ref readerT\n  \"works with tagless final style readerT\" \u003e\u003e {\n    // Layer 1: Environment\n    trait HasLogger {\n      def logger: String =\u003e Unit\n    }\n    trait HasCacheProvider {\n      def cacheProvider: cache.Cache[String, String]\n    }\n\n    type Env = HasLogger with HasCacheProvider\n\n    // Layer 2: DSL\n    trait LogDsl[F[_]] {\n      def log(msg: String)(implicit M: Applicative[F]): Kleisli[F, Env, Unit] = Kleisli(a =\u003e M.pure(a.logger(msg)))\n    }\n\n    type Dsl[F[_]] = CaffeineCache[F, String, String] with LogDsl[F]\n\n    // Layer 3: Business\n    def program[F[_]](dsl: Dsl[F])(\n      implicit ev: Async[F]\n    ) =\n      for {\n        _ \u003c- dsl.log(\"something\")\n        value \u003c- dsl.fetch(\"key\").local[Env](_.cacheProvider)\n      } yield value\n\n    object dsl extends CaffeineCache[IO, String, String] with LogDsl[IO]\n\n    program[IO](dsl)\n      .run(new HasLogger with HasCacheProvider {\n        def logger = println\n        def cacheProvider = Caffeine().sync\n      })\n      .unsafeRunSync() must_== None\n  }\n#+END_SRC\n\nnotice that proper contravariant adapt need =.local[Env](_.cacheProvider)=\n\n** Extensible\nit's extensible by design as Kleisli, if you provider another cache provider, the same dsl\nwill work.\n#+BEGIN_SRC scala :noweb-ref redis\n  \"run on redis\" \u003e\u003e {\n    import redis.clients.jedis._\n\n    def program[F[_]: Async, S[_, _]](dsl: Cache[F, S, String, String]) = for {\n      r1 \u003c- dsl.fetch(\"not exist yet\")\n      r2 \u003c- dsl.fetch(\"not exist yet\", _ =\u003e Async[F].delay(\"default\"))\n      _ \u003c- dsl.put(\"not exist yet\", \"now exist\")\n      r3 \u003c- dsl.fetch(\"not exist yet\")\n      _ \u003c- dsl.clear(\"not exist yet\")\n      r4 \u003c- dsl.fetch(\"not exist yet\")\n    } yield (r1, r2, r3, r4)\n\n    type J[A, B] = Jedis\n    object dsl extends Cache[IO, J, String, String] {\n      def put(k: String, v: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Unit] =\n        Kleisli { redis =\u003e\n          M.delay{\n            redis.set(k, v)\n            ()\n          }\n        }\n      def fetch(k: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Option[String]] =\n        Kleisli(redis =\u003e M.delay(Option(redis.get(k))))\n      def clear(k: String)(implicit M: Async[IO]): Kleisli[IO, Jedis, Unit] =\n        Kleisli(redis =\u003e M.delay{\n          redis.del(k)\n          ()\n        })\n    }\n\n    program(dsl).run(\n       new Jedis(\"localhost\")\n    ).unsafeRunSync() must_== ((None, \"default\", Some(\"now exist\"), None))\n  }.pendingUntilFixed(\"Redis\")\n#+END_SRC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcouyang%2Fjujiu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcouyang%2Fjujiu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcouyang%2Fjujiu/lists"}