Client

laserdisc-fs2 gives you RedisClient, which connects to a server and sends commands.

Connecting

RedisClient[IO].to(host, port) returns a Resource. The connection opens when you use it and closes when you're done:

import cats.effect.{IO, Resource}

import laserdisc.*
import laserdisc.auto.*
import laserdisc.fs2.*

val client: Resource[IO, RedisClient[IO]] = RedisClient[IO].to("localhost", 6379)

If the connection drops, commands still waiting for a reply get a Left right away, and the client reconnects. While it can't reach the server, it tries again every 3 seconds, and commands sent in the meantime fail with ServerUnavailable.

The client also reconnects when the server stops replying: once a reply is late by more than its send's timeout, the client drops the connection and opens a new one.

Several servers

RedisClient[IO].toNodes takes a set of addresses. The client uses one server at a time. If it can't reach that server, or the connection drops, it moves on to the next one:

import cats.effect.{IO, Resource}

import laserdisc.*
import laserdisc.auto.*
import laserdisc.fs2.*

val client: Resource[IO, RedisClient[IO]] =
  RedisClient[IO].toNodes(Set(RedisAddress("redis-1", 6379), RedisAddress("redis-2", 6379)))

When none of the servers can be reached, it waits 3 seconds before trying them all again.

This doesn't make it a Redis Cluster client: laserdisc doesn't send each key to the node that holds it, or follow a cluster's MOVED and ASK redirects. On a cluster, a command for a key held by another node fails with a MOVED error.

Sending commands

send takes from 1 to 22 commands and sends them together. You get back one result per command, in the same order:

import cats.effect.IO

import laserdisc.*
import laserdisc.redis_8_10.*
import laserdisc.auto.*
import laserdisc.fs2.*

def setAndGet(client: RedisClient[IO]): IO[(Maybe[OK], Maybe[Option[PosInt]])] =
  client.send(set("a", 23), get[PosInt]("a"))

Each result is a Maybe[A], which is an Either[Throwable, A]. It's a Left when the server answers with an error or when the reply can't be read as the type you asked for, so one failed command doesn't affect the others.

Timeouts

A send times out if its replies haven't all arrived within 20 seconds. To choose another timeout, pass it after the commands:

import cats.effect.IO
import scala.concurrent.duration.*

import laserdisc.*
import laserdisc.redis_8_10.*
import laserdisc.auto.*
import laserdisc.fs2.*

def quickGet(client: RedisClient[IO]): IO[Maybe[Option[String]]] =
  client.send(get[String]("a"), 2.seconds)

When it times out, the whole send fails with RequestTimedOut, which names the first command still without a reply. Commands that were still waiting to be sent aren't sent at all.

Loops

Some commands are meant to be sent again and again. SCAN replies with a cursor to send back until the server is done, XRANGE reads a stream a page at a time, and XREAD can follow a stream as entries are added. For these there are loops: scanAll, hscanAll, hscannovaluesAll, sscanAll, zscanAll, zscannoscoresAll, xrangeAll and xrevrangeAll, and, with the blocking commands, xreadFollow and xreadgroupFollow. stream runs a loop and gives you its values as a Stream:

import cats.effect.IO

import laserdisc.*
import laserdisc.redis_8_10.*
import laserdisc.auto.*
import laserdisc.fs2.*

def userKeys(client: RedisClient[IO]): Stream[IO, Key] =
  client.stream(scanAll("user:*"))

Each command is sent once the reply to the one before it is in. A failed command fails the stream. Each command gets the same timeout as a send, or the one you pass after the loop, and for a blocking command its wait is added to it.

To make your own loop, give Loop where to start, the command to send from there, and what to do with the reply: the values it gives, and where to go next, or None to stop. This one reads a list 100 items at a time:

import laserdisc.*
import laserdisc.redis_8_10.*

def wholeList(key: Key): Loop[String] =
  Loop(0L)(start => lrange[String](key, Index.unsafeFrom(start), Index.unsafeFrom(start + 99))) { (start, page) =>
    page -> (if (page.size < 100) None else Some(start + 100))
  }

For blocking commands, use Loop.blocking with how long each one waits.

Blocking commands

Blocking commands, under blocking, wait on the server for something to happen. While one waits, it holds the client's connection: commands sent after it on the same client, from anywhere in your program, only get their replies once it's done. Give blocking commands, and loops that follow streams, a client of their own.

Logging

The client logs through log-effect. To see its logs, put a LogWriter in implicit scope, for example SyncLogWriter.consoleLog[IO]. Without one, the client doesn't log.