- Python 95.2%
- Shell 4.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| .githooks | ||
| .github/workflows | ||
| scripts | ||
| src | ||
| tests | ||
| tools/pdisc | ||
| .gitignore | ||
| .pdisc.yaml | ||
| INDEX.md | ||
| LICENSE | ||
| project.trb | ||
| README.md | ||
| title | summary | keywords | type | updated | body_hash | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| torb/redis: Redis and Valkey client for TorbScript | README of the TorbScript client for Redis and Valkey: RESP2/RESP3, typed commands, pipelining, transactions, pub/sub and a connection pool. Read when using Redis from a TorbScript program. |
|
reference | 2026-10-07 | cc281caa6b52 |
torb/redis
A TorbScript client for Redis and Valkey: RESP2 and
RESP3 (negotiated with HELLO 3, falling back for a server older than Redis 6), typed commands for the common
families, pipelining, MULTI/EXEC transactions, pub/sub, and a small connection pool.
Quick start
use RedisClient from "torb/redis"
const client = RedisClient.open("redis://localhost:6379")?
using leased = client.acquire().await()?
leased.connection.set("greeting", "hello").await()?
print leased.connection.get("greeting").await()? // Some("hello")
A single connection, with no pool, is just as direct:
use RedisConnection from "torb/redis"
var connection = RedisConnection.open("redis://localhost:6379").await()?
connection.incr("visits").await()?
rediss:// connects over TLS; redis://user:secret@host:port/3 carries a username, a password and a database index,
the way the IANA redis URI scheme defines it.
What is here
-
Connecting:
RedisConnection.open(url)or.connect(options);AUTHandSELECTrun as part of the handshake, and RESP3 is used wherever the server answersHELLO 3. -
Typed commands, each an
extend RedisConnectioninsrc/commands/:- strings:
get,set(withSetCondition.IfNotExists/IfExistsandExpiry.Seconds/Milliseconds/KeepTtl),incr,incrBy,decr,decrBy,append,strlen,mget,mset. - keys:
del,exists,expire,pexpire,persist,ttl,pttl,keyType,rename,scan. - hashes:
hset,hget,hdel,hexists,hgetAll,hkeys,hvals,hlen,hincrBy,hmset. - lists:
lpush,rpush,lpop,rpop,lrange,llen,lindex,lrem,ltrim. - sets:
sadd,srem,smembers,sisMember,scard,sunion,sinter,sdiff. - sorted sets:
zadd,zscore,zincrBy,zrem,zrank,zcard,zrange,zrangeWithScores. - anything else:
connection.command(textCommand("OBJECT", ["ENCODING", "key"])).await()reaches every command this package has no typed wrapper for, and reads back the rawRespValue.
- strings:
-
Pipelining (
src/pipeline.trb):connection.pipeline()queues frames and reads every reply together.var pipeline = connection.pipeline() pipeline.queueCommand("SET", ["a", "1"]).await()? pipeline.queueCommand("INCR", ["a"]).await()? const replies = pipeline.execute().await()? -
Transactions (
src/transaction.trb):WATCH,MULTI, queued commands,EXEC(Nonewhere a watched key changed) orDISCARD.var transaction = connection.transaction() transaction.watch(["balance"]).await()? transaction.begin().await()? transaction.queueCommand("DECRBY", ["balance", "10"]).await()? match transaction.exec().await()? { Some(replies) => print replies None => print "balance changed under us; retry" } -
Pub/sub (
src/pubsub.trb): a connection given over entirely toSUBSCRIBE/PSUBSCRIBE.var subscription = connection.subscription() subscription.subscribeTo(["news"]).await()? loop { const message = subscription.next().await()? print "{message.channel}: {message.payload}" } -
A small pool (
src/pool.trb,src/client.trb):RedisClientkeeps at most a fixed number of connections to one server open, waiting for one to come back where more are asked for than that. -
The RESP model (
src/resp.trb):RespValuecovers every RESP2 and RESP3 type - a RESP3 big number is kept as its decimal digits, sincestd/numberhas no arbitrary-precision integer yet. -
Errors (
src/error.trb): oneRedisError, withisWrongType(),isNoAuth(),isMoved(),isAsk(),isLoading(),isBusy(),isReadonly(),isExecAbort()andisRetryable()reading a server's error code,Nilread asOption, never a null.
Developing
$ torb format --check . # the layout torb format writes
$ torb check . # types, exhaustiveness, everything torb check finds
$ torb test tests/unit # the codec and URL parsing, no server needed
$ torb doc --check . # every doc comment link resolves, every example runs
The integration suite needs a real server:
$ sh scripts/start-servers.sh # valkey/valkey:8-alpine on :16379, redis:7-alpine on :16380
$ torb test tests/integration
$ sh scripts/stop-servers.sh
torb format . writes the layout instead of only checking it. .github/workflows/ci.yml and
.forgejo/workflows/ci.yml run torb format --check, torb check, torb test tests/unit and torb doc --check on
every push and pull request, and a second job runs torb test tests/integration against Valkey and Redis service
containers.
Publishing
torb publish needs name, version, description and license in project.trb, and it refuses a version that
is published already - a published version is never replaced, so publishing again means raising version first.
Try it without uploading anything:
$ torb publish --dry-run
Publishing from CI (publish.yml) needs no token at all - trusted publishing. Before the first tag, an owner of
torb's account on the registry (packages.torb.dev) has to add a trusted publisher for torb/redis that names:
| Setting | GitHub Actions (.github/workflows/publish.yml) |
Forgejo Actions (.forgejo/workflows/publish.yml) |
|---|---|---|
| Repository | torb/redis |
git.torb.dev/torb/redis |
| Workflow file | publish.yml |
publish.yml |
| Ref | refs/tags/v* |
refs/tags/v* |
| Environment | release (or delete the environment: line of the workflow to name none) |
none - a Forgejo token never names one |
Once that is configured, pushing a tag v0.1.0 (matching the version in project.trb) runs publish.yml, which
installs torb and runs torb publish with an identity token instead of a secret.
Files
redis/
├─ project.trb name, version, description, license
├─ src/
│ ├─ lib.trb the public surface: "torb/redis" resolves to this file
│ ├─ resp.trb RespValue and the command frame
│ ├─ error.trb RedisError
│ ├─ url.trb ConnectOptions.tryFrom, the redis://rediss:// URL
│ ├─ transport.trb TCP or TLS, one type for both
│ ├─ connection.trb RedisConnection: the handshake and readReply
│ ├─ reply.trb RespValue -> the type a command promises
│ ├─ commands/ one file per family, each an extend RedisConnection
│ ├─ pipeline.trb, transaction.trb, pubsub.trb
│ ├─ pool.trb, client.trb RedisPool, PooledConnection, RedisClient
├─ tests/
│ ├─ unit/ the codec and URL parsing; no server needed
│ └─ integration/ against real Valkey and Redis (scripts/start-servers.sh)
├─ scripts/ start-servers.sh, stop-servers.sh
├─ .github/workflows/ ci.yml (push, pull request), publish.yml (tag v*)
├─ .forgejo/workflows/ the same, for git.torb.dev
├─ LICENSE MIT
└─ .gitignore
Related
- torb/sql and torb/postgres - the other first-party clients;
docs/design/FIRST-PARTY.mdof the language repository has the shape they, and this package, follow. - torb publish - what publishing checks, and trusted publishing in full.
- The RESP protocol: https://redis.io/docs/latest/develop/reference/protocol-spec/.