├── project ├── build.properties ├── plugins.sbt └── Dependencies.scala ├── site ├── docs │ ├── CNAME │ ├── streams │ │ └── index.md │ ├── effects │ │ ├── index.md │ │ ├── server.md │ │ ├── keys.md │ │ ├── connection.md │ │ ├── lists.md │ │ ├── strings.md │ │ ├── sortedsets.md │ │ ├── hashes.md │ │ ├── sets.md │ │ ├── geo.md │ │ ├── bitmaps.md │ │ └── scripting.md │ └── quickstart.md └── src │ └── main │ └── resources │ └── microsite │ ├── img │ └── favicon.png │ └── data │ └── menu.yml ├── .scala-steward.conf ├── scripts └── check_dirty.sh ├── flake.nix ├── modules ├── tests │ └── src │ │ └── test │ │ ├── resources │ │ └── lua │ │ │ └── hsetAndExpire.lua │ │ └── scala │ │ └── dev │ │ └── profunktor │ │ └── redis4cats │ │ ├── package.scala │ │ ├── IOSuite.scala │ │ ├── RedisClusterSpec.scala │ │ ├── RedisStreamSpec.scala │ │ ├── RedisSpec.scala │ │ └── OptimisticLockSuite.scala ├── examples │ └── src │ │ └── main │ │ ├── resources │ │ └── logback.xml │ │ └── scala │ │ └── dev │ │ └── profunktor │ │ └── redis4cats │ │ ├── LoggerIOApp.scala │ │ ├── Demo.scala │ │ ├── RedisLiftKDemo.scala │ │ ├── RedisSortedSetsDemo.scala │ │ ├── RedisClusterStringsDemo.scala │ │ ├── PublisherDemo.scala │ │ ├── RedisKeysDemo.scala │ │ ├── RedisHashesDemo.scala │ │ ├── RedisSetsDemo.scala │ │ ├── RedisPipelineDemo.scala │ │ ├── JsonCodecDemo.scala │ │ ├── RedisGeoDemo.scala │ │ ├── RedisListsDemo.scala │ │ ├── RedisClusterFromUnderlyingDemo.scala │ │ ├── RedisMasterReplicaStringsDemo.scala │ │ ├── StreamingDemo.scala │ │ ├── RedisClusterTransactionsDemo.scala │ │ ├── RedisScriptsDemo.scala │ │ ├── RedisStringsDemo.scala │ │ ├── PubSubDemo.scala │ │ ├── RedisBitmapsDemo.scala │ │ ├── RedisPoolDemo.scala │ │ └── RedisTxDemo.scala ├── core │ └── src │ │ ├── main │ │ ├── scala-2.12 │ │ │ └── dev │ │ │ │ └── profunktor │ │ │ │ └── redis4cats │ │ │ │ └── JavaConversions.scala │ │ ├── scala-2.13+ │ │ │ └── dev.profunktor.redis4cats │ │ │ │ └── JavaConversions.scala │ │ ├── scala │ │ │ └── dev │ │ │ │ └── profunktor │ │ │ │ └── redis4cats │ │ │ │ ├── tx │ │ │ │ ├── package.scala │ │ │ │ ├── TxStore.scala │ │ │ │ └── TxRunner.scala │ │ │ │ ├── codecs │ │ │ │ ├── splits │ │ │ │ │ ├── package.scala │ │ │ │ │ ├── SplitEpi.scala │ │ │ │ │ └── SplitMono.scala │ │ │ │ └── Codecs.scala │ │ │ │ ├── effect │ │ │ │ ├── TxThreadFactory.scala │ │ │ │ ├── FutureLift.scala │ │ │ │ ├── Log.scala │ │ │ │ ├── TxExecutor.scala │ │ │ │ └── MkRedis.scala │ │ │ │ └── connection │ │ │ │ └── RedisURI.scala │ │ ├── scala-3 │ │ │ └── dev │ │ │ │ └── profunktor │ │ │ │ └── redis4cats │ │ │ │ ├── TypeInequalityCompat.scala │ │ │ │ └── syntax.scala │ │ └── scala-2 │ │ │ └── dev │ │ │ └── profunktor │ │ │ └── redis4cats │ │ │ ├── syntax │ │ │ ├── RedisURIOps.scala │ │ │ └── macros.scala │ │ │ └── TypeInequalityCompat.scala │ │ └── test │ │ └── scala │ │ └── dev │ │ └── profunktor │ │ └── redis4cats │ │ ├── codecs │ │ ├── laws │ │ │ ├── SplitEpiLaws.scala │ │ │ └── SplitMonoLaws.scala │ │ ├── DisciplineSuite.scala │ │ ├── SplitEpiTests.scala │ │ ├── SplitMonoTests.scala │ │ └── SplitMorphismTest.scala │ │ └── effect │ │ └── FutureLiftSuite.scala ├── effects │ └── src │ │ └── main │ │ └── scala │ │ └── dev │ │ └── profunktor │ │ └── redis4cats │ │ ├── algebra │ │ ├── hyperloglog.scala │ │ ├── autoflush.scala │ │ ├── server.scala │ │ ├── connection.scala │ │ ├── transaction.scala │ │ ├── streams.scala │ │ ├── geo.scala │ │ ├── lists.scala │ │ ├── sets.scala │ │ ├── bitmaps.scala │ │ ├── strings.scala │ │ ├── keys.scala │ │ ├── scripts.scala │ │ └── hashes.scala │ │ ├── commands.scala │ │ └── extensions │ │ └── luaScripting.scala ├── streams │ └── src │ │ └── main │ │ └── scala │ │ └── dev │ │ └── profunktor │ │ └── redis4cats │ │ ├── streams │ │ ├── data.scala │ │ └── streams.scala │ │ ├── StreamsInstances.scala │ │ ├── pubsub │ │ ├── data.scala │ │ └── internals │ │ │ ├── PubSubState.scala │ │ │ ├── Redis4CatsSubscription.scala │ │ │ ├── Publisher.scala │ │ │ ├── PubSubInternals.scala │ │ │ ├── LivePubSubStats.scala │ │ │ └── LivePubSubCommands.scala │ │ └── RestartOnTimeout.scala └── log4cats │ └── src │ └── main │ └── scala │ └── dev │ └── profunktor │ └── redis4cats │ └── log4cats.scala ├── .github ├── workflows │ ├── release-drafter.yml │ ├── publish-site.yml │ ├── release.yml │ └── ci.yml ├── release-drafter.yml └── stale.yml ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── .gitignore ├── docker-compose.yml ├── redis.yml ├── .scalafmt.conf ├── CONTRIBUTING.md └── flake.lock /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /site/docs/CNAME: -------------------------------------------------------------------------------- 1 | redis4cats.profunktor.dev 2 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ { groupId = "org.scalameta", artifactId = "scalafmt-core" } ] 2 | -------------------------------------------------------------------------------- /scripts/check_dirty.sh: -------------------------------------------------------------------------------- 1 | diff=$(git diff --shortstat 2> /dev/null | tail -n1) 2 | if [ -n "$diff" ] ; 3 | then (echo "dirty"; exit 1) 4 | fi -------------------------------------------------------------------------------- /site/src/main/resources/microsite/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/profunktor/redis4cats/HEAD/site/src/main/resources/microsite/img/favicon.png -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ProfunKtor - Scala development tools"; 3 | 4 | inputs.dev-tools.url = github:profunktor/dev-tools; 5 | 6 | outputs = { dev-tools, ... }: { 7 | inherit (dev-tools) devShells packages; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /modules/tests/src/test/resources/lua/hsetAndExpire.lua: -------------------------------------------------------------------------------- 1 | local key = KEYS[1] 2 | local field = ARGV[1] 3 | local value = ARGV[2] 4 | local ttl = tonumber(ARGV[3]) 5 | 6 | local numFieldsSet = redis.call('hset', key, field, value) 7 | redis.call('expire', key, ttl) 8 | return numFieldsSet 9 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - series/1.x # CE3 7 | - series/2.x 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 16 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🔧 Internal Changes' 5 | label: 'internal' 6 | - title: '📗 Documentation' 7 | label: 'documentation' 8 | - title: '🤖 Dependency Updates' 9 | label: 'dependency-update' 10 | exclude-labels: 11 | - 'maintenance' 12 | template: | 13 | ## 🚀 Changes 14 | 15 | $CHANGES 16 | -------------------------------------------------------------------------------- /modules/examples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /site/docs/streams/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Streams API" 4 | number: 5 5 | position: 5 6 | --- 7 | 8 | # Streams API (experimental) 9 | 10 | The experimental API that operates at the stream level `Stream[F[_], A]` on top of `fs2`. 11 | 12 | - **[PubSub](./pubsub.html)**: Simple, safe and pure functional streaming client to interact with [Redis PubSub](https://redis.io/topics/pubsub). 13 | - **[Streams](./streams.html)**: High-level, safe and pure functional API on top of [Redis Streams](https://redis.io/topics/streams-intro). 14 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: assign and label scala-steward's PRs 3 | conditions: 4 | - author=gvolpe 5 | - title~=(?i)Update 6 | - body~=(?i)Configure Scala Steward for your repository 7 | actions: 8 | label: 9 | add: [dependency-update] 10 | - name: automatically merge gvolpe's Scala Steward PRs on CI success 11 | conditions: 12 | - author=gvolpe 13 | - title~=(?i)Update 14 | - body~=(?i)Configure Scala Steward for your repository 15 | - status-success=Build 16 | actions: 17 | merge: 18 | method: merge 19 | -------------------------------------------------------------------------------- /site/docs/effects/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Effects API" 4 | number: 4 5 | position: 4 6 | --- 7 | 8 | # Effects API 9 | 10 | The API that operates at the effect level `F[_]` on top of `cats-effect`. 11 | 12 | - **[Bitmaps API](./bitmaps.html)** 13 | - **[Connection API](./connection.html)** 14 | - **[Geo API](./geo.html)** 15 | - **[Hashes API](./hashes.html)** 16 | - **[Keys API](./keys.html)** 17 | - **[Lists API](./lists.html)** 18 | - **[Scripting API](./scripting.html)** 19 | - **[Server API](./server.html)** 20 | - **[Sets API](./sets.html)** 21 | - **[Sorted Sets API](./sortedsets.html)** 22 | - **[Strings API](./strings.html)** 23 | 24 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | # Label to use when marking an issue as stale 10 | staleLabel: wontfix 11 | # Comment to post when marking an issue as stale. Set to `false` to disable 12 | markComment: > 13 | This issue has been automatically marked as stale because it has not had 14 | recent activity. It will be closed if no further activity occurs. Thank you 15 | for your contributions. 16 | # Comment to post when closing a stale issue. Set to `false` to disable 17 | closeComment: false 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We are committed to providing a friendly, safe and welcoming 4 | environment for all, regardless of level of experience, gender, gender 5 | identity and expression, sexual orientation, disability, personal 6 | appearance, body size, race, ethnicity, age, religion, nationality, or 7 | other such characteristics. 8 | 9 | Everyone is expected to follow the [Scala Code of Conduct] when 10 | discussing the project on the available communication channels. If you 11 | are being harassed, please contact us immediately so that we can 12 | support you. 13 | 14 | ## Moderation 15 | 16 | For any questions, concerns, or moderation requests please contact a 17 | member of the project. 18 | 19 | [Scala Code of Conduct]: https://typelevel.org/code-of-conduct.html 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.pyc 6 | *.tm.epoch 7 | *.vim 8 | */project/boot 9 | */project/build/target 10 | */project/project.target.config-classes 11 | *-shim.sbt 12 | *~ 13 | .#* 14 | .*.swp 15 | .DS_Store 16 | .cache 17 | .cache 18 | .classpath 19 | .codefellow 20 | .ensime* 21 | .eprj 22 | .history 23 | .idea 24 | .manager 25 | .multi-jvm 26 | .project 27 | .scala_dependencies 28 | .scalastyle 29 | .settings 30 | .tags 31 | .tags_sorted_by_file 32 | .target 33 | .vscode 34 | .worksheet 35 | Makefile 36 | TAGS 37 | lib_managed 38 | logs 39 | project/boot/* 40 | project/**/metals.sbt 41 | project/plugins/project 42 | src_managed 43 | target 44 | tm*.lck 45 | tm*.log 46 | tm.out 47 | worker*.log 48 | /bin 49 | tags 50 | tq:q 51 | *DoNotCommit* 52 | .sbtopts 53 | .metals 54 | .bloop 55 | .bsp 56 | .envrc 57 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Classpaths.sbtPluginReleases 2 | 3 | ThisBuild / libraryDependencySchemes += "org.scala-lang.modules" %% "scala-xml" % VersionScheme.Always 4 | 5 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 6 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") 7 | addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") 8 | addSbtPlugin("com.github.sbt" % "sbt-header" % "5.11.0") 9 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 10 | addSbtPlugin("com.47deg" % "sbt-microsites" % "1.4.4") 11 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.7.2") 12 | addSbtPlugin("com.github.sbt" % "sbt-site" % "1.7.0") 13 | addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.6.0") 14 | addSbtPlugin("com.scalapenos" % "sbt-prompt" % "2.0.0") 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | SingleNode: 3 | restart: always 4 | image: redis:8.0.1 5 | ports: 6 | - "6379:6379" 7 | environment: 8 | - DEBUG=false 9 | command: redis-server --notify-keyspace-events KEA 10 | 11 | ReplicaNode: 12 | restart: always 13 | image: redis:8.0.1 14 | ports: 15 | - "6380:6379" 16 | command: redis-server --replicaof SingleNode 6379 17 | links: 18 | - SingleNode:SingleNode 19 | environment: 20 | - DEBUG=false 21 | 22 | RedisCluster: 23 | restart: always 24 | image: yisraelu/redis-cluster:8.0.1 25 | ports: 26 | - "30001:30001" 27 | - "30002:30002" 28 | - "30003:30003" 29 | - "30004:30004" 30 | - "30005:30005" 31 | - "30006:30006" 32 | environment: 33 | - IP=0.0.0.0 34 | - INITIAL_PORT=30001 35 | - DEBUG=false 36 | -------------------------------------------------------------------------------- /.github/workflows/publish-site.yml: -------------------------------------------------------------------------------- 1 | name: Microsite 2 | 3 | on: 4 | # to allow the manual trigger workflows 5 | workflow_dispatch: {} 6 | push: 7 | branches: 8 | - series/2.x 9 | paths: 10 | - "site/**" 11 | - "**/README.md" 12 | 13 | jobs: 14 | publish: 15 | env: 16 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v2.3.2 20 | with: 21 | fetch-depth: 0 # fetch all branches & tags 22 | 23 | - name: "Install Nix ❄️" 24 | uses: cachix/install-nix-action@v31.2.0 25 | 26 | - name: "Install Cachix ❄️" 27 | uses: cachix/cachix-action@v16 28 | with: 29 | name: profunktor 30 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 31 | 32 | - name: "Building and publishing microsite 🚧" 33 | run: nix run .#sbt -- publishSite 34 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2.12/dev/profunktor/redis4cats/JavaConversions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import scala.collection.convert.{ DecorateAsJava, DecorateAsScala } 20 | 21 | object JavaConversions extends DecorateAsJava with DecorateAsScala 22 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2.13+/dev.profunktor.redis4cats/JavaConversions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import scala.collection.convert.{ AsJavaExtensions, AsScalaExtensions } 20 | 21 | object JavaConversions extends AsJavaExtensions with AsScalaExtensions 22 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/tx/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import scala.util.control.NoStackTrace 20 | 21 | package object tx { 22 | case object PipelineError extends NoStackTrace 23 | case object TransactionDiscarded extends NoStackTrace 24 | } 25 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/hyperloglog.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | trait HyperLogLogCommands[F[_], K, V] { 20 | def pfAdd(key: K, values: V*): F[Long] 21 | def pfCount(key: K): F[Long] 22 | def pfMerge(outputKey: K, inputKeys: K*): F[Unit] 23 | } 24 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/autoflush.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | trait PipelineCommands[F[_]] extends AutoFlush[F] 20 | 21 | trait AutoFlush[F[_]] { 22 | def enableAutoFlush: F[Unit] 23 | def disableAutoFlush: F[Unit] 24 | def flushCommands: F[Unit] 25 | } 26 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/streams/data.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | package streams 19 | 20 | import dev.profunktor.redis4cats.effects.XAddArgs 21 | 22 | object data { 23 | 24 | final case class XAddMessage[K, V](key: K, body: Map[K, V], args: XAddArgs = XAddArgs()) 25 | 26 | } 27 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-3/dev/profunktor/redis4cats/TypeInequalityCompat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import scala.util.NotGiven 20 | 21 | trait TypeInequalityCompat { 22 | 23 | sealed class =!=[A, B] 24 | 25 | object =!= { 26 | implicit def nequal[A, B](implicit ev: NotGiven[A =:= B]): =!=[A, B] = new =!=[A, B] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /redis.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | # See Contribution Guidelines in README. Redis 7.2.4 is the last redis version before the Valkey fork. 3 | services: 4 | SingleNodeRedis: 5 | restart: always 6 | image: redis:${REDIS_VERSION:-7.2.4} 7 | ports: 8 | - "6379:6379" 9 | environment: 10 | - DEBUG=false 11 | command: redis-server --notify-keyspace-events KEA 12 | 13 | ReplicaNode: 14 | restart: always 15 | image: redis:${REDIS_VERSION:-7.2.4} 16 | ports: 17 | - "6380:6379" 18 | command: redis-server --replicaof SingleNodeRedis 6379 19 | links: 20 | - SingleNodeRedis:SingleNodeRedis 21 | environment: 22 | - DEBUG=false 23 | 24 | RedisCluster: 25 | restart: always 26 | image: grokzen/redis-cluster:${REDIS_VERSION:-7.2.4} 27 | ports: 28 | - "30001:30001" 29 | - "30002:30002" 30 | - "30003:30003" 31 | - "30004:30004" 32 | - "30005:30005" 33 | - "30006:30006" 34 | environment: 35 | - INITIAL_PORT=30001 36 | - DEBUG=false 37 | 38 | -------------------------------------------------------------------------------- /site/docs/quickstart.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Quick Start" 4 | number: 1 5 | position: 1 6 | --- 7 | 8 | # Quick Start 9 | 10 | ```scala mdoc:silent 11 | import cats.effect._ 12 | import cats.implicits._ 13 | import dev.profunktor.redis4cats.Redis 14 | import dev.profunktor.redis4cats.effect.Log.Stdout._ 15 | 16 | object QuickStart extends IOApp.Simple { 17 | 18 | def run: IO[Unit] = 19 | Redis[IO].utf8("redis://localhost").use { redis => 20 | for { 21 | _ <- redis.set("foo", "123") 22 | x <- redis.get("foo") 23 | _ <- redis.setNx("foo", "should not happen") 24 | y <- redis.get("foo") 25 | _ <- IO(println(x === y)) // true 26 | } yield () 27 | } 28 | 29 | } 30 | ``` 31 | 32 | This is the simplest way to get up and running with a single-node Redis connection. To learn more about commands, clustering, pipelining and transactions, please have a look at the extensive documentation. 33 | 34 | You can continue reading about the different ways of acquiring a client and a connection [here](./client.html). 35 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor 18 | 19 | import cats.effect.{ IO, Resource } 20 | import fs2.Stream 21 | 22 | package object redis4cats { 23 | 24 | implicit class ResourceOps[A](res: Resource[IO, A]) { 25 | def withFinalizer(f: Stream[IO, Boolean]): Resource[IO, A] = 26 | Stream.resource(res).interruptWhen(f).compile.resource.lastOrError 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | # to allow the manual trigger workflows 4 | workflow_dispatch: {} 5 | push: 6 | branches: 7 | - series/1.x # CE3 8 | - series/2.x 9 | tags: 10 | - "v1.*" 11 | - "v2.*" 12 | 13 | jobs: 14 | build: 15 | name: Publish 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 # fetch all branches & tags 21 | 22 | - name: "Install Nix ❄️" 23 | uses: cachix/install-nix-action@v31.2.0 24 | 25 | - name: "Install Cachix ❄️" 26 | uses: cachix/cachix-action@v16 27 | with: 28 | name: profunktor 29 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 30 | 31 | - name: "Publish ${{ github.ref }} 🚀" 32 | env: 33 | PGP_PASSPHRASE: "${{ secrets.PGP_PASSPHRASE }}" 34 | PGP_SECRET: "${{ secrets.PGP_SECRET }}" 35 | SONATYPE_PASSWORD: "${{ secrets.SONATYPE_PASSWORD }}" 36 | SONATYPE_USERNAME: "${{ secrets.SONATYPE_USERNAME }}" 37 | run: nix run .#sbt -- ci-release 38 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.0" 2 | runner.dialect = "scala213source3" 3 | align.openParenCallSite = false 4 | align.tokens = ["%", "%%", {code = "=>", owner = "Case"}, {code = "=", owner = "(Enumerator.Val|Defn.(Va(l|r)|Def|Type))"}, ] 5 | align.arrowEnumeratorGenerator = true 6 | binPack.parentConstructors = false 7 | danglingParentheses.preset = true 8 | newlines.implicitParamListModifierForce = [before] 9 | maxColumn = 120 10 | project.excludeFilters = [ .scalafmt.conf ] 11 | project.git = true 12 | rewrite.rules = [PreferCurlyFors, RedundantBraces, RedundantParens, SortImports] 13 | spaces.inImportCurlyBraces = true 14 | style = defaultWithAlign 15 | 16 | rewriteTokens { 17 | "⇒" = "=>" 18 | "→" = "->" 19 | "←" = "<-" 20 | } 21 | 22 | project { 23 | excludeFilters = [ 24 | "/scala-3/" 25 | ] 26 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/RedisURIOps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.syntax 18 | 19 | import dev.profunktor.redis4cats.connection.RedisURI 20 | 21 | class RedisURIOps(val sc: StringContext) extends AnyVal { 22 | def redis(args: Any*): RedisURI = macro macros.RedisLiteral.make 23 | } 24 | 25 | trait RedisSyntax { 26 | implicit def toRedisURIOps(sc: StringContext): RedisURIOps = 27 | new RedisURIOps(sc) 28 | } 29 | 30 | object literals extends RedisSyntax 31 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/laws/SplitEpiLaws.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs.laws 18 | 19 | import cats.laws._ 20 | import dev.profunktor.redis4cats.codecs.splits.SplitEpi 21 | 22 | final case class SplitEpiLaws[A, B]( 23 | epi: SplitEpi[A, B] 24 | ) { 25 | 26 | def identity(b: B): IsEq[B] = 27 | (epi.get compose epi.reverseGet)(b) <-> b 28 | 29 | def idempotence(a: A): IsEq[A] = { 30 | val f = epi.reverseGet compose epi.get 31 | f(a) <-> f(f(a)) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /modules/log4cats/src/main/scala/dev/profunktor/redis4cats/log4cats.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import dev.profunktor.redis4cats.effect.Log 20 | import org.typelevel.log4cats.Logger 21 | 22 | object log4cats { 23 | 24 | implicit def log4CatsInstance[F[_]: Logger]: Log[F] = 25 | new Log[F] { 26 | def debug(msg: => String): F[Unit] = Logger[F].debug(msg) 27 | def error(msg: => String): F[Unit] = Logger[F].error(msg) 28 | def info(msg: => String): F[Unit] = Logger[F].info(msg) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/laws/SplitMonoLaws.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs.laws 18 | 19 | import cats.laws._ 20 | import dev.profunktor.redis4cats.codecs.splits.SplitMono 21 | 22 | final case class SplitMonoLaws[A, B]( 23 | mono: SplitMono[A, B] 24 | ) { 25 | 26 | def identity(a: A): IsEq[A] = 27 | (mono.reverseGet compose mono.get)(a) <-> a 28 | 29 | def idempotence(b: B): IsEq[B] = { 30 | val f = mono.get compose mono.reverseGet 31 | f(b) <-> f(f(b)) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /site/docs/effects/server.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Server" 4 | number: 13 5 | --- 6 | 7 | # Server API 8 | 9 | Purely functional interface for the [Server API](https://redis.io/commands#server). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.ServerCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, ServerCommands[IO, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[ServerCommands[IO, String]] 25 | } 26 | ``` 27 | 28 | ### Server Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 36 | 37 | commandsApi.use { redis => // ServerCommands[IO] 38 | redis.flushAll 39 | } 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/splits/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | 19 | import scala.util.Try 20 | 21 | package object splits { 22 | 23 | val stringDoubleEpi: SplitEpi[String, Double] = 24 | SplitEpi(s => Try(s.toDouble).getOrElse(0), _.toString) 25 | 26 | val stringLongEpi: SplitEpi[String, Long] = 27 | SplitEpi(s => Try(s.toLong).getOrElse(0), _.toString) 28 | 29 | val stringIntEpi: SplitEpi[String, Int] = 30 | SplitEpi(s => Try(s.toInt).getOrElse(0), _.toString) 31 | 32 | } 33 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/IOSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect._ 20 | import cats.effect.unsafe.IORuntime 21 | import munit._ 22 | 23 | trait IOSuite extends FunSuite { 24 | implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global 25 | 26 | override def munitValueTransforms: List[ValueTransform] = 27 | super.munitValueTransforms :+ new ValueTransform( 28 | "IO", 29 | { case ioa: IO[_] => 30 | IO.defer(ioa).unsafeToFuture() 31 | } 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2/dev/profunktor/redis4cats/TypeInequalityCompat.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | private[redis4cats] trait TypeInequalityCompat { 20 | 21 | /** Type inequality 22 | * 23 | * Credits: https://stackoverflow.com/a/6929051 24 | */ 25 | sealed class =!=[A, B] 26 | 27 | object =!= extends NEqualLowPriority { 28 | implicit def nequal[A, B]: =!=[A, B] = new =!=[A, B] 29 | } 30 | 31 | trait NEqualLowPriority { 32 | implicit def equal[A]: =!=[A, A] = sys.error("should not be called") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/LoggerIOApp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, IOApp } 20 | import org.typelevel.log4cats.Logger 21 | import org.typelevel.log4cats.slf4j.Slf4jLogger 22 | 23 | /** Provides an instance of `Log` given an instance of `Logger`. 24 | * 25 | * For simplicity and re-usability in all the examples. 26 | */ 27 | trait LoggerIOApp extends IOApp.Simple { 28 | 29 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 30 | 31 | def program: IO[Unit] 32 | 33 | override def run: IO[Unit] = program 34 | 35 | } 36 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/Demo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import dev.profunktor.redis4cats.codecs.Codecs 20 | import dev.profunktor.redis4cats.codecs.splits._ 21 | import dev.profunktor.redis4cats.data.RedisCodec 22 | 23 | object Demo { 24 | 25 | val redisURI: String = "redis://localhost" 26 | val redisClusterURI: String = "redis://localhost:30001" 27 | val stringCodec: RedisCodec[String, String] = RedisCodec.Utf8 28 | val longCodec: RedisCodec[String, Long] = Codecs.derive(stringCodec, stringLongEpi) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/splits/SplitEpi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs.splits 18 | 19 | // Credits to Rob Norris (@tpolecat) -> https://skillsmatter.com/skillscasts/11626-keynote-pushing-types-and-gazing-at-the-stars 20 | final case class SplitEpi[A, B]( 21 | get: A => B, 22 | reverseGet: B => A 23 | ) extends (A => B) { 24 | 25 | def apply(a: A): B = get(a) 26 | 27 | def reverse: SplitMono[B, A] = 28 | SplitMono(reverseGet, get) 29 | 30 | def andThen[C](f: SplitEpi[B, C]): SplitEpi[A, C] = 31 | SplitEpi(get andThen f.get, f.reverseGet andThen reverseGet) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/splits/SplitMono.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs.splits 18 | 19 | // Credits to Rob Norris (@tpolecat) -> https://skillsmatter.com/skillscasts/11626-keynote-pushing-types-and-gazing-at-the-stars 20 | final case class SplitMono[A, B]( 21 | get: A => B, 22 | reverseGet: B => A 23 | ) extends (A => B) { 24 | 25 | def apply(a: A): B = get(a) 26 | 27 | def reverse: SplitEpi[B, A] = 28 | SplitEpi(reverseGet, get) 29 | 30 | def andThen[C](f: SplitMono[B, C]): SplitMono[A, C] = 31 | SplitMono(get andThen f.get, f.reverseGet andThen reverseGet) 32 | 33 | } 34 | -------------------------------------------------------------------------------- /site/docs/effects/keys.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Keys" 4 | number: 10 5 | --- 6 | 7 | # Keys API 8 | 9 | Purely functional interface for the [Keys API](https://redis.io/commands#generic). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.KeyCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | import scala.concurrent.duration._ 21 | 22 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 23 | 24 | val commandsApi: Resource[IO, KeyCommands[IO, String]] = { 25 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[KeyCommands[IO, String]] 26 | } 27 | ``` 28 | 29 | ### key Commands usage 30 | 31 | Once you have acquired a connection you can start using it: 32 | 33 | ```scala mdoc:silent 34 | import cats.effect.IO 35 | 36 | val key = "users" 37 | 38 | commandsApi.use { redis => // KeyCommands[IO, String] 39 | for { 40 | _ <- redis.del(key) 41 | _ <- redis.exists(key) 42 | _ <- redis.expire(key, Duration(5, SECONDS)) 43 | } yield () 44 | } 45 | ``` 46 | 47 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/StreamsInstances.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.Applicative 20 | import cats.effect.kernel.Clock 21 | import fs2.Stream 22 | 23 | import scala.concurrent.duration.FiniteDuration 24 | 25 | object StreamsInstances { 26 | implicit def fs2Clock[F[_]: Clock]: Clock[Stream[F, *]] = new Clock[Stream[F, *]] { 27 | override def applicative: Applicative[Stream[F, *]] = Applicative[Stream[F, *]] 28 | override def monotonic: Stream[F, FiniteDuration] = Stream.eval(Clock[F].monotonic) 29 | override def realTime: Stream[F, FiniteDuration] = Stream.eval(Clock[F].realTime) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/DisciplineSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | 19 | import munit._ 20 | import org.typelevel.discipline._ 21 | 22 | trait DisciplineSuite extends ScalaCheckSuite { 23 | 24 | def checkAll(name: String, ruleSet: Laws#RuleSet)( 25 | implicit loc: Location 26 | ): Unit = checkAll(new TestOptions(name, Set.empty, loc), ruleSet) 27 | 28 | def checkAll(options: TestOptions, ruleSet: Laws#RuleSet)( 29 | implicit loc: Location 30 | ): Unit = 31 | ruleSet.all.properties.toList.foreach { case (id, prop) => 32 | property(options.withName(s"${options.name}: $id"))(prop) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Scala 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - series/1.x # CE3 8 | - series/2.x 9 | paths: 10 | - "modules/**" 11 | 12 | jobs: 13 | build: 14 | name: Build 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | fetch-tags: true 22 | 23 | - name: "Starting up Redis 🐳" 24 | run: docker compose up -d 25 | 26 | - name: "Cache for sbt & coursier ♨️" 27 | uses: coursier/cache-action@v6 28 | 29 | - name: "Install Nix ❄️" 30 | uses: cachix/install-nix-action@v31.2.0 31 | 32 | - name: "Install Cachix ❄️" 33 | uses: cachix/cachix-action@v16 34 | with: 35 | name: profunktor 36 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 37 | 38 | - name: "Run tests and compile documentation 🚀" 39 | run: nix run .#sbt -- buildRedis4Cats 40 | 41 | - name: Check for untracked changes 42 | run: | 43 | git status 44 | ./scripts/check_dirty.sh 45 | 46 | - name: "Test for Binary Compatibility 📦" 47 | run: nix run .#sbt mimaReportBinaryIssuesIfRelevant 48 | 49 | - name: "Shutting down Redis 🐳" 50 | run: docker compose down 51 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/effect/TxThreadFactory.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.effect 18 | 19 | import java.time.{ Instant, ZoneOffset } 20 | import java.time.format.DateTimeFormatter 21 | import java.util.concurrent.ThreadFactory 22 | 23 | private[redis4cats] object TxThreadFactory extends ThreadFactory { 24 | override def newThread(r: Runnable): Thread = 25 | this.synchronized { 26 | val t: Thread = new Thread(r) 27 | val f = DateTimeFormatter.ofPattern("Hmd.S") 28 | val now = Instant.now().atOffset(ZoneOffset.UTC) 29 | val time = f.format(now) 30 | t.setName(s"redis-tx-ec-$time") 31 | return t 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-3/dev/profunktor/redis4cats/syntax.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.syntax 18 | 19 | import dev.profunktor.redis4cats.connection.RedisURI 20 | import org.typelevel.literally.Literally 21 | import scala.language.`3.0` 22 | 23 | object literals { 24 | extension (inline ctx: StringContext){ 25 | inline def redis(inline args: Any*):RedisURI = ${RedisLiteral('ctx, 'args)} 26 | } 27 | 28 | object RedisLiteral extends Literally[RedisURI]{ 29 | def validate(s: String)(using Quotes) = 30 | RedisURI.fromString(s) match { 31 | case Left(e) => Left(e.getMessage) 32 | case Right(_) => Right('{RedisURI.unsafeFromString(${ Expr(s) })}) 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/tx/TxStore.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.tx 18 | 19 | import cats.effect.kernel.{ Async, Ref } 20 | import cats.syntax.functor._ 21 | 22 | /** Provides a way to store transactional results for later retrieval. 23 | */ 24 | trait TxStore[F[_], K, V] { 25 | def get: F[Map[K, V]] 26 | def set(key: K)(v: V): F[Unit] 27 | } 28 | 29 | object TxStore { 30 | private[redis4cats] def make[F[_]: Async, K, V]: F[TxStore[F, K, V]] = 31 | Ref.of[F, Map[K, V]](Map.empty).map { ref => 32 | new TxStore[F, K, V] { 33 | def get: F[Map[K, V]] = ref.get 34 | def set(key: K)(v: V): F[Unit] = ref.update(_.updated(key, v)) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/server.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.effects.FlushMode 20 | 21 | import java.time.Instant 22 | 23 | trait ServerCommands[F[_], K] extends Flush[F, K] with Diagnostic[F] 24 | 25 | trait Flush[F[_], K] { 26 | def keys(key: K): F[List[K]] 27 | def flushAll: F[Unit] 28 | def flushAll(mode: FlushMode): F[Unit] 29 | def flushDb: F[Unit] 30 | def flushDb(mode: FlushMode): F[Unit] 31 | } 32 | 33 | trait Diagnostic[F[_]] { 34 | def info: F[Map[String, String]] 35 | def info(section: String): F[Map[String, String]] 36 | def dbsize: F[Long] 37 | def lastSave: F[Instant] 38 | def slowLogLen: F[Long] 39 | } 40 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/data.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.pubsub 18 | 19 | import dev.profunktor.redis4cats.data.RedisChannel 20 | import io.lettuce.core.pubsub.api.async.RedisPubSubAsyncCommands 21 | 22 | object data { 23 | 24 | trait RedisPubSubCommands[K, V] { 25 | def underlying: RedisPubSubAsyncCommands[K, V] 26 | } 27 | case class LivePubSubCommands[K, V](underlying: RedisPubSubAsyncCommands[K, V]) extends RedisPubSubCommands[K, V] 28 | 29 | case class Subscription[K](channel: RedisChannel[K], number: Long) 30 | 31 | object Subscription { 32 | def empty[K](channel: RedisChannel[K]): Subscription[K] = 33 | Subscription[K](channel, 0L) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /modules/core/src/main/scala-2/dev/profunktor/redis4cats/syntax/macros.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.syntax 18 | 19 | import dev.profunktor.redis4cats.connection.RedisURI 20 | import org.typelevel.literally.Literally 21 | 22 | object macros { 23 | 24 | object RedisLiteral extends Literally[RedisURI] { 25 | 26 | override def validate(c: Context)(s: String): Either[String, c.Expr[RedisURI]] = { 27 | import c.universe._ 28 | RedisURI.fromString(s) match { 29 | case Left(e) => Left(e.getMessage) 30 | case Right(_) => Right(c.Expr(q"dev.profunktor.redis4cats.connection.RedisURI.unsafeFromString($s)")) 31 | } 32 | } 33 | 34 | def make(c: Context)(args: c.Expr[Any]*): c.Expr[RedisURI] = apply(c)(args: _*) 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/connection.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | trait ConnectionCommands[F[_], K] extends Ping[F] with Auth[F] with Client[F, K] 20 | 21 | trait Ping[F[_]] { 22 | def ping: F[String] 23 | def select(index: Int): F[Unit] 24 | } 25 | 26 | trait Auth[F[_]] { 27 | def auth(password: CharSequence): F[Boolean] 28 | def auth(username: String, password: CharSequence): F[Boolean] 29 | } 30 | 31 | trait Client[F[_], K] { 32 | def setClientName(name: K): F[Boolean] 33 | def getClientName(): F[Option[K]] 34 | def getClientId(): F[Long] 35 | def getClientInfo: F[Map[String, String]] 36 | def setLibName(name: String): F[Boolean] 37 | def setLibVersion(version: String): F[Boolean] 38 | } 39 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisLiftKDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.data.EitherT 20 | import cats.effect.{ IO, Resource } 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisLiftKDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val usernameKey = "test" 29 | 30 | val showResult: Option[String] => IO[Unit] = 31 | _.fold(IO.println(s"Not found key: $usernameKey"))(s => IO.println(s)) 32 | 33 | val commandsApi: Resource[IO, RedisCommands[IO, String, String]] = 34 | Redis[IO].utf8(redisURI) 35 | 36 | commandsApi.use( 37 | _.liftK[EitherT[IO, String, *]] 38 | .get(usernameKey) 39 | .semiflatMap(x => showResult(x)) 40 | .value 41 | .void 42 | ) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /site/docs/effects/connection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Connection" 4 | number: 11 5 | --- 6 | 7 | # Connection API 8 | 9 | Purely functional interface for the [Connection API](https://redis.io/commands#connection). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.ConnectionCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, ConnectionCommands[IO, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[ConnectionCommands[IO, String]] 25 | } 26 | ``` 27 | 28 | ### Connection Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 36 | 37 | commandsApi.use { redis => // ConnectionCommands[IO, String] 38 | val clientName = "client_x" 39 | for { 40 | _ <- redis.ping.flatMap(putStrLn) // "pong" 41 | _ <- redis.setClientName(clientName) // true 42 | retrievedClientName <- redis.getClientName() 43 | _ <- putStrLn(retrievedClientName.getOrElse("")) // "client_x" 44 | } yield () 45 | } 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/transaction.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.tx.TxStore 20 | 21 | trait TransactionalCommands[F[_], K] extends Transaction[F] with Watcher[F, K] with HighLevelTx[F] with Pipelining[F] 22 | 23 | trait Transaction[F[_]] { 24 | def multi: F[Unit] 25 | def exec: F[Unit] 26 | def discard: F[Unit] 27 | } 28 | 29 | trait Watcher[F[_], K] { 30 | def watch(keys: K*): F[Unit] 31 | def unwatch: F[Unit] 32 | } 33 | 34 | trait HighLevelTx[F[_]] { 35 | def transact[A](fs: TxStore[F, String, A] => List[F[Unit]]): F[Map[String, A]] 36 | def transact_(fs: List[F[Unit]]): F[Unit] 37 | } 38 | 39 | trait Pipelining[F[_]] { 40 | def pipeline[A](fs: TxStore[F, String, A] => List[F[Unit]]): F[Map[String, A]] 41 | def pipeline_(fs: List[F[Unit]]): F[Unit] 42 | } 43 | -------------------------------------------------------------------------------- /site/docs/effects/lists.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Lists" 4 | number: 7 5 | --- 6 | 7 | # Lists API 8 | 9 | Purely functional interface for the [Lists API](https://redis.io/commands#list). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.ListCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, ListCommands[IO, String, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[ListCommands[IO, String, String]] 25 | } 26 | ``` 27 | 28 | ### List Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | val testKey = "listos" 36 | 37 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 38 | 39 | commandsApi.use { redis => // ListCommands[IO, String, String] 40 | for { 41 | _ <- redis.rPush(testKey, "one", "two", "three") 42 | x <- redis.lRange(testKey, 0, 10) 43 | _ <- putStrLn(s"Range: $x") 44 | y <- redis.lLen(testKey) 45 | _ <- putStrLn(s"Length: $y") 46 | a <- redis.lPop(testKey) 47 | _ <- putStrLn(s"Left Pop: $a") 48 | b <- redis.rPop(testKey) 49 | _ <- putStrLn(s"Right Pop: $b") 50 | z <- redis.lRange(testKey, 0, 10) 51 | _ <- putStrLn(s"Range: $z") 52 | } yield () 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /site/docs/effects/strings.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Strings" 4 | number: 10 5 | --- 6 | 7 | # Strings API 8 | 9 | Purely functional interface for the [Strings API](https://redis.io/commands#string). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.StringCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, StringCommands[IO, String, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[StringCommands[IO, String, String]] 25 | } 26 | ``` 27 | 28 | ### String Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | val usernameKey = "users" 36 | 37 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 38 | 39 | val showResult: Option[String] => IO[Unit] = 40 | _.fold(putStrLn(s"Not found key: $usernameKey"))(s => putStrLn(s)) 41 | 42 | commandsApi.use { redis => // StringCommands[IO, String, String] 43 | for { 44 | x <- redis.get(usernameKey) 45 | _ <- showResult(x) 46 | _ <- redis.set(usernameKey, "gvolpe") 47 | y <- redis.get(usernameKey) 48 | _ <- showResult(y) 49 | _ <- redis.setNx(usernameKey, "should not happen") 50 | w <- redis.get(usernameKey) 51 | _ <- showResult(w) 52 | } yield () 53 | } 54 | ``` 55 | 56 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/commands.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import algebra._ 20 | import cats.effect.kernel.Async 21 | import dev.profunktor.redis4cats.effect.Log 22 | 23 | trait RedisCommands[F[_], K, V] 24 | extends StringCommands[F, K, V] 25 | with HashCommands[F, K, V] 26 | with SetCommands[F, K, V] 27 | with SortedSetCommands[F, K, V] 28 | with ListCommands[F, K, V] 29 | with GeoCommands[F, K, V] 30 | with ConnectionCommands[F, K] 31 | with ServerCommands[F, K] 32 | with TransactionalCommands[F, K] 33 | with PipelineCommands[F] 34 | with ScriptCommands[F, K, V] 35 | with KeyCommands[F, K] 36 | with HyperLogLogCommands[F, K, V] 37 | with BitCommands[F, K, V] 38 | with StreamCommands[F, K, V] 39 | 40 | object RedisCommands { 41 | implicit class LiftKOps[F[_], K, V](val cmd: RedisCommands[F, K, V]) extends AnyVal { 42 | def liftK[G[_]: Async: Log]: RedisCommands[G, K, V] = 43 | cmd.asInstanceOf[BaseRedis[F, K, V]].liftK[G] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/PubSubState.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.pubsub.internals 18 | 19 | import cats.syntax.all._ 20 | import cats.effect.kernel.Concurrent 21 | import cats.effect.std.AtomicCell 22 | import dev.profunktor.redis4cats.data.{ RedisChannel, RedisPattern, RedisPatternEvent } 23 | 24 | /** We use `AtomicCell` instead of `Ref` because we need locking while side-effecting. */ 25 | case class PubSubState[F[_], K, V]( 26 | channelSubs: AtomicCell[F, Map[RedisChannel[K], Redis4CatsSubscription[F, V]]], 27 | patternSubs: AtomicCell[F, Map[RedisPattern[K], Redis4CatsSubscription[F, RedisPatternEvent[K, V]]]] 28 | ) 29 | object PubSubState { 30 | def make[F[_]: Concurrent, K, V]: F[PubSubState[F, K, V]] = 31 | for { 32 | channelSubs <- AtomicCell[F].of(Map.empty[RedisChannel[K], Redis4CatsSubscription[F, V]]) 33 | patternSubs <- AtomicCell[F].of(Map.empty[RedisPattern[K], Redis4CatsSubscription[F, RedisPatternEvent[K, V]]]) 34 | } yield apply(channelSubs, patternSubs) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /site/docs/effects/sortedsets.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Sorted Sets" 4 | number: 9 5 | --- 6 | 7 | # Sorted Sets API 8 | 9 | Purely functional interface for the [Sorted Sets API](https://redis.io/commands#sorted_set). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.SortedSetCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, SortedSetCommands[IO, String, Long]] = { 24 | Redis[IO].fromClient[String, Long](null, null.asInstanceOf[RedisCodec[String, Long]]).widen[SortedSetCommands[IO, String, Long]] 25 | } 26 | ``` 27 | 28 | ### Sorted Set Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | import dev.profunktor.redis4cats.effects.{Score, ScoreWithValue, ZRange} 35 | 36 | val testKey = "zztop" 37 | 38 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 39 | 40 | commandsApi.use { redis => // SortedSetCommands[IO, String, Long] 41 | for { 42 | _ <- redis.zAdd(testKey, args = None, ScoreWithValue(Score(1), 1), ScoreWithValue(Score(3), 2)) 43 | x <- redis.zRevRangeByScore(testKey, ZRange(0, 2), limit = None) 44 | _ <- putStrLn(s"Score: $x") 45 | y <- redis.zCard(testKey) 46 | _ <- putStrLn(s"Size: $y") 47 | z <- redis.zCount(testKey, ZRange(0, 1)) 48 | _ <- putStrLn(s"Count: $z") 49 | } yield () 50 | } 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/streams.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.effects.{ MessageId, StreamMessage, XAddArgs, XRangePoint, XReadOffsets, XTrimArgs } 20 | 21 | import scala.concurrent.duration.Duration 22 | 23 | trait StreamCommands[F[_], K, V] extends StreamGetter[F, K, V] with StreamSetter[F, K, V] 24 | 25 | trait StreamGetter[F[_], K, V] { 26 | 27 | def xRead( 28 | streams: Set[XReadOffsets[K]], 29 | block: Option[Duration] = None, 30 | count: Option[Long] = None 31 | ): F[List[StreamMessage[K, V]]] 32 | def xRange(key: K, start: XRangePoint, end: XRangePoint, count: Option[Long] = None): F[List[StreamMessage[K, V]]] 33 | def xRevRange(key: K, start: XRangePoint, end: XRangePoint, count: Option[Long] = None): F[List[StreamMessage[K, V]]] 34 | def xLen(key: K): F[Long] 35 | } 36 | 37 | trait StreamSetter[F[_], K, V] { 38 | 39 | def xAdd(key: K, body: Map[K, V], args: XAddArgs = XAddArgs()): F[MessageId] 40 | def xTrim(key: K, args: XTrimArgs): F[Long] 41 | def xDel(key: K, ids: String*): F[Long] 42 | } 43 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/connection/RedisURI.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.connection 18 | 19 | import cats.ApplicativeThrow 20 | import cats.implicits.toBifunctorOps 21 | import io.lettuce.core.{ RedisURI => JRedisURI } 22 | 23 | import scala.util.Try 24 | import scala.util.control.NoStackTrace 25 | 26 | sealed abstract class RedisURI private (val underlying: JRedisURI) 27 | 28 | object RedisURI { 29 | def make[F[_]: ApplicativeThrow](uri: => String): F[RedisURI] = 30 | ApplicativeThrow[F].catchNonFatal(new RedisURI(JRedisURI.create(uri)) {}) 31 | 32 | def fromUnderlying(j: JRedisURI): RedisURI = new RedisURI(j) {} 33 | 34 | def fromString(uri: String): Either[InvalidRedisURI, RedisURI] = 35 | Try(JRedisURI.create(uri)).toEither.bimap(InvalidRedisURI(uri, _), new RedisURI(_) {}) 36 | 37 | def unsafeFromString(uri: String): RedisURI = new RedisURI(JRedisURI.create(uri)) {} 38 | } 39 | 40 | final case class InvalidRedisURI(uri: String, throwable: Throwable) extends NoStackTrace { 41 | override def getMessage: String = Option(throwable.getMessage).getOrElse(s"Invalid Redis URI: $uri") 42 | } 43 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisSortedSetsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.SortedSetCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | import dev.profunktor.redis4cats.effects.{ Score, ScoreWithValue, ZRange } 23 | 24 | object RedisSortedSetsDemo extends LoggerIOApp { 25 | 26 | import Demo._ 27 | 28 | val program: IO[Unit] = { 29 | val testKey = "zztop" 30 | 31 | val commandsApi: Resource[IO, SortedSetCommands[IO, String, Long]] = 32 | Redis[IO].simple(redisURI, longCodec) 33 | 34 | commandsApi.use { redis => 35 | for { 36 | _ <- redis.zAdd(testKey, args = None, ScoreWithValue(Score(1), 1), ScoreWithValue(Score(3), 2)) 37 | x <- redis.zRevRangeByScore(testKey, ZRange(0, 2), limit = None) 38 | _ <- IO.println(s"Score: $x") 39 | y <- redis.zCard(testKey) 40 | _ <- IO.println(s"Size: $y") 41 | z <- redis.zCount(testKey, ZRange(0, 1)) 42 | _ <- IO.println(s"Count: $z") 43 | } yield () 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisClusterStringsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.StringCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisClusterStringsDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val usernameKey = "test" 29 | 30 | val showResult: Option[String] => IO[Unit] = 31 | _.fold(IO.println(s"Not found key: $usernameKey"))(s => IO.println(s)) 32 | 33 | val commandsApi: Resource[IO, StringCommands[IO, String, String]] = 34 | Redis[IO].clusterUtf8(redisClusterURI)() 35 | 36 | commandsApi 37 | .use { cmd => 38 | for { 39 | x <- cmd.get(usernameKey) 40 | _ <- showResult(x) 41 | _ <- cmd.set(usernameKey, "some value") 42 | y <- cmd.get(usernameKey) 43 | _ <- showResult(y) 44 | _ <- cmd.setNx(usernameKey, "should not happen") 45 | w <- cmd.get(usernameKey) 46 | _ <- showResult(w) 47 | } yield () 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/SplitEpiTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | 19 | import cats.Eq 20 | import cats.laws.discipline._ 21 | import dev.profunktor.redis4cats.codecs.laws.SplitEpiLaws 22 | import dev.profunktor.redis4cats.codecs.splits.SplitEpi 23 | import org.scalacheck.Arbitrary 24 | import org.scalacheck.Prop._ 25 | import org.typelevel.discipline.Laws 26 | 27 | // Credits to Rob Norris (@tpolecat) -> https://skillsmatter.com/skillscasts/11626-keynote-pushing-types-and-gazing-at-the-stars 28 | trait SplitEpiTests[A, B] extends Laws { 29 | def laws: SplitEpiLaws[A, B] 30 | 31 | def splitEpi( 32 | implicit a: Arbitrary[A], 33 | b: Arbitrary[B], 34 | eqA: Eq[A], 35 | eqB: Eq[B] 36 | ): RuleSet = 37 | new DefaultRuleSet( 38 | name = "splitEpimorphism", 39 | parent = None, 40 | "identity" -> forAll(laws.identity _), 41 | "idempotence" -> forAll(laws.idempotence _) 42 | ) 43 | } 44 | 45 | object SplitEpiTests { 46 | def apply[A, B](epi: SplitEpi[A, B]): SplitEpiTests[A, B] = 47 | new SplitEpiTests[A, B] { 48 | val laws = SplitEpiLaws[A, B](epi) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/SplitMonoTests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | 19 | import cats.Eq 20 | import cats.laws.discipline._ 21 | import dev.profunktor.redis4cats.codecs.laws.SplitMonoLaws 22 | import dev.profunktor.redis4cats.codecs.splits.SplitMono 23 | import org.scalacheck.Arbitrary 24 | import org.scalacheck.Prop._ 25 | import org.typelevel.discipline.Laws 26 | 27 | // Credits to Rob Norris (@tpolecat) -> https://skillsmatter.com/skillscasts/11626-keynote-pushing-types-and-gazing-at-the-stars 28 | trait SplitMonoTests[A, B] extends Laws { 29 | def laws: SplitMonoLaws[A, B] 30 | 31 | def splitMono( 32 | implicit a: Arbitrary[A], 33 | b: Arbitrary[B], 34 | eqA: Eq[A], 35 | eqB: Eq[B] 36 | ): RuleSet = 37 | new DefaultRuleSet( 38 | name = "splitMonomorphism", 39 | parent = None, 40 | "identity" -> forAll(laws.identity _), 41 | "idempotence" -> forAll(laws.idempotence _) 42 | ) 43 | } 44 | 45 | object SplitMonoTests { 46 | def apply[A, B](mono: SplitMono[A, B]): SplitMonoTests[A, B] = 47 | new SplitMonoTests[A, B] { 48 | val laws = SplitMonoLaws[A, B](mono) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /site/docs/effects/hashes.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Hashes" 4 | number: 6 5 | --- 6 | 7 | # Hashes API 8 | 9 | Purely functional interface for the [Hashes API](https://redis.io/commands#hash). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.HashCommands 16 | import dev.profunktor.redis4cats.log4cats._ 17 | import dev.profunktor.redis4cats.data._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, HashCommands[IO, String, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[HashCommands[IO, String, String]] 25 | } 26 | ``` 27 | 28 | ### Hash Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | val testKey = "foo" 36 | val testField = "bar" 37 | 38 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 39 | 40 | val showResult: Option[String] => IO[Unit] = 41 | _.fold(putStrLn(s"Not found key: $testKey | field: $testField"))(s => putStrLn(s)) 42 | 43 | commandsApi.use { redis => // HashCommands[IO, String, String] 44 | for { 45 | x <- redis.hGet(testKey, testField) 46 | _ <- showResult(x) 47 | _ <- redis.hSet(testKey, testField, "some value") 48 | y <- redis.hGet(testKey, testField) 49 | _ <- showResult(y) 50 | _ <- redis.hSetNx(testKey, testField, "should not happen") 51 | w <- redis.hGet(testKey, testField) 52 | _ <- showResult(w) 53 | _ <- redis.hDel(testKey, testField) 54 | z <- redis.hGet(testKey, testField) 55 | _ <- showResult(z) 56 | } yield () 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/PublisherDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.IO 20 | import dev.profunktor.redis4cats.connection._ 21 | import dev.profunktor.redis4cats.data.RedisChannel 22 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 23 | import dev.profunktor.redis4cats.pubsub.PubSub 24 | import fs2.Stream 25 | import scala.concurrent.duration._ 26 | import scala.util.Random 27 | 28 | object PublisherDemo extends LoggerIOApp { 29 | 30 | import Demo._ 31 | 32 | private val eventsChannel = RedisChannel("events") 33 | 34 | val stream: Stream[IO, Unit] = 35 | (for { 36 | client <- Stream.resource(RedisClient[IO].from(redisURI)) 37 | pubSub <- Stream.resource(PubSub.mkPublisherConnection[IO, String, String](client, stringCodec)) 38 | pub1 = pubSub.publish(eventsChannel) 39 | } yield Stream( 40 | Stream.awakeEvery[IO](3.seconds) >> Stream.eval(IO(Random.nextInt(100).toString)).through(pub1), 41 | Stream.awakeEvery[IO](6.seconds) >> Stream.eval(pubSub.pubSubSubscriptions(eventsChannel)).evalMap(IO.println) 42 | ).parJoin(2).drain).flatten 43 | 44 | val program: IO[Unit] = 45 | stream.compile.drain 46 | 47 | } 48 | -------------------------------------------------------------------------------- /site/docs/effects/sets.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Sets" 4 | number: 8 5 | --- 6 | 7 | # Sets API 8 | 9 | Purely functional interface for the [Sets API](https://redis.io/commands#set). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.SetCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, SetCommands[IO, String, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[SetCommands[IO, String, String]] 25 | } 26 | ``` 27 | 28 | ### Set Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | 35 | val testKey = "foos" 36 | 37 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 38 | 39 | val showResult: Set[String] => IO[Unit] = x => putStrLn(s"$testKey members: $x") 40 | 41 | commandsApi.use { redis => // SetCommands[IO, String, String] 42 | for { 43 | x <- redis.sMembers(testKey) 44 | _ <- showResult(x) 45 | _ <- redis.sAdd(testKey, "set value") 46 | y <- redis.sMembers(testKey) 47 | _ <- showResult(y) 48 | _ <- redis.sCard(testKey).flatMap(s => putStrLn(s"size: ${s.toString}")) 49 | _ <- redis.sRem("non-existing", "random") 50 | w <- redis.sMembers(testKey) 51 | _ <- showResult(w) 52 | _ <- redis.sRem(testKey, "set value") 53 | z <- redis.sMembers(testKey) 54 | _ <- showResult(z) 55 | _ <- redis.sCard(testKey).flatMap(s => putStrLn(s"size: ${s.toString}")) 56 | } yield () 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/streams/streams.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.streams 18 | 19 | import dev.profunktor.redis4cats.RestartOnTimeout 20 | import dev.profunktor.redis4cats.effects.{ MessageId, StreamMessage, XReadOffsets } 21 | import dev.profunktor.redis4cats.streams.data._ 22 | 23 | import scala.concurrent.duration.Duration 24 | 25 | /** @tparam F 26 | * the effect type 27 | * @tparam S 28 | * the stream type 29 | * @tparam K 30 | * the key type 31 | * @tparam V 32 | * the value type 33 | */ 34 | trait Streaming[F[_], S[_], K, V] { 35 | def append: S[XAddMessage[K, V]] => S[MessageId] 36 | 37 | def append(msg: XAddMessage[K, V]): F[MessageId] 38 | 39 | /** Read data from one or multiple streams, returning an entry per stream with an ID greater than the last received 40 | * ID. ID's are initialized with initialOffset field. 41 | * 42 | * @see 43 | * https://redis.io/commands/xread 44 | */ 45 | def read( 46 | streams: Set[XReadOffsets[K]], 47 | block: Option[Duration] = Some(Duration.Zero), 48 | count: Option[Long] = None, 49 | restartOnTimeout: RestartOnTimeout = RestartOnTimeout.always 50 | ): S[StreamMessage[K, V]] 51 | } 52 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/codecs/SplitMorphismTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | import dev.profunktor.redis4cats.codecs.splits._ 19 | 20 | class SplitMorphismTest extends DisciplineSuite { 21 | import TestSplitEpiInstances._ 22 | 23 | checkAll("IntDoubleInt", SplitMonoTests(intDoubleMono).splitMono) 24 | checkAll("IntString", SplitMonoTests(intStringMono).splitMono) 25 | 26 | checkAll("DoubleInt", SplitEpiTests(doubleIntEpi).splitEpi) 27 | checkAll("StringDouble", SplitEpiTests(stringDoubleEpi).splitEpi) 28 | checkAll("StringLong", SplitEpiTests(stringLongEpi).splitEpi) 29 | checkAll("StringInt", SplitEpiTests(stringIntEpi).splitEpi) 30 | } 31 | 32 | object TestSplitEpiInstances { 33 | import scala.util.Try 34 | 35 | // Just proving that these form a split monomorphism and won't pass the laws of epimorphisms 36 | val intDoubleMono: SplitMono[Int, Double] = 37 | SplitMono(_.toDouble, _.toInt) 38 | 39 | val intStringMono: SplitMono[Int, String] = 40 | SplitMono(_.toString, s => Try(s.toInt).getOrElse(0)) 41 | 42 | // Epimorphisms 43 | val doubleIntEpi: SplitEpi[Double, Int] = 44 | SplitEpi(s => Try(s.toInt).getOrElse(0), s => Try(s.toDouble).getOrElse(0)) 45 | 46 | } 47 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisKeysDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.{ KeyCommands, StringCommands } 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisKeysDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val usernameKey = "test" 29 | 30 | val showResult: Option[String] => IO[Unit] = 31 | _.fold(IO.println(s"Not found key: $usernameKey"))(s => IO.println(s)) 32 | 33 | val commandsApi: Resource[IO, KeyCommands[IO, String] with StringCommands[IO, String, String]] = 34 | Redis[IO].utf8(redisURI) 35 | 36 | commandsApi.use { redis => 37 | for { 38 | x <- redis.get(usernameKey) 39 | _ <- showResult(x) 40 | _ <- redis.set(usernameKey, "some value") 41 | y <- redis.get(usernameKey) 42 | _ <- showResult(y) 43 | _ <- redis.setNx(usernameKey, "should not happen") 44 | w <- redis.get(usernameKey) 45 | v <- redis.del(usernameKey) 46 | _ <- IO.println(s"del: $v") 47 | z <- redis.get(usernameKey) 48 | _ <- showResult(z) 49 | _ <- showResult(w) 50 | } yield () 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisHashesDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.HashCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisHashesDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val testKey = "foo" 29 | val testField = "bar" 30 | 31 | val showResult: Option[String] => IO[Unit] = 32 | _.fold(IO.println(s"Not found key: $testKey | field: $testField"))(s => IO.println(s)) 33 | 34 | val commandsApi: Resource[IO, HashCommands[IO, String, String]] = 35 | Redis[IO].utf8(redisURI) 36 | 37 | commandsApi.use { redis => 38 | for { 39 | x <- redis.hGet(testKey, testField) 40 | _ <- showResult(x) 41 | _ <- redis.hSet(testKey, testField, "some value") 42 | y <- redis.hGet(testKey, testField) 43 | _ <- showResult(y) 44 | _ <- redis.hSetNx(testKey, testField, "should not happen") 45 | w <- redis.hGet(testKey, testField) 46 | _ <- showResult(w) 47 | _ <- redis.hDel(testKey, testField) 48 | z <- redis.hGet(testKey, testField) 49 | _ <- showResult(z) 50 | } yield () 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisSetsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.SetCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisSetsDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val testKey = "foos" 29 | 30 | val showResult: Set[String] => IO[Unit] = x => IO.println(s"$testKey members: $x") 31 | 32 | val commandsApi: Resource[IO, SetCommands[IO, String, String]] = 33 | Redis[IO].utf8(redisURI) 34 | 35 | commandsApi.use { redis => 36 | for { 37 | x <- redis.sMembers(testKey) 38 | _ <- showResult(x) 39 | _ <- redis.sAdd(testKey, "set value") 40 | y <- redis.sMembers(testKey) 41 | _ <- showResult(y) 42 | _ <- redis.sCard(testKey).flatMap(s => IO.println(s"size: $s")) 43 | _ <- redis.sRem("non-existing", "random") 44 | w <- redis.sMembers(testKey) 45 | _ <- showResult(w) 46 | _ <- redis.sRem(testKey, "set value") 47 | z <- redis.sMembers(testKey) 48 | _ <- showResult(z) 49 | _ <- redis.sCard(testKey).flatMap(s => IO.println(s"size: $s")) 50 | } yield () 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/Redis4CatsSubscription.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.pubsub.internals 18 | 19 | import cats.Applicative 20 | import fs2.concurrent.Topic 21 | 22 | /** Stores an ongoing subscription. 23 | * 24 | * @param topic 25 | * single-publisher, multiple-subscribers. The same topic is reused if `subscribe` is invoked more than once. The 26 | * subscribers' streams are terminated when `None` is published. 27 | * @param subscribers 28 | * subscriber count, when `subscribers` reaches 0 `cleanup` is called and `None` is published to the topic. 29 | */ 30 | final private[redis4cats] case class Redis4CatsSubscription[F[_], V]( 31 | topic: Topic[F, Option[V]], 32 | subscribers: Long, 33 | cleanup: F[Unit] 34 | ) { 35 | assert(subscribers > 0, s"subscribers must be > 0, was $subscribers") 36 | 37 | def addSubscriber: Redis4CatsSubscription[F, V] = copy(subscribers = subscribers + 1) 38 | def removeSubscriber: Redis4CatsSubscription[F, V] = copy(subscribers = subscribers - 1) 39 | def isLastSubscriber: Boolean = subscribers == 1 40 | 41 | def stream(onTermination: F[Unit])( 42 | implicit F: Applicative[F] 43 | ): fs2.Stream[F, V] = 44 | topic.subscribe(500).unNoneTerminate.onFinalize(onTermination) 45 | } 46 | -------------------------------------------------------------------------------- /site/src/main/resources/microsite/data/menu.yml: -------------------------------------------------------------------------------- 1 | options: 2 | - title: Quick Start 3 | url: quickstart.html 4 | menu_section: quickstart 5 | 6 | - title: Client 7 | url: client.html 8 | menu_section: client 9 | 10 | - title: Codecs 11 | url: codecs.html 12 | menu_section: codecs 13 | 14 | - title: Effects API 15 | url: effects/ 16 | menu_section: effectapi 17 | 18 | nested_options: 19 | - title: Bitmaps 20 | url: effects/bitmaps.html 21 | menu_section: effectapi 22 | 23 | - title: Connection 24 | url: effects/connection.html 25 | menu_section: effectapi 26 | 27 | - title: Geo 28 | url: effects/geo.html 29 | menu_section: effectapi 30 | 31 | - title: Hashes 32 | url: effects/hashes.html 33 | menu_section: effectapi 34 | 35 | - title: Keys 36 | url: effects/keys.html 37 | menu_section: effectapi 38 | 39 | - title: Lists 40 | url: effects/lists.html 41 | menu_section: effectapi 42 | 43 | - title: Scripting 44 | url: effects/scripting.html 45 | menu_section: effectapi 46 | 47 | - title: Server 48 | url: effects/server.html 49 | menu_section: effectapi 50 | 51 | - title: Sets 52 | url: effects/sets.html 53 | menu_section: effectapi 54 | 55 | - title: Sorted Sets 56 | url: effects/sortedsets.html 57 | menu_section: effectapi 58 | 59 | - title: Strings 60 | url: effects/strings.html 61 | menu_section: effectapi 62 | 63 | - title: Streams API 64 | url: streams/ 65 | menu_section: streamapi 66 | 67 | nested_options: 68 | - title: PubSub 69 | url: streams/pubsub.html 70 | menu_section: streamapi 71 | 72 | - title: Streams 73 | url: streams/streams.html 74 | menu_section: streamapi 75 | 76 | - title: Transactions 77 | url: transactions.html 78 | menu_section: transactions 79 | 80 | - title: Pipelining 81 | url: pipelining.html 82 | menu_section: pipelining 83 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/effect/FutureLift.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.effect 18 | 19 | import cats.ApplicativeThrow 20 | import cats.effect.kernel.Async 21 | import cats.effect.kernel.syntax.monadCancel._ 22 | import cats.syntax.all._ 23 | import io.lettuce.core.RedisFuture 24 | 25 | import java.util.concurrent._ 26 | 27 | private[redis4cats] trait FutureLift[F[_]] { 28 | def delay[A](thunk: => A): F[A] 29 | def blocking[A](thunk: => A): F[A] 30 | def guarantee[A](fa: F[A], fu: F[Unit]): F[A] 31 | def lift[A](fa: => FutureLift.JFuture[A]): F[A] 32 | } 33 | 34 | object FutureLift { 35 | private[redis4cats] type JFuture[A] = CompletionStage[A] with Future[A] 36 | 37 | def apply[F[_]: FutureLift]: FutureLift[F] = implicitly 38 | 39 | implicit def forAsync[F[_]: Async]: FutureLift[F] = 40 | new FutureLift[F] { 41 | val F = Async[F] 42 | 43 | def delay[A](thunk: => A): F[A] = F.delay(thunk) 44 | 45 | def blocking[A](thunk: => A): F[A] = F.blocking(thunk) 46 | 47 | def guarantee[A](fa: F[A], fu: F[Unit]): F[A] = fa.guarantee(fu) 48 | 49 | def lift[A](fa: => JFuture[A]): F[A] = 50 | F.fromCompletionStage(F.delay(fa)) 51 | } 52 | 53 | implicit final class FutureLiftOps[F[_]: ApplicativeThrow: FutureLift: Log, A](fa: => RedisFuture[A]) { 54 | def futureLift: F[A] = 55 | FutureLift[F].lift(fa).onError { case e: ExecutionException => 56 | Log[F].error(s"${e.getMessage()} - ${Option(e.getCause())}") 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/geo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.effects._ 20 | import io.lettuce.core.GeoArgs 21 | 22 | // format: off 23 | trait GeoCommands[F[_], K, V] extends GeoGetter[F, K, V] with GeoSetter[F, K, V] 24 | 25 | trait GeoGetter[F[_], K, V] { 26 | def geoDist(key: K, from: V, to: V, unit: GeoArgs.Unit): F[Double] 27 | def geoHash(key: K, value:V, values: V*): F[List[Option[String]]] 28 | def geoPos(key: K, value:V, values: V*): F[List[GeoCoordinate]] 29 | def geoRadius(key: K, geoRadius: GeoRadius, unit: GeoArgs.Unit): F[Set[V]] 30 | def geoRadius(key: K, geoRadius: GeoRadius, unit: GeoArgs.Unit, args: GeoArgs): F[List[GeoRadiusResult[V]]] 31 | def geoRadiusByMember(key: K, value: V, dist: Distance, unit: GeoArgs.Unit): F[Set[V]] 32 | def geoRadiusByMember(key: K, value: V, dist: Distance, unit: GeoArgs.Unit, args: GeoArgs): F[List[GeoRadiusResult[V]]] 33 | } 34 | 35 | trait GeoSetter[F[_], K, V] { 36 | def geoAdd(key: K, geoValues: GeoLocation[V]*): F[Unit] 37 | def geoRadius(key: K, geoRadius: GeoRadius, unit: GeoArgs.Unit, storage: GeoRadiusKeyStorage[K]): F[Unit] 38 | def geoRadius(key: K, geoRadius: GeoRadius, unit: GeoArgs.Unit, storage: GeoRadiusDistStorage[K]): F[Unit] 39 | def geoRadiusByMember(key: K, value: V, dist: Distance, unit: GeoArgs.Unit, storage: GeoRadiusKeyStorage[K]): F[Unit] 40 | def geoRadiusByMember(key: K, value: V, dist: Distance, unit: GeoArgs.Unit, storage: GeoRadiusDistStorage[K]): F[Unit] 41 | } 42 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/lists.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import cats.data.NonEmptyList 20 | 21 | import scala.concurrent.duration.Duration 22 | 23 | trait ListCommands[F[_], K, V] 24 | extends ListBlocking[F, K, V] 25 | with ListGetter[F, K, V] 26 | with ListSetter[F, K, V] 27 | with ListPushPop[F, K, V] 28 | 29 | trait ListBlocking[F[_], K, V] { 30 | def blPop(timeout: Duration, keys: NonEmptyList[K]): F[Option[(K, V)]] 31 | def brPop(timeout: Duration, keys: NonEmptyList[K]): F[Option[(K, V)]] 32 | def brPopLPush(timeout: Duration, source: K, destination: K): F[Option[V]] 33 | } 34 | 35 | trait ListGetter[F[_], K, V] { 36 | def lIndex(key: K, index: Long): F[Option[V]] 37 | def lLen(key: K): F[Long] 38 | def lRange(key: K, start: Long, stop: Long): F[List[V]] 39 | } 40 | 41 | trait ListSetter[F[_], K, V] { 42 | def lInsertAfter(key: K, pivot: V, value: V): F[Long] 43 | def lInsertBefore(key: K, pivot: V, value: V): F[Long] 44 | def lRem(key: K, count: Long, value: V): F[Long] 45 | def lSet(key: K, index: Long, value: V): F[Unit] 46 | def lTrim(key: K, start: Long, stop: Long): F[Unit] 47 | } 48 | 49 | trait ListPushPop[F[_], K, V] { 50 | def lPop(key: K): F[Option[V]] 51 | def lPush(key: K, values: V*): F[Long] 52 | def lPushX(key: K, values: V*): F[Long] 53 | def rPop(key: K): F[Option[V]] 54 | def rPopLPush(source: K, destination: K): F[Option[V]] 55 | def rPush(key: K, values: V*): F[Long] 56 | def rPushX(key: K, values: V*): F[Long] 57 | } 58 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/sets.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.data.ValueScanCursor 20 | import dev.profunktor.redis4cats.effects.ScanArgs 21 | 22 | trait SetCommands[F[_], K, V] extends SetGetter[F, K, V] with SetSetter[F, K, V] with SetDeletion[F, K, V] { 23 | def sIsMember(key: K, value: V): F[Boolean] 24 | def sMisMember(key: K, values: V*): F[List[Boolean]] 25 | } 26 | 27 | trait SetGetter[F[_], K, V] { 28 | def sCard(key: K): F[Long] 29 | def sDiff(keys: K*): F[Set[V]] 30 | def sInter(keys: K*): F[Set[V]] 31 | def sMembers(key: K): F[Set[V]] 32 | def sRandMember(key: K): F[Option[V]] 33 | def sRandMember(key: K, count: Long): F[List[V]] 34 | def sUnion(keys: K*): F[Set[V]] 35 | def sUnionStore(destination: K, keys: K*): F[Unit] 36 | def sScan(key: K): F[ValueScanCursor[V]] 37 | def sScan(key: K, cursor: ValueScanCursor[V]): F[ValueScanCursor[V]] 38 | def sScan(key: K, scanArgs: ScanArgs): F[ValueScanCursor[V]] 39 | def sScan(key: K, cursor: ValueScanCursor[V], scanArgs: ScanArgs): F[ValueScanCursor[V]] 40 | } 41 | 42 | trait SetSetter[F[_], K, V] { 43 | def sAdd(key: K, values: V*): F[Long] 44 | def sDiffStore(destination: K, keys: K*): F[Long] 45 | def sInterStore(destination: K, keys: K*): F[Long] 46 | def sMove(source: K, destination: K, value: V): F[Boolean] 47 | } 48 | 49 | trait SetDeletion[F[_], K, V] { 50 | def sPop(key: K): F[Option[V]] 51 | def sPop(key: K, count: Long): F[Set[V]] 52 | def sRem(key: K, values: V*): F[Long] 53 | } 54 | -------------------------------------------------------------------------------- /project/Dependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object Dependencies { 4 | 5 | object V { 6 | val cats = "2.13.0" 7 | val catsEffect = "3.6.3" 8 | val circe = "0.14.15" 9 | val fs2 = "3.12.2" 10 | val log4cats = "2.7.1" 11 | val keyPool = "0.4.10" 12 | 13 | val lettuce = "7.2.1.RELEASE" 14 | 15 | val logback = "1.5.22" 16 | 17 | val kindProjector = "0.13.4" 18 | 19 | val munit = "1.2.1" 20 | val munitScalacheck = "1.2.0" 21 | 22 | } 23 | 24 | object Libraries { 25 | def cats(artifact: String): ModuleID = "org.typelevel" %% s"cats-$artifact" % V.cats 26 | def log4cats(artifact: String): ModuleID = "org.typelevel" %% s"log4cats-$artifact" % V.log4cats 27 | 28 | val catsEffectKernel = "org.typelevel" %% "cats-effect-kernel" % V.catsEffect 29 | val fs2Core = "co.fs2" %% "fs2-core" % V.fs2 30 | val keyPool = "org.typelevel" %% "keypool" % V.keyPool 31 | 32 | val log4CatsCore = log4cats("core") 33 | 34 | val redisClient = "io.lettuce" % "lettuce-core" % V.lettuce 35 | 36 | val literally = "org.typelevel" %% "literally" % "1.2.0" 37 | 38 | def reflect(version: String): ModuleID = "org.scala-lang" % "scala-reflect" % version 39 | 40 | // Examples libraries 41 | val catsEffect = "org.typelevel" %% "cats-effect" % V.catsEffect 42 | val circeCore = "io.circe" %% "circe-core" % V.circe 43 | val circeGeneric = "io.circe" %% "circe-generic" % V.circe 44 | val circeParser = "io.circe" %% "circe-parser" % V.circe 45 | val log4CatsSlf4j = log4cats("slf4j") 46 | val logback = "ch.qos.logback" % "logback-classic" % V.logback 47 | 48 | // Testing libraries 49 | val catsLaws = cats("core") 50 | val catsTestKit = cats("testkit") 51 | val munitCore = "org.scalameta" %% "munit" % V.munit 52 | val munitScalacheck = "org.scalameta" %% "munit-scalacheck" % V.munitScalacheck 53 | } 54 | 55 | object CompilerPlugins { 56 | val kindProjector = compilerPlugin( 57 | "org.typelevel" % "kind-projector" % V.kindProjector cross CrossVersion.full 58 | ) 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPipelineDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect._ 20 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 21 | import dev.profunktor.redis4cats.tx._ 22 | 23 | object RedisPipelineDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val key1 = "testp1" 29 | val key2 = "testp2" 30 | 31 | val showResult: String => Option[String] => IO[Unit] = key => 32 | _.fold(IO.println(s"Not found key: $key"))(s => IO.println(s"$key: $s")) 33 | 34 | val commandsApi: Resource[IO, RedisCommands[IO, String, String]] = 35 | Redis[IO].utf8(redisURI) 36 | 37 | commandsApi.use { redis => 38 | val getters = 39 | redis.get(key1).flatTap(showResult(key1)) *> 40 | redis.get(key2).flatTap(showResult(key2)) 41 | 42 | val ops = (store: TxStore[IO, String, Option[String]]) => 43 | List( 44 | redis.set(key1, "noop"), 45 | redis.set(key2, "windows"), 46 | redis.get(key1).flatMap(store.set(s"$key1-v1")), 47 | redis.set(key1, "nix"), 48 | redis.set(key2, "linux"), 49 | redis.get(key1).flatMap(store.set(s"$key1-v2")) 50 | ) 51 | 52 | val prog = 53 | redis 54 | .pipeline(ops) 55 | .flatMap(kv => IO.println(s"KV: $kv")) 56 | .recoverWith { case e => 57 | IO.println(s"[Error] - ${e.getMessage}") 58 | } 59 | 60 | getters >> prog >> getters >> IO.println("keep doing stuff...") 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisClusterSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import dev.profunktor.redis4cats.data.RedisCodec 20 | 21 | class RedisClusterSpec extends Redis4CatsFunSuite(true) with TestScenarios { 22 | 23 | test("cluster: keys api")(withRedisCluster(cmd => keysScenario(cmd) >> clusterScanScenario(cmd))) 24 | 25 | test("cluster: geo api")(withRedisCluster(locationScenario)) 26 | 27 | test("cluster: hashes api")(withRedisCluster(hashesScenario)) 28 | 29 | test("cluster: lists api")(withRedisCluster(listsScenario)) 30 | 31 | test("cluster: sets api")(withRedisCluster(setsScenario)) 32 | 33 | test("cluster: sorted sets api")( 34 | withAbstractRedisCluster[Unit, String, Long](sortedSetsScenario)(RedisCodec(LongCodec)) 35 | ) 36 | 37 | test("cluster: strings api")(withRedisCluster(stringsClusterScenario)) 38 | 39 | test("cluster: connection api")(withRedisCluster(connectionScenario)) 40 | 41 | test("cluster: server api")(withRedisCluster(serverScenario)) 42 | 43 | test("cluster: scripts")(withRedis(scriptsScenario)) 44 | 45 | test("cluster: scripts lua extensions")(withRedis(scriptingLuaExtensionsScenario)) 46 | 47 | test("cluster: functions")(withRedis(functionsScenario)) 48 | 49 | test("cluster: hyperloglog api")(withRedis(hyperloglogScenario)) 50 | 51 | test("cluster: streams api")(withRedisCluster(streamsScenario)) 52 | 53 | // FIXME: The Cluster impl cannot connect to a single node just yet 54 | // test("cluster: pipelining")(withRedisCluster(pipelineScenario)) 55 | // test("cluster: transactions")(withRedisCluster(transactionScenario)) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Welcome to redis4cats! 2 | 3 | We follow the standard GitHub [fork & pull](https://help.github.com/articles/using-pull-requests/#fork--pull) approach to pull requests. Just fork the official repo, develop in a branch, and submit a PR! 4 | 5 | You're always welcome to submit your PR straight away and start the discussion (without reading the rest of this wonderful doc, or the [`README.md`](README.md)). The goal of these notes is to make your experience contributing to redis4cats as smooth and pleasant as possible. We're happy to guide you through the process once you've submitted your PR. 6 | 7 | ## Ticket Guidelines 8 | 9 | - **Bugs**: 10 | - Contain steps to reproduce. 11 | - Contain a code sample that exhibits the error. 12 | - Inform us if the issue is blocking you with no visible workaround 13 | - This will give the ticket priority 14 | - **Features** 15 | - Show a code example of how that feature would be used. 16 | 17 | ## Contribution Guidelines 18 | 19 | - **All code PRs should**: 20 | - have a meaningful commit message description 21 | - comment important things 22 | - include unit tests (positive and negative) 23 | - pass [CI](https://app.codeship.com/projects/223399), which automatically runs when your pull request is submitted. 24 | - **Be prepared to discuss/argue-for your changes if you want them merged**! 25 | You will probably need to refactor so your changes fit into the larger 26 | codebase 27 | - **If your code is hard to unit test, and you don't want to unit test it, 28 | that's ok**. But be prepared to argue why that's the case! 29 | - **It's entirely possible your changes won't be merged**, or will get ripped 30 | out later. This is also the case for maintainer changes! 31 | - **Even a rejected/reverted PR is valuable**! It helps explore the solution 32 | space, and know what works and what doesn't. For every line in the repo, at 33 | least three lines were tried, committed, and reverted/refactored, and more 34 | than 10 were tried without committing. 35 | - **Feel free to send Proof-Of-Concept PRs** that you don't intend to get merged. 36 | 37 | In case of any questions, don't hesitate to ask on our [gitter channel](https://gitter.im/profunktor-dev/redis4cats). 38 | 39 | (these guidelines have been adapted from https://github.com/scalameta/scalameta/blob/master/CONTRIBUTING.md) 40 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/JsonCodecDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.codecs.Codecs 21 | import dev.profunktor.redis4cats.codecs.splits.SplitEpi 22 | import dev.profunktor.redis4cats.data.RedisCodec 23 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 24 | import io.circe.generic.auto._ 25 | import io.circe.parser.{ decode => jsonDecode } 26 | import io.circe.syntax._ 27 | 28 | object JsonCodecDemo extends LoggerIOApp { 29 | 30 | import Demo._ 31 | 32 | sealed trait Event 33 | 34 | object Event { 35 | case class Ack(id: Long) extends Event 36 | case class Message(id: Long, payload: String) extends Event 37 | case object Unknown extends Event 38 | } 39 | 40 | val program: IO[Unit] = { 41 | val eventsKey = "events" 42 | 43 | val eventSplitEpi: SplitEpi[String, Event] = 44 | SplitEpi[String, Event]( 45 | str => jsonDecode[Event](str).getOrElse(Event.Unknown), 46 | _.asJson.noSpaces 47 | ) 48 | 49 | val eventsCodec: RedisCodec[String, Event] = 50 | Codecs.derive(RedisCodec.Utf8, eventSplitEpi) 51 | 52 | val commandsApi: Resource[IO, RedisCommands[IO, String, Event]] = 53 | Redis[IO].simple(redisURI, eventsCodec) 54 | 55 | commandsApi.use { redis => 56 | for { 57 | x <- redis.sCard(eventsKey) 58 | _ <- IO.println(s"Number of events: $x") 59 | _ <- redis.sAdd(eventsKey, Event.Ack(1), Event.Message(23, "foo")) 60 | y <- redis.sMembers(eventsKey) 61 | _ <- IO.println(s"Events: $y") 62 | } yield () 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /site/docs/effects/geo.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Geo" 4 | number: 4 5 | --- 6 | 7 | # Geo API 8 | 9 | Purely functional interface for the [Geo API](https://redis.io/commands#geo). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.GeoCommands 16 | import dev.profunktor.redis4cats.data._ 17 | import dev.profunktor.redis4cats.log4cats._ 18 | import org.typelevel.log4cats.Logger 19 | import org.typelevel.log4cats.slf4j.Slf4jLogger 20 | 21 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 22 | 23 | val commandsApi: Resource[IO, GeoCommands[IO, String, String]] = { 24 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[GeoCommands[IO, String, String]] 25 | } 26 | ``` 27 | 28 | ### Geo Commands usage 29 | 30 | Once you have acquired a connection you can start using it: 31 | 32 | ```scala mdoc:silent 33 | import cats.effect.IO 34 | import dev.profunktor.redis4cats.effects._ 35 | import io.lettuce.core.GeoArgs 36 | 37 | val testKey = "location" 38 | 39 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 40 | 41 | val _BuenosAires = GeoLocation(Longitude(-58.3816), Latitude(-34.6037), "Buenos Aires") 42 | val _RioDeJaneiro = GeoLocation(Longitude(-43.1729), Latitude(-22.9068), "Rio de Janeiro") 43 | val _Montevideo = GeoLocation(Longitude(-56.164532), Latitude(-34.901112), "Montevideo") 44 | val _Tokyo = GeoLocation(Longitude(139.6917), Latitude(35.6895), "Tokyo") 45 | 46 | commandsApi.use { redis => // GeoCommands[IO, String, String] 47 | for { 48 | _ <- redis.geoAdd(testKey, _BuenosAires) 49 | _ <- redis.geoAdd(testKey, _RioDeJaneiro) 50 | _ <- redis.geoAdd(testKey, _Montevideo) 51 | _ <- redis.geoAdd(testKey, _Tokyo) 52 | x <- redis.geoDist(testKey, _BuenosAires.value, _Tokyo.value, GeoArgs.Unit.km) 53 | _ <- putStrLn(s"Distance from ${_BuenosAires.value} to Tokyo: $x km") 54 | y <- redis.geoPos(testKey, _RioDeJaneiro.value) 55 | _ <- putStrLn(s"Geo Pos of ${_RioDeJaneiro.value}: ${y.headOption}") 56 | z <- redis.geoRadius(testKey, GeoRadius(_Montevideo.lon, _Montevideo.lat, Distance(10000.0)), GeoArgs.Unit.km) 57 | _ <- putStrLn(s"Geo Radius in 1000 km: $z") 58 | } yield () 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /site/docs/effects/bitmaps.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Bitmaps" 4 | number: 14 5 | --- 6 | 7 | # Bitmaps API 8 | 9 | Purely functional interface for the [Bitmaps API](https://redis.io/commands#bitmap). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.BitCommands 16 | import dev.profunktor.redis4cats.algebra.BitCommandOperation.{ IncrUnsignedBy, SetUnsigned } 17 | import dev.profunktor.redis4cats.data._ 18 | import dev.profunktor.redis4cats.log4cats._ 19 | import org.typelevel.log4cats.Logger 20 | import org.typelevel.log4cats.slf4j.Slf4jLogger 21 | 22 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 23 | 24 | val commandsApi: Resource[IO, BitCommands[IO, String, String]] = { 25 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[BitCommands[IO, String, String]] 26 | } 27 | ``` 28 | 29 | ### List Commands usage 30 | 31 | Once you have acquired a connection you can start using it: 32 | 33 | ```scala mdoc:silent 34 | import cats.effect.IO 35 | 36 | val testKey = "foo" 37 | val testKey2 = "bar" 38 | val testKey3 = "baz" 39 | 40 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 41 | 42 | commandsApi.use { cmd => // BitCommands[IO, String, String] 43 | for { 44 | a <- cmd.setBit(testKey, 7, 1) 45 | _ <- cmd.setBit(testKey2, 7, 0) 46 | _ <- putStrLn(s"Set as $a") 47 | b <- cmd.getBit(testKey, 6) 48 | _ <- putStrLn(s"Bit at offset 6 is $b") 49 | _ <- cmd.bitOpOr(testKey3, testKey, testKey2) 50 | _ <- for { 51 | s1 <- cmd.setBit("bitmapsarestrings", 2, 1) 52 | s2 <- cmd.setBit("bitmapsarestrings", 3, 1) 53 | s3 <- cmd.setBit("bitmapsarestrings", 5, 1) 54 | s4 <- cmd.setBit("bitmapsarestrings", 10, 1) 55 | s5 <- cmd.setBit("bitmapsarestrings", 11, 1) 56 | s6 <- cmd.setBit("bitmapsarestrings", 14, 1) 57 | } yield s1 + s2 + s3 + s4 + s5 + s6 58 | bf <- cmd.bitField( 59 | "inmap", 60 | SetUnsigned(2, 1), 61 | SetUnsigned(3, 1), 62 | SetUnsigned(5, 1), 63 | SetUnsigned(10, 1), 64 | SetUnsigned(11, 1), 65 | SetUnsigned(14, 1), 66 | IncrUnsignedBy(14, 1) 67 | ) 68 | _ <- putStrLn(s"Via bitfield $bf") 69 | } yield () 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisGeoDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.GeoCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | import dev.profunktor.redis4cats.effects._ 23 | import io.lettuce.core.GeoArgs 24 | 25 | object RedisGeoDemo extends LoggerIOApp { 26 | 27 | import Demo._ 28 | 29 | val program: IO[Unit] = { 30 | val testKey = "location" 31 | 32 | val commandsApi: Resource[IO, GeoCommands[IO, String, String]] = 33 | Redis[IO].utf8(redisURI) 34 | 35 | val _BuenosAires = GeoLocation(Longitude(-58.3816), Latitude(-34.6037), "Buenos Aires") 36 | val _RioDeJaneiro = GeoLocation(Longitude(-43.1729), Latitude(-22.9068), "Rio de Janeiro") 37 | val _Montevideo = GeoLocation(Longitude(-56.164532), Latitude(-34.901112), "Montevideo") 38 | val _Tokyo = GeoLocation(Longitude(139.6917), Latitude(35.6895), "Tokyo") 39 | 40 | commandsApi.use { redis => 41 | for { 42 | _ <- redis.geoAdd(testKey, _BuenosAires) 43 | _ <- redis.geoAdd(testKey, _RioDeJaneiro) 44 | _ <- redis.geoAdd(testKey, _Montevideo) 45 | _ <- redis.geoAdd(testKey, _Tokyo) 46 | x <- redis.geoDist(testKey, _BuenosAires.value, _Tokyo.value, GeoArgs.Unit.km) 47 | _ <- IO.println(s"Distance from ${_BuenosAires.value} to Tokyo: $x km") 48 | y <- redis.geoPos(testKey, _RioDeJaneiro.value) 49 | _ <- IO.println(s"Geo Pos of ${_RioDeJaneiro.value}: ${y.headOption}") 50 | z <- redis.geoRadius(testKey, GeoRadius(_Montevideo.lon, _Montevideo.lat, Distance(10000.0)), GeoArgs.Unit.km) 51 | _ <- IO.println(s"Geo Radius in 1000 km: $z") 52 | } yield () 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /modules/core/src/test/scala/dev/profunktor/redis4cats/effect/FutureLiftSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.effect 18 | 19 | import java.util.concurrent.{ CancellationException, CompletableFuture } 20 | import cats.effect.IO 21 | import cats.effect.unsafe.IORuntime 22 | import munit.FunSuite 23 | 24 | class FutureLiftSuite extends FunSuite { 25 | implicit val ioRuntime: IORuntime = cats.effect.unsafe.IORuntime.global 26 | 27 | val currentThread: IO[String] = IO(Thread.currentThread().getName) 28 | 29 | val instance = FutureLift[IO] 30 | 31 | test("it shifts back once the Future is converted") { 32 | val ioa = 33 | instance.lift[String] { 34 | val jFuture = new CompletableFuture[String]() 35 | jFuture.complete("foo") 36 | jFuture 37 | } 38 | 39 | (ioa *> currentThread) 40 | .flatMap(t => IO(assert(t.contains("io-compute")))) 41 | .unsafeToFuture() 42 | } 43 | 44 | test("it shifts back even when the CompletableFuture fails") { 45 | val ioa = 46 | instance.lift[String] { 47 | val jFuture = new CompletableFuture[String]() 48 | jFuture.completeExceptionally(new RuntimeException("Purposely fail")) 49 | jFuture 50 | } 51 | 52 | (ioa.attempt *> currentThread) 53 | .flatMap(t => IO(assert(t.contains("io-compute")))) 54 | .unsafeToFuture() 55 | } 56 | 57 | test("it fails with CancellationException") { 58 | val e = new CancellationException("purposeful") 59 | val ioa = 60 | instance.lift[String] { 61 | val jFuture = new CompletableFuture[String]() 62 | jFuture.completeExceptionally(e) 63 | jFuture 64 | } 65 | ioa.attempt 66 | .flatMap(att => IO(assertEquals(att, Left[Throwable, String](e)))) 67 | .unsafeToFuture() 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisListsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.ListCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisListsDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val testKey = "listos" 29 | 30 | val commandsApi: Resource[IO, ListCommands[IO, String, String]] = 31 | Redis[IO].utf8(redisURI) 32 | 33 | commandsApi.use { redis => 34 | for { 35 | d <- redis.rPush(testKey, "one", "two", "three") 36 | _ <- IO.println(s"Length on Push: $d") 37 | x <- redis.lRange(testKey, 0, 10) 38 | _ <- IO.println(s"Range: $x") 39 | y <- redis.lLen(testKey) 40 | _ <- IO.println(s"Length: $y") 41 | a <- redis.lPop(testKey) 42 | _ <- IO.println(s"Left Pop: $a") 43 | b <- redis.rPop(testKey) 44 | _ <- IO.println(s"Right Pop: $b") 45 | z <- redis.lRange(testKey, 0, 10) 46 | _ <- IO.println(s"Range: $z") 47 | c <- redis.lInsertAfter(testKey, "two", "four") 48 | _ <- IO.println(s"Length on Insert After: $c") 49 | e <- redis.lInsertBefore(testKey, "four", "three") 50 | _ <- IO.println(s"Length on Insert Before: $e") 51 | f <- redis.lRange(testKey, 0, 10) 52 | _ <- IO.println(s"Range: $f") 53 | _ <- redis.lSet(testKey, 0, "four") 54 | g <- redis.lRange(testKey, 0, 10) 55 | _ <- IO.println(s"Range after Set: $g") 56 | h <- redis.lRem(testKey, 2, "four") 57 | _ <- IO.println(s"Removed: $h") 58 | i <- redis.lRange(testKey, 0, 10) 59 | _ <- IO.println(s"Range: $i") 60 | } yield () 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/effect/Log.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.effect 18 | 19 | import cats.Applicative 20 | import cats.effect.kernel.Sync 21 | 22 | /** Typeclass used for internal logging such as acquiring and releasing connections. 23 | * 24 | * It is recommended to use `log4cats` for production usage but if you do not want the extra dependency, you can opt to 25 | * use either of the simple instances provided. 26 | * 27 | * If you don't need logging at all, you can use [[Log.NoOp]] 28 | * 29 | * {{{ 30 | * import dev.profunktor.redis4cats.effect.Log.NoOp._ 31 | * }}} 32 | * 33 | * If you need simple logging to STDOUT for quick debugging, you can use [[Log.Stdout]] 34 | * 35 | * {{{ 36 | * import dev.profunktor.redis4cats.effect.Log.Stdout._ 37 | * }}} 38 | */ 39 | trait Log[F[_]] { 40 | def debug(msg: => String): F[Unit] 41 | def error(msg: => String): F[Unit] 42 | def info(msg: => String): F[Unit] 43 | } 44 | 45 | object Log { 46 | def apply[F[_]]( 47 | implicit ev: Log[F] 48 | ): Log[F] = ev 49 | 50 | object NoOp { 51 | implicit def instance[F[_]: Applicative]: Log[F] = 52 | new Log[F] { 53 | def debug(msg: => String): F[Unit] = Applicative[F].unit 54 | def error(msg: => String): F[Unit] = Applicative[F].unit 55 | def info(msg: => String): F[Unit] = Applicative[F].unit 56 | } 57 | } 58 | 59 | object Stdout { 60 | implicit def instance[F[_]: Sync]: Log[F] = 61 | new Log[F] { 62 | def debug(msg: => String): F[Unit] = 63 | Sync[F].delay(Console.out.println(msg)) 64 | def error(msg: => String): F[Unit] = 65 | Sync[F].delay(Console.err.println(msg)) 66 | def info(msg: => String): F[Unit] = 67 | Sync[F].delay(Console.out.println(msg)) 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/tx/TxRunner.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.tx 18 | 19 | import cats.effect.kernel._ 20 | import cats.effect.kernel.syntax.all._ 21 | import cats.syntax.all._ 22 | 23 | import dev.profunktor.redis4cats.effect.TxExecutor 24 | 25 | private[redis4cats] trait TxRunner[F[_]] { 26 | def run[A]( 27 | acquire: F[Unit], 28 | release: F[Unit], 29 | onError: F[Unit] 30 | )( 31 | fs: TxStore[F, String, A] => List[F[Unit]] 32 | ): F[Map[String, A]] 33 | def liftK[G[_]: Async]: TxRunner[G] 34 | } 35 | 36 | private[redis4cats] object TxRunner { 37 | private[redis4cats] def make[F[_]: Async](t: TxExecutor[F]): TxRunner[F] = 38 | new TxRunner[F] { 39 | def run[A]( 40 | acquire: F[Unit], 41 | release: F[Unit], 42 | onError: F[Unit] 43 | )( 44 | fs: TxStore[F, String, A] => List[F[Unit]] 45 | ): F[Map[String, A]] = 46 | TxStore.make[F, String, A].flatMap { store => 47 | (Deferred[F, Unit], Ref.of[F, List[Fiber[F, Throwable, Unit]]](List.empty)).tupled.flatMap { 48 | case (gate, fbs) => 49 | t.eval(acquire) 50 | .bracketCase { _ => 51 | fs(store) 52 | .traverse_(f => t.start(f).flatMap(fb => fbs.update(_ :+ fb))) 53 | .guarantee(gate.complete(()).void) 54 | } { 55 | case (_, Outcome.Succeeded(_)) => 56 | gate.get *> t.eval(release).guarantee(fbs.get.flatMap(_.traverse_(_.join))) 57 | case (_, _) => 58 | t.eval(onError).guarantee(fbs.get.flatMap(_.traverse_(_.cancel))) 59 | } 60 | } *> store.get 61 | } 62 | 63 | def liftK[G[_]: Async]: TxRunner[G] = make[G](t.liftK[G]) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisClusterFromUnderlyingDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import java.time.Duration 20 | 21 | import cats.effect.{ IO, Resource } 22 | import dev.profunktor.redis4cats.connection.{ RedisClusterClient, RedisURI } 23 | import dev.profunktor.redis4cats.effect.FutureLift 24 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 25 | import io.lettuce.core.TimeoutOptions 26 | import io.lettuce.core.cluster.{ ClusterClientOptions, RedisClusterClient => JRedisClusterClient } 27 | 28 | object RedisClusterFromUnderlyingDemo extends LoggerIOApp { 29 | 30 | import Demo._ 31 | 32 | def makeRedisCluster(uri: RedisURI) = 33 | Resource.make(IO { 34 | val timeoutOptions = 35 | TimeoutOptions 36 | .builder() 37 | .fixedTimeout(Duration.ofMillis(500L)) 38 | .build() 39 | val clusterOptions = 40 | ClusterClientOptions 41 | .builder() 42 | .pingBeforeActivateConnection(true) 43 | .autoReconnect(true) 44 | .validateClusterNodeMembership(true) 45 | .timeoutOptions(timeoutOptions) 46 | .build() 47 | 48 | val client = JRedisClusterClient.create(uri.underlying) 49 | client.setOptions(clusterOptions) 50 | client 51 | })(client => FutureLift[IO].lift(client.shutdownAsync()).void) 52 | 53 | val program: IO[Unit] = { 54 | val usernameKey = "test" 55 | 56 | val commandsApi = 57 | for { 58 | uri <- Resource.eval(RedisURI.make[IO](redisClusterURI)) 59 | underlying <- makeRedisCluster(uri) 60 | client = RedisClusterClient.fromUnderlying(underlying) 61 | redis <- Redis[IO].fromClusterClient(client, stringCodec)() 62 | } yield redis 63 | 64 | commandsApi.use(_.get(usernameKey).flatMap(IO.println)) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisMasterReplicaStringsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import cats.syntax.all._ 21 | import dev.profunktor.redis4cats.connection._ 22 | import dev.profunktor.redis4cats.data.ReadFrom 23 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 24 | 25 | object RedisMasterReplicaStringsDemo extends LoggerIOApp { 26 | 27 | import Demo._ 28 | 29 | val program: IO[Unit] = { 30 | val usernameKey = "test" 31 | 32 | val showResult: Option[String] => IO[Unit] = 33 | _.fold(IO.println(s"Not found key: $usernameKey"))(s => IO.println(s)) 34 | 35 | val masterUri: String = "redis://localhost" 36 | val replicaUri: String = "redis://localhost:6380" 37 | 38 | val connection: Resource[IO, RedisCommands[IO, String, String]] = 39 | for { 40 | uri1 <- Resource.eval(RedisURI.make[IO](masterUri)) 41 | uri2 <- Resource.eval(RedisURI.make[IO](replicaUri)) 42 | conn <- RedisMasterReplica[IO].make(stringCodec, uri1, uri2)(Some(ReadFrom.Replica)) 43 | redis <- Redis[IO].masterReplica(conn) 44 | } yield redis 45 | 46 | connection.use { redis => 47 | for { 48 | i <- redis.info("replication") 49 | _ <- IO.println("SERVER INFO\n") 50 | _ <- i.toList.traverse_(kv => IO.println(kv)) 51 | _ <- IO.println("----------\n") 52 | x <- redis.get(usernameKey) 53 | _ <- showResult(x) 54 | _ <- redis.set(usernameKey, "some value") 55 | y <- redis.get(usernameKey) 56 | _ <- showResult(y) 57 | _ <- redis.setNx(usernameKey, "should not happen") 58 | w <- redis.get(usernameKey) 59 | _ <- showResult(w) 60 | _ <- redis.del(usernameKey) 61 | z <- redis.get(usernameKey) 62 | _ <- showResult(z) 63 | } yield () 64 | } 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/Publisher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | package pubsub 19 | package internals 20 | 21 | import cats.FlatMap 22 | import cats.syntax.functor._ 23 | import dev.profunktor.redis4cats.data.RedisChannel 24 | import dev.profunktor.redis4cats.effect.FutureLift 25 | import dev.profunktor.redis4cats.pubsub.data.Subscription 26 | import fs2.Stream 27 | import io.lettuce.core.pubsub.StatefulRedisPubSubConnection 28 | 29 | private[pubsub] class Publisher[F[_]: FlatMap: FutureLift, K, V]( 30 | pubConnection: StatefulRedisPubSubConnection[K, V] 31 | ) extends PublishCommands[F, Stream[F, *], K, V] { 32 | 33 | private[redis4cats] val pubSubStats: PubSubStats[F, K] = new LivePubSubStats(pubConnection) 34 | 35 | override def publish(channel: RedisChannel[K]): Stream[F, V] => Stream[F, Long] = 36 | _.evalMap(publish(channel, _)) 37 | 38 | override def publish(channel: RedisChannel[K], message: V): F[Long] = 39 | FutureLift[F].lift(pubConnection.async().publish(channel.underlying, message)).map(l => l: Long) 40 | 41 | override def pubSubChannels: F[List[RedisChannel[K]]] = 42 | pubSubStats.pubSubChannels 43 | 44 | override def pubSubSubscriptions(channel: RedisChannel[K]): F[Option[Subscription[K]]] = 45 | pubSubStats.pubSubSubscriptions(channel) 46 | 47 | override def pubSubSubscriptions(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 48 | pubSubStats.pubSubSubscriptions(channels) 49 | 50 | override def numPat: F[Long] = 51 | pubSubStats.numPat 52 | 53 | override def numSub: F[List[Subscription[K]]] = 54 | pubSubStats.numSub 55 | 56 | override def pubSubShardChannels: F[List[RedisChannel[K]]] = 57 | pubSubStats.pubSubShardChannels 58 | 59 | override def shardNumSub(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 60 | pubSubStats.shardNumSub(channels) 61 | } 62 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/StreamingDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.IO 20 | import cats.syntax.parallel._ 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | import dev.profunktor.redis4cats.effects.XReadOffsets 23 | import dev.profunktor.redis4cats.streams.RedisStream 24 | import dev.profunktor.redis4cats.streams.data.XAddMessage 25 | import fs2.Stream 26 | 27 | import scala.concurrent.duration._ 28 | import scala.util.Random 29 | 30 | object StreamingDemo extends LoggerIOApp { 31 | 32 | import Demo._ 33 | 34 | private val streamKey1 = "demo" 35 | private val streamKey2 = "users" 36 | 37 | def randomMessage: Stream[IO, XAddMessage[String, String]] = Stream.evals { 38 | val rndKey = IO(Random.nextInt(1000).toString) 39 | val rndValue = IO(Random.nextString(10)) 40 | (rndKey, rndValue).parMapN { case (k, v) => 41 | List( 42 | XAddMessage(streamKey1, Map(k -> v)), 43 | XAddMessage(streamKey2, Map(k -> v)) 44 | ) 45 | } 46 | } 47 | 48 | private val readStream: Stream[IO, Unit] = 49 | for { 50 | redis <- Stream.resource(Redis[IO].simple(redisURI, stringCodec)) 51 | streaming = RedisStream[IO, String, String](redis) 52 | message <- streaming.read(XReadOffsets.all(streamKey1, streamKey2)) 53 | _ <- Stream.eval(IO.println(message)) 54 | } yield () 55 | 56 | private val writeStream: Stream[IO, Unit] = 57 | for { 58 | redis <- Stream.resource(Redis[IO].simple(redisURI, stringCodec)) 59 | streaming = RedisStream[IO, String, String](redis) 60 | _ <- Stream.awakeEvery[IO](2.seconds) 61 | _ <- randomMessage.through(streaming.append) 62 | } yield () 63 | 64 | val program: IO[Unit] = 65 | readStream 66 | .concurrently(writeStream) 67 | .interruptAfter(5.seconds) 68 | .compile 69 | .drain 70 | 71 | } 72 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/bitmaps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.algebra.BitCommandOperation.Overflows.Overflows 20 | import io.lettuce.core.BitFieldArgs.BitFieldType 21 | 22 | sealed trait BitCommandOperation 23 | 24 | object BitCommandOperation { 25 | final case class Get(bitFieldType: BitFieldType, offset: Int) extends BitCommandOperation 26 | 27 | final case class SetSigned(offset: Int, value: Long, bits: Int = 1) extends BitCommandOperation 28 | 29 | final case class SetUnsigned(offset: Int, value: Long, bits: Int = 1) extends BitCommandOperation 30 | 31 | final case class IncrSignedBy(offset: Int, increment: Long, bits: Int = 1) extends BitCommandOperation 32 | 33 | final case class IncrUnsignedBy(offset: Int, increment: Long, bits: Int = 1) extends BitCommandOperation 34 | 35 | final case class Overflow(overflow: Overflows) extends BitCommandOperation 36 | 37 | object Overflows extends Enumeration { 38 | type Overflows = Value 39 | val WRAP, SAT, FAIL = Value 40 | } 41 | } 42 | 43 | trait BitCommands[F[_], K, V] { 44 | def bitCount(key: K): F[Long] 45 | 46 | def bitCount(key: K, start: Long, end: Long): F[Long] 47 | 48 | def bitField(key: K, operations: BitCommandOperation*): F[List[Long]] 49 | 50 | def bitOpAnd(destination: K, source: K, sources: K*): F[Unit] 51 | 52 | def bitOpNot(destination: K, source: K): F[Unit] 53 | 54 | def bitOpOr(destination: K, source: K, sources: K*): F[Unit] 55 | 56 | def bitOpXor(destination: K, source: K, sources: K*): F[Unit] 57 | 58 | def bitPos(key: K, state: Boolean): F[Long] 59 | 60 | def bitPos(key: K, state: Boolean, start: Long): F[Long] 61 | 62 | def bitPos(key: K, state: Boolean, start: Long, end: Long): F[Long] 63 | 64 | def getBit(key: K, offset: Long): F[Option[Long]] 65 | 66 | def setBit(key: K, offset: Long, value: Int): F[Long] 67 | } 68 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/PubSubInternals.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.pubsub.internals 18 | 19 | import scala.util.control.NoStackTrace 20 | import cats.effect.std.Dispatcher 21 | import dev.profunktor.redis4cats.data.RedisChannel 22 | import dev.profunktor.redis4cats.data.RedisPattern 23 | import dev.profunktor.redis4cats.data.RedisPatternEvent 24 | import io.lettuce.core.pubsub.RedisPubSubListener 25 | import io.lettuce.core.pubsub.RedisPubSubAdapter 26 | 27 | object PubSubInternals { 28 | case class DispatcherAlreadyShutdown() extends NoStackTrace 29 | 30 | private[redis4cats] def channelListener[F[_], K, V]( 31 | channel: RedisChannel[K], 32 | publish: V => F[Unit], 33 | dispatcher: Dispatcher[F] 34 | ): RedisPubSubListener[K, V] = 35 | new RedisPubSubAdapter[K, V] { 36 | override def message(ch: K, msg: V): Unit = 37 | if (ch == channel.underlying) { 38 | try 39 | dispatcher.unsafeRunSync(publish(msg)) 40 | catch { 41 | case _: IllegalStateException => throw DispatcherAlreadyShutdown() 42 | } 43 | } 44 | 45 | // Do not uncomment this, as if you will do this the channel listener will get a message twice 46 | // override def message(pattern: K, channel: K, message: V): Unit = {} 47 | } 48 | private[redis4cats] def patternListener[F[_], K, V]( 49 | redisPattern: RedisPattern[K], 50 | publish: RedisPatternEvent[K, V] => F[Unit], 51 | dispatcher: Dispatcher[F] 52 | ): RedisPubSubListener[K, V] = 53 | new RedisPubSubAdapter[K, V] { 54 | override def message(pattern: K, channel: K, message: V): Unit = 55 | if (pattern == redisPattern.underlying) { 56 | try 57 | dispatcher.unsafeRunSync(publish(RedisPatternEvent(pattern, channel, message))) 58 | catch { 59 | case _: IllegalStateException => throw DispatcherAlreadyShutdown() 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/codecs/Codecs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.codecs 18 | 19 | import dev.profunktor.redis4cats.codecs.splits.SplitEpi 20 | import dev.profunktor.redis4cats.data.RedisCodec 21 | import io.lettuce.core.codec.{ RedisCodec => JRedisCodec } 22 | import java.nio.ByteBuffer 23 | 24 | object Codecs { 25 | 26 | /** Given a base RedisCodec[K, V1] and a split epimorphism between V1 and V2, a new RedisCodec[K, V2] can be derived. 27 | */ 28 | def derive[K, V1, V2]( 29 | baseCodec: RedisCodec[K, V1], 30 | epi: SplitEpi[V1, V2] 31 | ): RedisCodec[K, V2] = { 32 | val codec = baseCodec.underlying 33 | RedisCodec( 34 | new JRedisCodec[K, V2] { 35 | override def decodeKey(bytes: ByteBuffer): K = codec.decodeKey(bytes) 36 | override def encodeKey(key: K): ByteBuffer = codec.encodeKey(key) 37 | override def encodeValue(value: V2): ByteBuffer = codec.encodeValue(epi.reverseGet(value)) 38 | override def decodeValue(bytes: ByteBuffer): V2 = epi.get(codec.decodeValue(bytes)) 39 | } 40 | ) 41 | } 42 | 43 | /** Given a base RedisCodec[K1, V1], a split epimorphism between K1 and K2, and a split epimorphism between V1 and V2, 44 | * a new RedisCodec[K2, V2] can be derived. 45 | */ 46 | def derive[K1, K2, V1, V2]( 47 | baseCodec: RedisCodec[K1, V1], 48 | epiKeys: SplitEpi[K1, K2], 49 | epiValues: SplitEpi[V1, V2] 50 | ): RedisCodec[K2, V2] = { 51 | val codec = baseCodec.underlying 52 | RedisCodec( 53 | new JRedisCodec[K2, V2] { 54 | override def decodeKey(bytes: ByteBuffer): K2 = epiKeys.get(codec.decodeKey(bytes)) 55 | override def encodeKey(key: K2): ByteBuffer = codec.encodeKey(epiKeys.reverseGet(key)) 56 | override def encodeValue(value: V2): ByteBuffer = codec.encodeValue(epiValues.reverseGet(value)) 57 | override def decodeValue(bytes: ByteBuffer): V2 = epiValues.get(codec.decodeValue(bytes)) 58 | } 59 | ) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisClusterTransactionsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.connection._ 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | 23 | object RedisClusterTransactionsDemo extends LoggerIOApp { 24 | 25 | import Demo._ 26 | 27 | val program: IO[Unit] = { 28 | val key1 = "test1" 29 | 30 | val showResult: String => Option[String] => IO[Unit] = key => 31 | _.fold(IO.println(s"Not found key: $key"))(s => IO.println(s)) 32 | 33 | val commandsApi: Resource[IO, (RedisClusterClient, RedisCommands[IO, String, String])] = 34 | for { 35 | uri <- Resource.eval(RedisURI.make[IO](redisClusterURI)) 36 | client <- RedisClusterClient[IO](uri) 37 | redis <- Redis[IO].fromClusterClient(client, stringCodec)() 38 | } yield client -> redis 39 | 40 | commandsApi 41 | .use { case (client, cmd) => 42 | val nodeCmdResource = 43 | for { 44 | _ <- Resource.eval(cmd.set(key1, "empty")) 45 | nodeId <- Resource.eval(RedisClusterClient.nodeId[IO](client, key1)) 46 | redis <- Redis[IO].fromClusterClientByNode(client, stringCodec, nodeId)() 47 | } yield redis 48 | 49 | // Transactions are only supported on a single node 50 | val notAllowed: IO[Unit] = 51 | cmd.multi 52 | .bracket(_ => cmd.set(key1, "nope") >> cmd.exec.void)(_ => cmd.discard) 53 | .recoverWith { case e: OperationNotSupported => 54 | IO.println(e) 55 | } 56 | .void 57 | 58 | notAllowed *> 59 | // Transaction runs in a single shard, where "key1" is stored 60 | nodeCmdResource.use { redis => 61 | val getter = redis.get(key1).flatTap(showResult(key1)) 62 | 63 | val tx1 = redis.transact_(List(redis.set(key1, "foo"))) 64 | 65 | getter *> tx1 *> getter.void 66 | } 67 | } 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/RestartOnTimeout.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.MonadThrow 20 | import cats.effect.kernel.Clock 21 | import cats.kernel.Monoid 22 | import cats.syntax.all._ 23 | import io.lettuce.core.RedisCommandTimeoutException 24 | 25 | import scala.concurrent.duration.FiniteDuration 26 | 27 | /** Configures restarting operations in case they time out. 28 | * 29 | * This is useful because Lettuce (the underlying Java client) does time out some operations if they do not send any 30 | * data, like reading from a stream. 31 | */ 32 | trait RestartOnTimeout { 33 | 34 | /** @param elapsed 35 | * amount of time elapsed from the start of operation 36 | * @return 37 | * `true` if the operation should be restarted 38 | */ 39 | def apply(elapsed: FiniteDuration): Boolean 40 | 41 | /** Wraps the given operation into a restart loop. */ 42 | def wrap[F[_], A](fa: F[A])( 43 | implicit clock: Clock[F], 44 | monadThrow: MonadThrow[F], 45 | monoid: Monoid[F[A]] 46 | ): F[A] = { 47 | val currentTime = clock.monotonic 48 | 49 | def onTimeout(startedAt: FiniteDuration): F[A] = 50 | for { 51 | now <- currentTime 52 | elapsed = now - startedAt 53 | restart = apply(elapsed) 54 | a <- if (restart) doOp else monoid.empty 55 | } yield a 56 | 57 | def doOp: F[A] = 58 | for { 59 | startedAt <- currentTime 60 | a <- fa.recoverWith { case _: RedisCommandTimeoutException => onTimeout(startedAt) } 61 | } yield a 62 | 63 | doOp 64 | } 65 | } 66 | object RestartOnTimeout { 67 | 68 | /** Always restart. */ 69 | def always: RestartOnTimeout = _ => true 70 | 71 | /** Never restart. */ 72 | def never: RestartOnTimeout = _ => false 73 | 74 | /** Restart if the elapsed time is less than the given duration. */ 75 | def ifBefore(duration: FiniteDuration): RestartOnTimeout = elapsed => elapsed < duration 76 | 77 | /** Restart if the elapsed time is greater than the given duration. */ 78 | def ifAfter(duration: FiniteDuration): RestartOnTimeout = elapsed => elapsed > duration 79 | } 80 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisStreamSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.IO 20 | import cats.implicits.toTraverseOps 21 | import dev.profunktor.redis4cats.effects.XReadOffsets 22 | import dev.profunktor.redis4cats.streams.data.XAddMessage 23 | 24 | import scala.concurrent.duration.DurationInt 25 | 26 | class RedisStreamSpec extends Redis4CatsFunSuite(false) { 27 | 28 | test("append/read to/from a stream") { 29 | readWriteTest("test-stream", 1).unsafeToFuture() 30 | } 31 | 32 | test("append/read to/from a stream - flakiness test") { 33 | (1 to 10).toList 34 | .traverse(i => readWriteTest(s"test-stream-$i", 100)) 35 | .void 36 | .unsafeToFuture() 37 | } 38 | 39 | test("reading from a silent stream should not fail with RedisCommandTimeoutException") { 40 | timeoutingOperationTest { (options, restartOnTimeout) => 41 | fs2.Stream.resource(withRedisStreamOptionsResource(options)).flatMap { case (readStream, _) => 42 | // This stream has no data and previously reading from such stream would fail with an exception 43 | readStream.read(XReadOffsets.all("test-stream-expiration"), restartOnTimeout = restartOnTimeout) 44 | } 45 | } 46 | } 47 | 48 | private def readWriteTest(streamKey: String, length: Long): IO[Unit] = 49 | IO.fromFuture { 50 | IO { 51 | withRedisStream { (readStream, writeStream) => 52 | val read = readStream.read(XReadOffsets.all(streamKey)) 53 | val write = 54 | writeStream.append(fs2.Stream(XAddMessage(streamKey, Map("hello" -> "world"))).repeatN(length)) 55 | 56 | read 57 | .concurrently(write) 58 | .take(length) 59 | .interruptAfter(3.seconds) 60 | .compile 61 | .toList 62 | .map { reads => 63 | assertEquals(reads.size, length.toInt) 64 | reads.foreach { read => 65 | assertEquals(read.key, streamKey) 66 | assertEquals(read.body, Map("hello" -> "world")) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "dev-tools": { 4 | "inputs": { 5 | "flake-utils": "flake-utils", 6 | "nixpkgs": "nixpkgs", 7 | "nixpkgs-jekyll": "nixpkgs-jekyll" 8 | }, 9 | "locked": { 10 | "lastModified": 1747642662, 11 | "narHash": "sha256-T4Vq7c4m9iTRY8qfWB3NmHFDDOx1wTdE2ljO2JydR08=", 12 | "owner": "profunktor", 13 | "repo": "dev-tools", 14 | "rev": "7a2649ae2d2c1a25a9e116821a47304ac8e11715", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "profunktor", 19 | "repo": "dev-tools", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-utils": { 24 | "inputs": { 25 | "systems": "systems" 26 | }, 27 | "locked": { 28 | "lastModified": 1694529238, 29 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 30 | "owner": "numtide", 31 | "repo": "flake-utils", 32 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "numtide", 37 | "repo": "flake-utils", 38 | "type": "github" 39 | } 40 | }, 41 | "nixpkgs": { 42 | "locked": { 43 | "lastModified": 1745526057, 44 | "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", 45 | "owner": "NixOS", 46 | "repo": "nixpkgs", 47 | "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "id": "nixpkgs", 52 | "ref": "nixos-unstable", 53 | "type": "indirect" 54 | } 55 | }, 56 | "nixpkgs-jekyll": { 57 | "locked": { 58 | "lastModified": 1708815994, 59 | "narHash": "sha256-hL7N/ut2Xu0NaDxDMsw2HagAjgDskToGiyZOWriiLYM=", 60 | "owner": "NixOS", 61 | "repo": "nixpkgs", 62 | "rev": "9a9dae8f6319600fa9aebde37f340975cab4b8c0", 63 | "type": "github" 64 | }, 65 | "original": { 66 | "owner": "NixOS", 67 | "repo": "nixpkgs", 68 | "rev": "9a9dae8f6319600fa9aebde37f340975cab4b8c0", 69 | "type": "github" 70 | } 71 | }, 72 | "root": { 73 | "inputs": { 74 | "dev-tools": "dev-tools" 75 | } 76 | }, 77 | "systems": { 78 | "locked": { 79 | "lastModified": 1681028828, 80 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 81 | "owner": "nix-systems", 82 | "repo": "default", 83 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 84 | "type": "github" 85 | }, 86 | "original": { 87 | "owner": "nix-systems", 88 | "repo": "default", 89 | "type": "github" 90 | } 91 | } 92 | }, 93 | "root": "root", 94 | "version": 7 95 | } 96 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisScriptsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import dev.profunktor.redis4cats.algebra.ScriptCommands 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | import dev.profunktor.redis4cats.effects.ScriptOutputType 23 | 24 | object RedisScriptsDemo extends LoggerIOApp { 25 | 26 | import Demo._ 27 | 28 | val program: IO[Unit] = { 29 | val commandsApi: Resource[IO, ScriptCommands[IO, String, String]] = 30 | Redis[IO].utf8(redisURI) 31 | 32 | commandsApi.use { redis => 33 | for { 34 | greeting <- redis.eval("return 'Hello World'", ScriptOutputType.Value) 35 | _ <- IO.println(s"Greetings from Lua: $greeting") 36 | fortyTwo <- redis.eval("return 42", ScriptOutputType.Integer) 37 | _ <- IO.println(s"Answer to the Ultimate Question of Life, the Universe, and Everything: $fortyTwo") 38 | list <- redis.eval( 39 | "return {'Let', 'us', ARGV[1], ARGV[2]}", 40 | ScriptOutputType.Multi, 41 | Nil, 42 | List("have", "fun") 43 | ) 44 | _ <- IO.println(s"We can even return lists: $list") 45 | randomScript = "math.randomseed(tonumber(ARGV[1])); return math.random() * 1000" 46 | shaRandom <- redis.scriptLoad(randomScript) 47 | l <- redis.scriptExists(shaRandom) 48 | List(exists) = l 49 | _ <- IO.println(s"Script is cached on Redis: $exists") 50 | // seeding the RNG with 7 51 | random <- redis.evalSha(shaRandom, ScriptOutputType.Integer, Nil, List("7")) 52 | _ <- IO.println(s"Execution of cached script returns a pseudo-random number: $random") 53 | scriptDigest <- redis.digest(randomScript) 54 | l <- redis.scriptExists(scriptDigest) 55 | List(exists3) = l 56 | _ <- IO.println(s"Locally computed script digest is the same sha as Redis: $exists3") 57 | _ <- redis.scriptFlush 58 | _ <- IO.println("Flushed all cached scripts!") 59 | l <- redis.scriptExists(shaRandom) 60 | List(exists2) = l 61 | _ <- IO.println(s"Script is still cached on Redis: $exists2") 62 | } yield () 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisStringsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.IO 20 | import dev.profunktor.redis4cats.algebra.StringCommands 21 | import dev.profunktor.redis4cats.connection._ 22 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 23 | import io.lettuce.core.RedisCommandExecutionException 24 | 25 | object RedisStringsDemo extends LoggerIOApp { 26 | import Demo._ 27 | 28 | val usernameKey = "test" 29 | val numericKey = "numeric" 30 | 31 | val showResult: Option[String] => IO[Unit] = 32 | _.fold(IO.println(s"Not found key: $usernameKey"))(IO.println) 33 | 34 | // simple strings program 35 | def p1(redis: StringCommands[IO, String, String]): IO[Unit] = 36 | for { 37 | x <- redis.get(usernameKey) 38 | _ <- showResult(x) 39 | _ <- redis.set(usernameKey, "some value") 40 | y <- redis.get(usernameKey) 41 | _ <- showResult(y) 42 | _ <- redis.setNx(usernameKey, "should not happen") 43 | w <- redis.get(usernameKey) 44 | _ <- showResult(w) 45 | } yield () 46 | 47 | // proof that you can still get it wrong with `incr` and `decr`, even if type-safe 48 | def p2( 49 | redis: StringCommands[IO, String, String], 50 | redisN: StringCommands[IO, String, Long] 51 | ): IO[Unit] = 52 | for { 53 | x <- redis.get(numericKey) 54 | _ <- showResult(x) 55 | _ <- redis.set(numericKey, "not a number") 56 | y <- redis.get(numericKey) 57 | _ <- showResult(y) 58 | _ <- redisN.incr(numericKey).attempt.flatMap { 59 | case Left(e: RedisCommandExecutionException) => 60 | IO(assert(e.getMessage == "ERR value is not an integer or out of range")) 61 | case _ => 62 | IO.raiseError(new Exception("Expected error")) 63 | } 64 | w <- redis.get(numericKey) 65 | _ <- showResult(w) 66 | } yield () 67 | 68 | val program: IO[Unit] = { 69 | val res = 70 | for { 71 | cli <- RedisClient[IO].from(redisURI) 72 | rd1 <- Redis[IO].fromClient(cli, stringCodec) 73 | rd2 <- Redis[IO].fromClient(cli, longCodec) 74 | } yield rd1 -> rd2 75 | 76 | res.use { case (rd1, rd2) => 77 | p1(rd1) *> p2(rd1, rd2) 78 | } 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/strings.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import scala.concurrent.duration.FiniteDuration 20 | 21 | import dev.profunktor.redis4cats.effects.{ GetExArg, SetArgs } 22 | 23 | import io.lettuce.core.RedisFuture 24 | import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands 25 | 26 | trait StringCommands[F[_], K, V] 27 | extends Getter[F, K, V] 28 | with Setter[F, K, V] 29 | with MultiKey[F, K, V] 30 | with Decrement[F, K, V] 31 | with Increment[F, K, V] 32 | with Unsafe[F, K, V] 33 | 34 | trait Getter[F[_], K, V] { 35 | def get(key: K): F[Option[V]] 36 | def getEx(key: K, getExArg: GetExArg): F[Option[V]] 37 | def getRange(key: K, start: Long, end: Long): F[Option[V]] 38 | def strLen(key: K): F[Long] 39 | } 40 | 41 | trait Setter[F[_], K, V] { 42 | def append(key: K, value: V): F[Unit] 43 | def getSet(key: K, value: V): F[Option[V]] 44 | def set(key: K, value: V): F[Unit] 45 | def set(key: K, value: V, setArgs: SetArgs): F[Boolean] 46 | def setNx(key: K, value: V): F[Boolean] 47 | def setEx(key: K, value: V, expiresIn: FiniteDuration): F[Unit] 48 | def setRange(key: K, value: V, offset: Long): F[Unit] 49 | } 50 | 51 | trait MultiKey[F[_], K, V] { 52 | def mGet(keys: Set[K]): F[Map[K, V]] 53 | def mSet(keyValues: Map[K, V]): F[Unit] 54 | def mSetNx(keyValues: Map[K, V]): F[Boolean] 55 | } 56 | 57 | trait Decrement[F[_], K, V] { 58 | def decr(key: K): F[Long] 59 | def decrBy(key: K, amount: Long): F[Long] 60 | } 61 | 62 | trait Increment[F[_], K, V] { 63 | def incr(key: K): F[Long] 64 | def incrBy(key: K, amount: Long): F[Long] 65 | def incrByFloat(key: K, amount: Double): F[Double] 66 | } 67 | 68 | trait Unsafe[F[_], K, V] { 69 | 70 | /** USE WITH CAUTION! It gives you access to the underlying Java API. 71 | * 72 | * Useful whenever Redis4cats does not yet support the operation you're looking for. 73 | */ 74 | def unsafe[A](f: RedisClusterAsyncCommands[K, V] => RedisFuture[A]): F[A] 75 | 76 | /** USE WITH CAUTION! It gives you access to the underlying Java API. 77 | * 78 | * Useful whenever Redis4cats does not yet support the operation you're looking for. 79 | */ 80 | def unsafeSync[A](f: RedisClusterAsyncCommands[K, V] => A): F[A] 81 | } 82 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/PubSubDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import scala.concurrent.duration._ 20 | import scala.util.Random 21 | 22 | import cats.effect.IO 23 | import dev.profunktor.redis4cats.connection._ 24 | import dev.profunktor.redis4cats.data.RedisChannel 25 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 26 | import dev.profunktor.redis4cats.pubsub.PubSub 27 | import fs2.{ Pipe, Stream } 28 | 29 | object PubSubDemo extends LoggerIOApp { 30 | 31 | import Demo._ 32 | 33 | private val eventsChannel = RedisChannel("events") 34 | private val gamesChannel = RedisChannel("games") 35 | private val txChannel = RedisChannel("tx-ps") 36 | 37 | def sink(name: String): Pipe[IO, String, Unit] = 38 | _.evalMap(x => IO.println(s"Subscriber: $name >> $x")) 39 | 40 | val stream: Stream[IO, Stream[IO, Unit]] = 41 | for { 42 | client <- Stream.resource(RedisClient[IO].from(redisURI)) 43 | pubSub <- Stream.resource(PubSub.mkPubSubConnection[IO, String, String](client, stringCodec)) 44 | redis <- Stream.resource(Redis[IO].fromClient(client, stringCodec)) 45 | sub1 = pubSub.subscribe(eventsChannel) 46 | sub2 = pubSub.subscribe(gamesChannel) 47 | sub3 = pubSub.subscribe(txChannel) 48 | pub1 = pubSub.publish(eventsChannel) 49 | pub2 = pubSub.publish(gamesChannel) 50 | ops = List( 51 | redis.set("ps", "x"), 52 | redis.unsafe(_.publish(txChannel.underlying, "hey")).void 53 | ) 54 | } yield Stream( 55 | sub1.through(sink("#events")), 56 | sub2.through(sink("#games")), 57 | sub3.through(sink("#tx-ps")), 58 | Stream.awakeEvery[IO](3.seconds) >> Stream.eval(IO(Random.nextInt(100).toString)).through(pub1), 59 | Stream.awakeEvery[IO](5.seconds) >> Stream.emit("Pac-Man!").through(pub2), 60 | Stream.awakeDelay[IO](11.seconds) >> Stream.eval(pubSub.unsubscribe(gamesChannel)), 61 | Stream.awakeEvery[IO](6.seconds) >> Stream 62 | .eval(pubSub.pubSubSubscriptions(List(eventsChannel, gamesChannel, txChannel))) 63 | .evalMap(IO.println), 64 | Stream.sleep[IO](1.second) ++ Stream.exec(redis.transact_(ops)) 65 | ).parJoinUnbounded.drain 66 | 67 | val program: IO[Unit] = 68 | stream.flatten.compile.drain 69 | 70 | } 71 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisBitmapsDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.{ IO, Resource } 20 | import cats.syntax.all._ 21 | import dev.profunktor.redis4cats.algebra.BitCommandOperation.{ IncrUnsignedBy, SetUnsigned } 22 | import dev.profunktor.redis4cats.algebra.BitCommands 23 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 24 | 25 | object RedisBitmapsDemo extends LoggerIOApp { 26 | 27 | import Demo._ 28 | 29 | val program: IO[Unit] = { 30 | val testKey = "bitsets" 31 | 32 | val generalApi: Resource[IO, RedisCommands[IO, String, String]] = Redis[IO].utf8(redisURI) 33 | val bitmapsApi: Resource[IO, BitCommands[IO, String, String]] = Redis[IO].utf8(redisURI) 34 | 35 | (bitmapsApi, generalApi).tupled.use { case (bits, strings) => 36 | for { 37 | _ <- strings.del(testKey) 38 | a <- bits.setBit(testKey, 7, 1) 39 | b <- bits.setBit(testKey, 7, 0) 40 | _ <- IO.println(s"Before $a after $b") 41 | cSet <- bits.setBit(testKey, 6, 1) 42 | _ <- IO.println(s"Setting offset 6 to $cSet") 43 | c <- bits.getBit(testKey, 6) 44 | _ <- IO.println(s"Bit at offset 6 is $c") 45 | batchSet <- for { 46 | s1 <- bits.setBit("bitmapsarestrings", 2, 1) 47 | s2 <- bits.setBit("bitmapsarestrings", 3, 1) 48 | s3 <- bits.setBit("bitmapsarestrings", 5, 1) 49 | s4 <- bits.setBit("bitmapsarestrings", 10, 1) 50 | s5 <- bits.setBit("bitmapsarestrings", 11, 1) 51 | s6 <- bits.setBit("bitmapsarestrings", 14, 1) 52 | } yield s1 + s2 + s3 + s4 + s5 + s6 53 | _ <- IO.println(s"Set multiple $batchSet") 54 | truth <- strings.get("bitmapsarestrings") 55 | _ <- IO.println(s"The answer to everything is $truth") 56 | bf <- bits.bitField( 57 | "inmap", 58 | SetUnsigned(2, 1), 59 | SetUnsigned(3, 1), 60 | SetUnsigned(5, 1), 61 | SetUnsigned(10, 1), 62 | SetUnsigned(11, 1), 63 | SetUnsigned(14, 1), 64 | IncrUnsignedBy(14, 1) 65 | ) 66 | _ <- IO.println(s"Via bitfield $bf") 67 | } yield () 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/effect/TxExecutor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * This file contains code adapted from cats-effect, which is 19 | * Copyright (c) 2017-2021 The Typelevel Cats-effect Project Developers. 20 | * The license notice for cats-effect is the same as the above. 21 | */ 22 | 23 | package dev.profunktor.redis4cats.effect 24 | 25 | import java.util.concurrent.Executors 26 | 27 | import scala.concurrent.ExecutionContext 28 | import scala.util.control.NonFatal 29 | 30 | import cats.effect.kernel._ 31 | import cats.syntax.all._ 32 | 33 | private[redis4cats] trait TxExecutor[F[_]] { 34 | def delay[A](thunk: => A): F[A] 35 | def eval[A](fa: F[A]): F[A] 36 | def start[A](fa: F[A]): F[Fiber[F, Throwable, A]] 37 | def liftK[G[_]: Async]: TxExecutor[G] 38 | } 39 | 40 | private[redis4cats] object TxExecutor { 41 | def make[F[_]: Async]: Resource[F, TxExecutor[F]] = 42 | Resource 43 | .make(Sync[F].delay(Executors.newFixedThreadPool(1, TxThreadFactory))) { ec => 44 | Sync[F] 45 | .delay(ec.shutdownNow()) 46 | .ensure(new IllegalStateException("There were outstanding tasks at time of shutdown of the Redis thread"))( 47 | _.isEmpty 48 | ) 49 | .void 50 | } 51 | .map(es => fromEC(exitOnFatal(ExecutionContext.fromExecutorService(es)))) 52 | 53 | private def exitOnFatal(ec: ExecutionContext): ExecutionContext = new ExecutionContext { 54 | def execute(r: Runnable): Unit = 55 | ec.execute(() => 56 | try 57 | r.run() 58 | catch { 59 | case NonFatal(t) => 60 | reportFailure(t) 61 | 62 | case t: Throwable => 63 | // under most circumstances, this will work even with fatal errors 64 | t.printStackTrace() 65 | System.exit(1) 66 | } 67 | ) 68 | 69 | def reportFailure(t: Throwable): Unit = 70 | ec.reportFailure(t) 71 | } 72 | 73 | private def fromEC[F[_]: Async](ec: ExecutionContext): TxExecutor[F] = 74 | new TxExecutor[F] { 75 | def delay[A](thunk: => A): F[A] = eval(Sync[F].delay(thunk)) 76 | def eval[A](fa: F[A]): F[A] = Async[F].evalOn(fa, ec) 77 | def start[A](fa: F[A]): F[Fiber[F, Throwable, A]] = Async[F].startOn(fa, ec) 78 | def liftK[G[_]: Async]: TxExecutor[G] = fromEC[G](ec) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/LivePubSubStats.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | package pubsub 19 | package internals 20 | 21 | import cats.FlatMap 22 | import cats.syntax.all._ 23 | import dev.profunktor.redis4cats.data._ 24 | import dev.profunktor.redis4cats.effect.FutureLift 25 | import dev.profunktor.redis4cats.pubsub.data.Subscription 26 | import io.lettuce.core.pubsub.StatefulRedisPubSubConnection 27 | import dev.profunktor.redis4cats.JavaConversions._ 28 | import dev.profunktor.redis4cats.pubsub.internals.LivePubSubStats.toSubscription 29 | 30 | import java.{ util => ju } 31 | import java.lang.{ Long => JLong } 32 | private[pubsub] class LivePubSubStats[F[_]: FlatMap: FutureLift, K, V]( 33 | pubConnection: StatefulRedisPubSubConnection[K, V] 34 | ) extends PubSubStats[F, K] { 35 | 36 | override def numPat: F[Long] = 37 | FutureLift[F].lift(pubConnection.async().pubsubNumpat()).map(Long.unbox) 38 | 39 | override def numSub: F[List[Subscription[K]]] = 40 | FutureLift[F] 41 | .lift(pubConnection.async().pubsubNumsub()) 42 | .map(toSubscription[K]) 43 | 44 | override def pubSubChannels: F[List[RedisChannel[K]]] = 45 | FutureLift[F] 46 | .lift(pubConnection.async().pubsubChannels()) 47 | .map(_.asScala.toList.map(RedisChannel[K])) 48 | 49 | override def pubSubShardChannels: F[List[RedisChannel[K]]] = 50 | FutureLift[F] 51 | .lift(pubConnection.async().pubsubShardChannels()) 52 | .map(_.asScala.toList.map(RedisChannel[K])) 53 | 54 | override def pubSubSubscriptions(channel: RedisChannel[K]): F[Option[Subscription[K]]] = 55 | pubSubSubscriptions(List(channel)).map(_.headOption) 56 | 57 | override def pubSubSubscriptions(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 58 | FutureLift[F] 59 | .lift(pubConnection.async().pubsubNumsub(channels.map(_.underlying): _*)) 60 | .map(toSubscription[K]) 61 | 62 | override def shardNumSub(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 63 | FutureLift[F] 64 | .lift(pubConnection.async().pubsubShardNumsub(channels.map(_.underlying): _*)) 65 | .map(toSubscription[K]) 66 | } 67 | object LivePubSubStats { 68 | private def toSubscription[K](map: ju.Map[K, JLong]): List[Subscription[K]] = 69 | map.asScala.toList.map { case (k, n) => Subscription(RedisChannel[K](k), Long.unbox(n)) } 70 | } 71 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/RedisSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import dev.profunktor.redis4cats.data.RedisCodec 20 | import io.lettuce.core.codec.{ RedisCodec => JRedisCodec, StringCodec => JStringCodec, ToByteBufEncoder } 21 | import io.netty.buffer.ByteBuf 22 | 23 | class RedisSpec extends Redis4CatsFunSuite(false) with TestScenarios { 24 | 25 | test("geo api")(withRedis(locationScenario)) 26 | 27 | test("hashes api")(withRedis(hashesScenario)) 28 | 29 | test("lists api")(withRedis(listsScenario)) 30 | 31 | test("keys api")(withRedis(cmd => keysScenario(cmd) >> scanScenario(cmd))) 32 | 33 | test("sets api")(withRedis(setsScenario)) 34 | 35 | test("sorted sets api")(withAbstractRedis(sortedSetsScenario)(RedisCodec(LongCodec))) 36 | 37 | test("bitmaps api")(withRedis(bitmapsScenario)) 38 | 39 | test("strings api")(withRedis(stringsScenario)) 40 | 41 | test("connection api")(withRedis(connectionScenario)) 42 | 43 | test("pipelining")(withRedis(pipelineScenario)) 44 | 45 | test("server")(withRedis(serverScenario)) 46 | 47 | test("transactions: successful")(withRedis(transactionScenario)) 48 | 49 | test("scripts")(withRedis(scriptsScenario)) 50 | 51 | test("scripts lua extensions")(withRedis(scriptingLuaExtensionsScenario)) 52 | 53 | test("functions")(withRedis(functionsScenario)) 54 | 55 | test("hyperloglog api")(withRedis(hyperloglogScenario)) 56 | 57 | test("pattern key sub")(withRedisClient(keyPatternSubScenario)) 58 | 59 | test("pattern channel sub")(withRedisClient(channelPatternSubScenario)) 60 | 61 | test("streams api")(withRedis(streamsScenario)) 62 | } 63 | 64 | object LongCodec extends JRedisCodec[String, Long] with ToByteBufEncoder[String, Long] { 65 | 66 | import java.nio.ByteBuffer 67 | 68 | private val codec = JStringCodec.UTF8 69 | 70 | override def decodeKey(bytes: ByteBuffer): String = codec.decodeKey(bytes) 71 | override def encodeKey(key: String): ByteBuffer = codec.encodeKey(key) 72 | override def encodeValue(value: Long): ByteBuffer = codec.encodeValue(value.toString) 73 | override def decodeValue(bytes: ByteBuffer): Long = codec.decodeValue(bytes).toLong 74 | override def encodeKey(key: String, target: ByteBuf): Unit = codec.encodeKey(key, target) 75 | override def encodeValue(value: Long, target: ByteBuf): Unit = codec.encodeValue(value.toString, target) 76 | override def estimateSize(keyOrValue: scala.Any): Int = codec.estimateSize(keyOrValue) 77 | } 78 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/extensions/luaScripting.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.extensions 18 | 19 | import cats.effect.kernel.{ Resource, Sync } 20 | import cats.syntax.all._ 21 | import cats.{ ApplicativeThrow, Functor } 22 | import dev.profunktor.redis4cats.algebra.Scripting 23 | import dev.profunktor.redis4cats.effects.ScriptOutputType 24 | import io.lettuce.core.RedisNoScriptException 25 | 26 | object luaScripting { 27 | 28 | final case class LuaScript(contents: String, sha: String) 29 | 30 | object LuaScript { 31 | 32 | def make[F[_]: Functor](redis: Scripting[F, _, _])(contents: String): F[LuaScript] = 33 | redis.digest(contents).map(sha => LuaScript(contents, sha)) 34 | 35 | /** Helper to load a lua script from resources/lua/{resourceName}. The path to the lua scripts can be configured. 36 | * @param redis 37 | * redis commands 38 | * @param resourceName 39 | * filename of the lua script 40 | * @param pathToScripts 41 | * path to the lua scripts 42 | * @return 43 | * [[LuaScript]] 44 | */ 45 | def loadFromResources[F[_]: Sync](redis: Scripting[F, _, _])( 46 | resourceName: String, 47 | pathToScripts: String = "lua" 48 | ): F[LuaScript] = 49 | Resource 50 | .fromAutoCloseable( 51 | Sync[F].blocking( 52 | scala.io.Source.fromResource(resource = s"$pathToScripts/$resourceName") 53 | ) 54 | ) 55 | .evalMap(fileSrc => Sync[F].blocking(fileSrc.mkString)) 56 | .use(make(redis)) 57 | 58 | } 59 | 60 | implicit class LuaScriptingExtensions[F[_]: ApplicativeThrow, K, V](redis: Scripting[F, K, V]) { 61 | 62 | /** Evaluate the cached lua script via it's sha. If the script is not cached, fallback to evaluating the script 63 | * directly. 64 | * @param luaScript 65 | * the lua script with its content and sha 66 | * @param output 67 | * output of script 68 | * @param keys 69 | * keys to script 70 | * @param values 71 | * values to script 72 | * @return 73 | * ScriptOutputType 74 | */ 75 | def evalLua( 76 | luaScript: LuaScript, 77 | output: ScriptOutputType[V], 78 | keys: List[K], 79 | values: List[V] 80 | ): F[output.R] = 81 | redis 82 | .evalSha( 83 | luaScript.sha, 84 | output, 85 | keys, 86 | values 87 | ) 88 | .recoverWith { case _: RedisNoScriptException => 89 | redis.eval(luaScript.contents, output, keys, values) 90 | } 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisPoolDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect.IO 20 | import dev.profunktor.redis4cats.connection._ 21 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 22 | import dev.profunktor.redis4cats.Redis.Pool._ 23 | import io.lettuce.core.RedisCommandExecutionException 24 | import org.typelevel.keypool.KeyPool 25 | import fs2.Stream 26 | 27 | object RedisPoolDemo extends LoggerIOApp { 28 | import Demo._ 29 | 30 | val usernameKey = "test" 31 | val numericKey = "numeric" 32 | 33 | val showResult: Option[String] => IO[Unit] = 34 | _.fold(IO.println(s"Not found key: $usernameKey"))(IO.println) 35 | 36 | // simple strings program 37 | def p1(stringPool: KeyPool[IO, Unit, RedisCommands[IO, String, String]]): IO[Unit] = 38 | stringPool.withRedisCommands { redis => 39 | for { 40 | x <- redis.get(usernameKey) 41 | _ <- showResult(x) 42 | _ <- redis.set(usernameKey, "some value") 43 | y <- redis.get(usernameKey) 44 | _ <- showResult(y) 45 | _ <- redis.setNx(usernameKey, "should not happen") 46 | w <- redis.get(usernameKey) 47 | _ <- showResult(w) 48 | } yield () 49 | } 50 | 51 | // proof that you can still get it wrong with `incr` and `decr`, even if type-safe 52 | def p2( 53 | stringPool: KeyPool[IO, Unit, RedisCommands[IO, String, String]], 54 | longPool: KeyPool[IO, Unit, RedisCommands[IO, String, Long]] 55 | ): IO[Unit] = 56 | stringPool.withRedisCommands { redis => 57 | longPool.withRedisCommands { redisN => 58 | for { 59 | x <- redis.get(numericKey) 60 | _ <- showResult(x) 61 | _ <- redis.set(numericKey, "not a number") 62 | y <- redis.get(numericKey) 63 | _ <- showResult(y) 64 | _ <- redisN.incr(numericKey).attempt.flatMap { 65 | case Left(e: RedisCommandExecutionException) => 66 | IO(assert(e.getMessage == "ERR value is not an integer or out of range")) 67 | case _ => 68 | IO.raiseError(new Exception("Expected error")) 69 | } 70 | w <- redis.get(numericKey) 71 | _ <- showResult(w) 72 | } yield () 73 | } 74 | } 75 | 76 | val program: IO[Unit] = { 77 | val res: Stream[IO, Unit] = 78 | for { 79 | cli <- Stream.resource(RedisClient[IO].from(redisURI)) 80 | rd1 <- Stream.resource(Redis[IO].pooled(cli, stringCodec)) 81 | rd2 <- Stream.resource(Redis[IO].pooled(cli, longCodec)) 82 | _ <- Stream.eval(p1(rd1) *> p2(rd1, rd2)) 83 | } yield () 84 | 85 | res.compile.lastOrError 86 | 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /modules/tests/src/test/scala/dev/profunktor/redis4cats/OptimisticLockSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.data.EitherT 20 | import cats.effect._ 21 | import cats.syntax.all._ 22 | import dev.profunktor.redis4cats.connection.RedisClient 23 | import dev.profunktor.redis4cats.data.RedisCodec 24 | import dev.profunktor.redis4cats.effect.Log.NoOp._ 25 | import dev.profunktor.redis4cats.tx._ 26 | 27 | class OptimisticLockSuite extends IOSuite { 28 | 29 | private val redisURI = "redis://localhost:6379" 30 | private val mkRedis = RedisClient[IO].from(redisURI) 31 | private val Parallelism = 10 32 | 33 | private val testKey = "tx-lock-key" 34 | private val InitialValue = "a" 35 | private val UpdatedValue = "b" 36 | 37 | test("Optimistic lock allows single update") { 38 | mkRedis 39 | .use(client => setupTestData(client) >> concurrentUpdates(client)) 40 | .map { results => 41 | val (left, right) = results.separate 42 | assertEquals(left.size, Parallelism - 1) 43 | assertEquals(right.size, 1) 44 | } 45 | } 46 | 47 | private def setupTestData(client: RedisClient): IO[Unit] = 48 | commands(client).use(cmds => cmds.flushAll >> cmds.set(testKey, InitialValue)) 49 | 50 | private def concurrentUpdates(client: RedisClient): IO[List[Either[String, Unit]]] = 51 | (Deferred[IO, Unit], Ref.of[IO, Int](0)).parTupled.flatMap { case (promise, counter) => 52 | // A promise to make sure all the connections call WATCH before running the transaction 53 | def attemptComplete = counter.get.flatMap { count => 54 | promise.complete(()).attempt.void.whenA(count === Parallelism) 55 | } 56 | List.range(0, Parallelism).as(exclusiveUpdate(client, promise, counter, attemptComplete)).parSequence 57 | } 58 | 59 | private def exclusiveUpdate( 60 | client: RedisClient, 61 | promise: Deferred[IO, Unit], 62 | counter: Ref[IO, Int], 63 | attemptComplete: IO[Unit] 64 | ): IO[Either[String, Unit]] = 65 | commands(client).use { redis => 66 | EitherT 67 | .right[String](redis.watch(testKey)) 68 | .semiflatMap(_ => counter.update(_ + 1) >> attemptComplete >> promise.get) 69 | .flatMapF(_ => 70 | redis 71 | .transact_(redis.set(testKey, UpdatedValue) :: Nil) 72 | .as(Either.right[String, Unit](())) 73 | .recover { case TransactionDiscarded => 74 | Left("Discarded") 75 | } 76 | .uncancelable 77 | ) 78 | .value 79 | } 80 | 81 | private def commands(client: RedisClient): Resource[IO, RedisCommands[IO, String, String]] = 82 | Redis[IO].fromClient(client, RedisCodec.Ascii) 83 | 84 | } 85 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/keys.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import java.time.Instant 20 | import dev.profunktor.redis4cats.data.KeyScanCursor 21 | import dev.profunktor.redis4cats.effects.{ CopyArgs, ExpireExistenceArg, KeyScanArgs, RedisType, RestoreArgs, ScanArgs } 22 | 23 | import scala.concurrent.duration.FiniteDuration 24 | 25 | trait KeyCommands[F[_], K] { 26 | def copy(source: K, destination: K): F[Boolean] 27 | def copy(source: K, destination: K, copyArgs: CopyArgs): F[Boolean] 28 | def del(k: K, keys: K*): F[Long] 29 | def dump(key: K): F[Option[Array[Byte]]] 30 | def exists(key: K, keys: K*): F[Boolean] 31 | def expire(key: K, expiresIn: FiniteDuration): F[Boolean] 32 | def expire(key: K, expiresIn: FiniteDuration, expireExistenceArg: ExpireExistenceArg): F[Boolean] 33 | def expireAt(key: K, at: Instant): F[Boolean] 34 | def expireAt(key: K, at: Instant, expireExistenceArg: ExpireExistenceArg): F[Boolean] 35 | def objectIdletime(key: K): F[Option[FiniteDuration]] 36 | def persist(key: K): F[Boolean] 37 | def pttl(key: K): F[Option[FiniteDuration]] 38 | def randomKey: F[Option[K]] 39 | // restores a key with the given serialized value, previously obtained using DUMP without a ttl 40 | def restore(key: K, value: Array[Byte]): F[Unit] 41 | def restore(key: K, value: Array[Byte], restoreArgs: RestoreArgs): F[Unit] 42 | def scan: F[KeyScanCursor[K]] 43 | @deprecated("In favor of scan(cursor: KeyScanCursor[K])", since = "0.10.4") 44 | def scan(cursor: Long): F[KeyScanCursor[K]] 45 | def scan(previous: KeyScanCursor[K]): F[KeyScanCursor[K]] 46 | @deprecated("In favor of scan(keyScanArgs: KeyScanArgs)", since = "1.7.2") 47 | def scan(scanArgs: ScanArgs): F[KeyScanCursor[K]] 48 | def scan(keyScanArgs: KeyScanArgs): F[KeyScanCursor[K]] 49 | @deprecated("In favor of scan(cursor: KeyScanCursor[K], scanArgs: ScanArgs)", since = "0.10.4") 50 | def scan(cursor: Long, scanArgs: ScanArgs): F[KeyScanCursor[K]] 51 | @deprecated("In favor of scan(previous: KeyScanCursor[K], keyScanArgs: KeyScanArgs)", since = "1.7.2") 52 | def scan(previous: KeyScanCursor[K], scanArgs: ScanArgs): F[KeyScanCursor[K]] 53 | def scan(cursor: KeyScanCursor[K], keyScanArgs: KeyScanArgs): F[KeyScanCursor[K]] 54 | def typeOf(key: K): F[Option[RedisType]] 55 | def ttl(key: K): F[Option[FiniteDuration]] 56 | // This command is very similar to DEL: it removes the specified keys. Just like DEL a key is ignored if it does not exist. However the command performs the actual memory reclaiming in a different thread, so it is not blocking, while DEL is. This is where the command name comes from: the command just unlinks the keys from the keyspace. The actual removal will happen later asynchronously. 57 | def unlink(key: K*): F[Long] 58 | 59 | } 60 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/scripts.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.effects.{ FlushMode, FunctionRestoreMode, ScriptOutputType } 20 | 21 | trait ScriptCommands[F[_], K, V] extends Scripting[F, K, V] with Functions[F, K, V] 22 | 23 | trait Scripting[F[_], K, V] { 24 | // these methods don't use varargs as they cause problems with type inference, see: 25 | // https://github.com/scala/bug/issues/11488 26 | def eval(script: String, output: ScriptOutputType[V]): F[output.R] 27 | def eval(script: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 28 | def eval(script: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 29 | def evalReadOnly(script: String, output: ScriptOutputType[V]): F[output.R] 30 | def evalReadOnly(script: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 31 | def evalReadOnly(script: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 32 | def evalSha(digest: String, output: ScriptOutputType[V]): F[output.R] 33 | def evalSha(digest: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 34 | def evalSha(digest: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 35 | def evalShaReadOnly(digest: String, output: ScriptOutputType[V]): F[output.R] 36 | def evalShaReadOnly(digest: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 37 | def evalShaReadOnly(digest: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 38 | def scriptLoad(script: String): F[String] 39 | def scriptLoad(script: Array[Byte]): F[String] 40 | def scriptExists(digests: String*): F[List[Boolean]] 41 | def scriptFlush: F[Unit] 42 | def digest(script: String): F[String] 43 | } 44 | 45 | trait Functions[F[_], K, V] { 46 | def fcall(function: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 47 | def fcall(function: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 48 | def fcallReadOnly(function: String, output: ScriptOutputType[V], keys: List[K]): F[output.R] 49 | def fcallReadOnly(function: String, output: ScriptOutputType[V], keys: List[K], values: List[V]): F[output.R] 50 | def functionLoad(functionCode: String): F[String] 51 | def functionLoad(functionCode: String, replace: Boolean): F[String] 52 | def functionDump(): F[Array[Byte]] 53 | def functionRestore(dump: Array[Byte]): F[String] 54 | def functionRestore(dump: Array[Byte], mode: FunctionRestoreMode): F[String] 55 | def functionFlush(flushMode: FlushMode): F[String] 56 | def functionKill(): F[String] 57 | def functionList(): F[List[Map[String, Any]]] 58 | def functionList(libraryName: String): F[List[Map[String, Any]]] 59 | } 60 | -------------------------------------------------------------------------------- /site/docs/effects/scripting.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: docs 3 | title: "Scripting" 4 | number: 12 5 | --- 6 | 7 | # Scripting API 8 | 9 | Purely functional interface for the [Scripting API](https://redis.io/commands#scripting). 10 | 11 | ```scala mdoc:invisible 12 | import cats.effect.{IO, Resource} 13 | import cats.implicits._ 14 | import dev.profunktor.redis4cats.Redis 15 | import dev.profunktor.redis4cats.algebra.ScriptCommands 16 | import dev.profunktor.redis4cats.effects.ScriptOutputType 17 | import dev.profunktor.redis4cats.data._ 18 | import dev.profunktor.redis4cats.log4cats._ 19 | import org.typelevel.log4cats.Logger 20 | import org.typelevel.log4cats.slf4j.Slf4jLogger 21 | 22 | implicit val logger: Logger[IO] = Slf4jLogger.getLogger[IO] 23 | 24 | val commandsApi: Resource[IO, ScriptCommands[IO, String, String]] = { 25 | Redis[IO].fromClient[String, String](null, null.asInstanceOf[RedisCodec[String, String]]).widen[ScriptCommands[IO, String, String]] 26 | } 27 | ``` 28 | 29 | ### Script Commands usage 30 | 31 | Once you have acquired a connection you can start using it: 32 | 33 | ```scala mdoc:silent 34 | import cats.effect.IO 35 | 36 | def putStrLn(str: String): IO[Unit] = IO(println(str)) 37 | 38 | commandsApi.use { redis => // ScriptCommands[IO, String, String] 39 | for { 40 | // returns a String according the value codec (the last type parameter of ScriptCommands) 41 | greeting <- redis.eval("return 'Hello World'", ScriptOutputType.Value) 42 | _ <- putStrLn(s"Greetings from Lua: $greeting") 43 | } yield () 44 | } 45 | ``` 46 | 47 | The return type depends on the `ScriptOutputType` you pass and needs to suite the result of the Lua script itself. Possible values are `Integer`, `Value` (for decoding the result using the value codec), `Multi` (for many values) and `Status` (maps to `Unit` in Scala). Scripts can be cached for better performance using `scriptLoad` and then executed via `evalSha`, see the [redis docs]((https://redis.io/commands#scripting)) for details. 48 | 49 | ### Lua Scripting Extensions 50 | 51 | Redis4cats provides useful extensions to the Lua scripting; methods for loading scripts and executing them by their SHA1 are provided. 52 | 53 | Suppose you have the following Lua script saved under the project's `resources` folder, `mymodule/src/main/resources/lua/hsetAndExpire.lua)`: 54 | 55 | ```lua 56 | local key = KEYS[1] 57 | local field = ARGV[1] 58 | local value = ARGV[2] 59 | local ttl = tonumber(ARGV[3]) 60 | 61 | local numFieldsSet = redis.call('hset', key, field, value) 62 | redis.call('expire', key, ttl) 63 | return numFieldsSet 64 | ``` 65 | 66 | Then you can load it into Redis and execute via: 67 | 68 | ```scala mdoc:silent 69 | import dev.profunktor.redis4cats.extensions.luaScripting._ 70 | 71 | commandsApi.use { redis => // ScriptCommands[IO, String, String] 72 | for { 73 | hsetAndExpire <- LuaScript.loadFromResources[IO](redis)("hsetAndExpire.lua") 74 | value = "42" 75 | ttl = "10" 76 | _ <- redis.evalLua( 77 | hsetAndExpire, 78 | ScriptOutputType.Integer[String], 79 | keys = List("mySetKey"), 80 | values = List("myField", value, ttl) 81 | ) 82 | } yield () 83 | } 84 | ``` 85 | 86 | The extension api provides the following methods: 87 | - `LuaScript.make` to create a `LuaScript` instance from a string 88 | - `LuaScript.loadFromResources` to load a Lua script from resources, path is configurable and defaults to `lua/` 89 | - `evalLua` method as a shortcut for `evalSha` then falling back to `eval` if the script is not loaded yet 90 | 91 | -------------------------------------------------------------------------------- /modules/examples/src/main/scala/dev/profunktor/redis4cats/RedisTxDemo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | 19 | import cats.effect._ 20 | import dev.profunktor.redis4cats.connection.RedisClient 21 | import dev.profunktor.redis4cats.data.RedisCodec 22 | import dev.profunktor.redis4cats.log4cats._ 23 | import dev.profunktor.redis4cats.tx._ 24 | 25 | object RedisTxDemo extends LoggerIOApp { 26 | 27 | import Demo._ 28 | 29 | val program: IO[Unit] = { 30 | val key1 = "test1" 31 | val key2 = "test2" 32 | val key3 = "test3" 33 | 34 | val showResult: String => Option[String] => IO[Unit] = key => 35 | _.fold(IO.println(s"Not found key: $key"))(s => IO.println(s"$key: $s")) 36 | 37 | val mkClient: Resource[IO, RedisClient] = 38 | RedisClient[IO].from(redisURI) 39 | 40 | def mkRedis(cli: RedisClient): Resource[IO, RedisCommands[IO, String, String]] = 41 | Redis[IO].fromClient(cli, RedisCodec.Utf8) 42 | 43 | def prog[A]( 44 | redis: RedisCommands[IO, String, String], 45 | ops: TxStore[IO, String, A] => List[IO[Unit]] 46 | ): IO[Unit] = 47 | redis 48 | .transact(ops) // or redis.transact_(ops) to discard the result 49 | .flatMap(kv => IO.println(s"KV: $kv")) 50 | .handleErrorWith { 51 | case TransactionDiscarded => 52 | IO.println("[Error] - Transaction Discarded") 53 | case e => 54 | IO.println(s"[Error] - ${e.getMessage}") 55 | } 56 | 57 | // Running two concurrent transactions (needs two different RedisCommands) 58 | mkClient.use { cli => 59 | val p1 = mkRedis(cli).use { redis => 60 | val getters = 61 | redis.get(key1).flatTap(showResult(key1)) *> 62 | redis.get(key2).flatTap(showResult(key2)) 63 | 64 | // it is not possible to mix different stores. In case of needing to preserve values 65 | // of other types, you'd need to use a local Ref or so. 66 | val ops = (store: TxStore[IO, String, Option[String]]) => 67 | List( 68 | redis.set(key1, "sad"), 69 | redis.set(key2, "windows"), 70 | redis.get(key1).flatMap(store.set(s"$key1-v1")), 71 | redis.set(key1, "nix"), 72 | redis.set(key2, "linux"), 73 | redis.get(key1).flatMap(store.set(s"$key1-v2")) 74 | ) 75 | 76 | getters >> prog(redis, ops) >> getters >> IO.println("keep doing stuff...") 77 | } 78 | 79 | val p2 = mkRedis(cli).use { redis => 80 | val ops = (store: TxStore[IO, String, Long]) => 81 | List( 82 | redis.set("yo", "wat"), 83 | redis.incr(key3).flatMap(store.set(s"$key3-v1")), 84 | redis.incr(key3).flatMap(store.set(s"$key3-v2")), 85 | redis.set("wat", "yo") 86 | ) 87 | 88 | prog(redis, ops) 89 | } 90 | 91 | p1 &> p2 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /modules/streams/src/main/scala/dev/profunktor/redis4cats/pubsub/internals/LivePubSubCommands.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats 18 | package pubsub 19 | package internals 20 | 21 | import cats.effect.kernel._ 22 | import cats.syntax.all._ 23 | import dev.profunktor.redis4cats.data.RedisChannel 24 | import dev.profunktor.redis4cats.data.RedisPattern 25 | import dev.profunktor.redis4cats.data.RedisPatternEvent 26 | import dev.profunktor.redis4cats.pubsub.data.Subscription 27 | import dev.profunktor.redis4cats.effect.{ FutureLift, Log } 28 | import fs2.Stream 29 | import io.lettuce.core.pubsub.StatefulRedisPubSubConnection 30 | 31 | private[pubsub] class LivePubSubCommands[F[_]: Async: Log, K, V]( 32 | state: PubSubState[F, K, V], 33 | subConnection: StatefulRedisPubSubConnection[K, V], 34 | pubConnection: StatefulRedisPubSubConnection[K, V] 35 | ) extends PubSubCommands[F, Stream[F, *], K, V] { 36 | 37 | private[redis4cats] val subCommands: SubscribeCommands[F, Stream[F, *], K, V] = 38 | new Subscriber[F, K, V](state, subConnection) 39 | private[redis4cats] val pubSubStats: PubSubStats[F, K] = new LivePubSubStats(pubConnection) 40 | 41 | override def subscribe(channel: RedisChannel[K]): Stream[F, V] = 42 | subCommands.subscribe(channel) 43 | 44 | override def unsubscribe(channel: RedisChannel[K]): F[Unit] = 45 | subCommands.unsubscribe(channel) 46 | 47 | override def psubscribe(pattern: RedisPattern[K]): Stream[F, RedisPatternEvent[K, V]] = 48 | subCommands.psubscribe(pattern) 49 | 50 | override def punsubscribe(pattern: RedisPattern[K]): F[Unit] = 51 | subCommands.punsubscribe(pattern) 52 | 53 | override def internalChannelSubscriptions: F[Map[RedisChannel[K], Long]] = 54 | subCommands.internalChannelSubscriptions 55 | 56 | override def internalPatternSubscriptions: F[Map[RedisPattern[K], Long]] = 57 | subCommands.internalPatternSubscriptions 58 | 59 | override def publish(channel: RedisChannel[K]): Stream[F, V] => Stream[F, Long] = 60 | _.evalMap(publish(channel, _)) 61 | 62 | override def publish(channel: RedisChannel[K], message: V): F[Long] = 63 | FutureLift[F].lift(pubConnection.async().publish(channel.underlying, message)).map(l => l: Long) 64 | 65 | override def numPat: F[Long] = 66 | pubSubStats.numPat 67 | 68 | override def numSub: F[List[Subscription[K]]] = 69 | pubSubStats.numSub 70 | 71 | override def pubSubChannels: F[List[RedisChannel[K]]] = 72 | pubSubStats.pubSubChannels 73 | 74 | override def pubSubShardChannels: F[List[RedisChannel[K]]] = 75 | pubSubStats.pubSubShardChannels 76 | 77 | override def pubSubSubscriptions(channel: RedisChannel[K]): F[Option[Subscription[K]]] = 78 | pubSubStats.pubSubSubscriptions(channel) 79 | 80 | override def pubSubSubscriptions(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 81 | pubSubStats.pubSubSubscriptions(channels) 82 | 83 | override def shardNumSub(channels: List[RedisChannel[K]]): F[List[Subscription[K]]] = 84 | pubSubStats.shardNumSub(channels) 85 | } 86 | -------------------------------------------------------------------------------- /modules/effects/src/main/scala/dev/profunktor/redis4cats/algebra/hashes.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.algebra 18 | 19 | import dev.profunktor.redis4cats.data.{ KeyScanCursor, MapScanCursor } 20 | import dev.profunktor.redis4cats.effects.{ ExpireExistenceArg, HGetExArgs, ScanArgs } 21 | 22 | import java.time.Instant 23 | import scala.concurrent.duration.FiniteDuration 24 | 25 | trait HashCommands[F[_], K, V] 26 | extends HashGetter[F, K, V] 27 | with HashSetter[F, K, V] 28 | with HashIncrement[F, K, V] 29 | with HashExpire[F, K] 30 | with HashDelete[F, K, V] { 31 | def hExists(key: K, field: K): F[Boolean] 32 | } 33 | 34 | trait HashGetter[F[_], K, V] { 35 | def hGet(key: K, field: K): F[Option[V]] 36 | def hGetAll(key: K): F[Map[K, V]] 37 | def hGetEx(key: K, getExArg: HGetExArgs, field: K, fields: K*): F[List[Option[V]]] 38 | def hmGet(key: K, field: K, fields: K*): F[Map[K, V]] 39 | def hKeys(key: K): F[List[K]] 40 | def hVals(key: K): F[List[V]] 41 | def hStrLen(key: K, field: K): F[Long] 42 | def hLen(key: K): F[Long] 43 | def hScan(key: K): F[MapScanCursor[K, V]] 44 | def hScan(key: K, cursor: MapScanCursor[K, V]): F[MapScanCursor[K, V]] 45 | def hScan(key: K, scanArgs: ScanArgs): F[MapScanCursor[K, V]] 46 | def hScan(key: K, cursor: MapScanCursor[K, V], scanArgs: ScanArgs): F[MapScanCursor[K, V]] 47 | def hScanNoValues(key: K): F[KeyScanCursor[K]] 48 | def hScanNoValues(key: K, cursor: KeyScanCursor[K]): F[KeyScanCursor[K]] 49 | def hScanNoValues(key: K, scanArgs: ScanArgs): F[KeyScanCursor[K]] 50 | def hScanNoValues(key: K, cursor: KeyScanCursor[K], scanArgs: ScanArgs): F[KeyScanCursor[K]] 51 | } 52 | 53 | trait HashSetter[F[_], K, V] { 54 | def hSet(key: K, field: K, value: V): F[Boolean] 55 | def hSet(key: K, fieldValues: Map[K, V]): F[Long] 56 | def hSetNx(key: K, field: K, value: V): F[Boolean] 57 | @deprecated("In favor of hSet(key: K, fieldValues: Map[K, V])", since = "1.0.1") 58 | def hmSet(key: K, fieldValues: Map[K, V]): F[Unit] 59 | } 60 | 61 | trait HashExpire[F[_], K] { 62 | def hExpire(key: K, expiresIn: FiniteDuration, fields: K*): F[List[Long]] 63 | def hExpire(key: K, expiresIn: FiniteDuration, args: ExpireExistenceArg, fields: K*): F[List[Long]] 64 | def hExpireAt(key: K, expireAt: Instant, fields: K*): F[List[Long]] 65 | def hExpireAt(key: K, expireAt: Instant, args: ExpireExistenceArg, fields: K*): F[List[Long]] 66 | def hExpireTime(key: K, fields: K*): F[List[Option[Instant]]] 67 | def hpExpireTime(key: K, fields: K*): F[List[Option[Instant]]] 68 | def httl(key: K, fields: K*): F[List[Option[FiniteDuration]]] 69 | def hpttl(key: K, fields: K*): F[List[Option[FiniteDuration]]] 70 | def hPersist(key: K, fields: K*): F[List[Boolean]] 71 | } 72 | 73 | trait HashIncrement[F[_], K, V] { 74 | def hIncrBy(key: K, field: K, amount: Long): F[Long] 75 | def hIncrByFloat(key: K, field: K, amount: Double): F[Double] 76 | } 77 | 78 | trait HashDelete[F[_], K, V] { 79 | def hDel(key: K, field: K, fields: K*): F[Long] 80 | def hGetDel(key: K, field: K, fields: K*): F[List[Option[V]]] 81 | } 82 | -------------------------------------------------------------------------------- /modules/core/src/main/scala/dev/profunktor/redis4cats/effect/MkRedis.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-2025 ProfunKtor 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package dev.profunktor.redis4cats.effect 18 | 19 | import scala.annotation.implicitNotFound 20 | 21 | import cats.effect.kernel._ 22 | import dev.profunktor.redis4cats.connection.{ RedisClient, RedisClusterClient, RedisURI } 23 | import dev.profunktor.redis4cats.config.Redis4CatsConfig 24 | import dev.profunktor.redis4cats.tx.TxRunner 25 | import io.lettuce.core.ClientOptions 26 | 27 | /** MkRedis is a capability trait that abstracts over the creation of RedisClient, RedisClusterClient, among other 28 | * things. 29 | * 30 | * It serves the internal purpose to orchastrate creation of such instances while avoiding impure constraints such as 31 | * `Async` or `Sync`. 32 | * 33 | * Users only need a `MkRedis` constraint and `MonadThrow` to create a `Redis` instance. 34 | */ 35 | @implicitNotFound( 36 | "MkRedis instance not found. You can summon one by having instances for cats.effect.Async and dev.profunktor.redis4cats.effect.Log in scope" 37 | ) 38 | sealed trait MkRedis[F[_]] { 39 | def clientFrom(strUri: => String): Resource[F, RedisClient] 40 | def clientFromUri(uri: => RedisURI): Resource[F, RedisClient] 41 | def clientWithOptions(strUri: => String, opts: ClientOptions): Resource[F, RedisClient] 42 | def clientCustom( 43 | uri: => RedisURI, 44 | opts: ClientOptions, 45 | config: Redis4CatsConfig = Redis4CatsConfig() 46 | ): Resource[F, RedisClient] 47 | 48 | def clusterClient(uri: RedisURI*): Resource[F, RedisClusterClient] 49 | 50 | private[redis4cats] def txRunner: Resource[F, TxRunner[F]] 51 | private[redis4cats] def futureLift: FutureLift[F] 52 | private[redis4cats] def log: Log[F] 53 | private[redis4cats] def availableProcessors: F[Int] 54 | } 55 | 56 | object MkRedis { 57 | def apply[F[_]: MkRedis]: MkRedis[F] = implicitly 58 | 59 | implicit def forAsync[F[_]: Async: Log]: MkRedis[F] = 60 | new MkRedis[F] { 61 | private implicit val implicitThis: MkRedis[F] = this 62 | 63 | def clientFrom(strUri: => String): Resource[F, RedisClient] = 64 | RedisClient[F].from(strUri) 65 | 66 | def clientFromUri(uri: => RedisURI): Resource[F, RedisClient] = 67 | RedisClient[F].fromUri(uri) 68 | 69 | def clientWithOptions(strUri: => String, opts: ClientOptions): Resource[F, RedisClient] = 70 | RedisClient[F].withOptions(strUri, opts) 71 | 72 | def clientCustom( 73 | uri: => RedisURI, 74 | opts: ClientOptions, 75 | config: Redis4CatsConfig = Redis4CatsConfig() 76 | ): Resource[F, RedisClient] = 77 | RedisClient[F].custom(uri, opts, config) 78 | 79 | def clusterClient(uri: RedisURI*): Resource[F, RedisClusterClient] = 80 | RedisClusterClient[F](uri: _*) 81 | 82 | private[redis4cats] def txRunner: Resource[F, TxRunner[F]] = 83 | TxExecutor.make[F].map(TxRunner.make[F]) 84 | 85 | private[redis4cats] def futureLift: FutureLift[F] = implicitly 86 | 87 | private[redis4cats] def log: Log[F] = implicitly 88 | 89 | private[redis4cats] def availableProcessors: F[Int] = Async[F].blocking(Runtime.getRuntime.availableProcessors()) 90 | } 91 | 92 | } 93 | --------------------------------------------------------------------------------