// STRYKE-REDIS — REDIS / VALKEY CLIENT FOR STRYKE // KV + LISTS + SETS + HASHES + ZSETS + PUB/SUB + SCAN

stryke package · cdylib libstryke_redis.{dylib,so} · dlopen'd in-process on use Redis · opt-in (kept out of stryke core)

Report GitHub Issues
// Color scheme

>_STRYKE-REDIS

In-memory state, one pipe away. Redis / Valkey client for stryke. KV, lists, sets, hashes, sorted sets, streams, geospatial, scripting, bitmaps, HyperLogLog, pub/sub, pipeline, scan, server admin. Opt-in package — v0.19.0.

184 public Redis::* functions: 166 connection-backed commands (one redis__* FFI symbol each) plus 18 pure helpers that run with no round trip. Backed by the redis Rust crate (sync) with rustls TLS. Works against Redis, Valkey, KeyDB, and Dragonfly.

Contents

Install

# from a release (no rustc on the consumer machine)
s pkg install -g github.com/MenkeTechnologies/stryke-redis

# from a local checkout
cd ~/projects/stryke-redis
cargo build --release            # produces target/release/libstryke_redis.{dylib,so}
s pkg install -g .               # cdylib lands in ~/.stryke/store/redis@<version>/

# one-liner
make install

The cdylib is dlopened in-process on first use Redis — no helper-binary fork per call. Any stryke script that declares use Redis resolves the package automatically.

-g installs globally with no project requirement — it means "no project needed", not a redundant store marker. s pkg install without -g resolves the current project's stryke.toml dependencies and is unrelated to installing this package itself. After install the cdylib lands in ~/.stryke/store/redis@<version>/.

Quick start: use Redis

use Redis

# Connection is read from $REDIS_URL, else defaults to 127.0.0.1:6379.
$ENV{REDIS_URL} = "redis://localhost:6379/0"

Redis::set "greeting", "hello from stryke"
p Redis::get "greeting"

Redis::mset { a => 1, b => 2, c => 3 }
p Redis::mget ["a", "b", "c"]              # ("1","2","3")

# TTL.
Redis::set "session:42", "...", ex => 3600
p Redis::ttl "session:42"

# Counters.
val $n = Redis::incr "page-views", by => 5

# Lists / queues.
Redis::lpush "events", ["a", "b", "c"]
val @recent = Redis::lrange "events", 0, 9
val $next   = Redis::rpop  "events"        # FIFO consumption

# Sets / dedup.
Redis::sadd "seen-users", ["alice", "bob"]
p Redis::sismember "seen-users", "alice"   # 1

# Hash = JSON-friendly objects.
Redis::hmset "user:42", { name => "alice", role => "admin", score => 100 }
val $u = Redis::hgetall "user:42"
p "$u->{name} ($u->{role})"

# Sorted set = leaderboard.
Redis::zadd "leaderboard", { alice => 100, bob => 200 }
val @top = Redis::zrange "leaderboard", 0, 9, withscores => 1, rev => 1

# Pub/sub publish.
Redis::publish "events", "new user signed up"

# SCAN — non-blocking iteration.
val @keys = Redis::scan match => "session:*", count => 100

# Server.
p to_json Redis::info section => "memory"
p Redis::dbsize
Redis::flushdb confirm => 1                # destructive — must pass confirm flag

The complete surface is documented in the API reference below. Source of truth: lib/Redis.stk in this repo.

Connection options

Every Redis::* op accepts %opts as its final argument. When %opts carries no connection fields, the connection is read from $REDIS_URL, else it defaults to 127.0.0.1:6379, db 0. The cdylib caches one redis::Connection per (url, db, tls, username, password) tuple for the life of the process — back-to-back calls with the same options reuse the same TCP socket, no handshake-per-call.

urlredis://… or rediss://… (TLS). Takes precedence over the host/port parts.
hostdefault 127.0.0.1
portdefault 6379
dbdefault 0
usernamedefault "" (ACL user)
passworddefault ""
tls1 / 0 — force TLS without a redis://rediss:// rewrite
# inline overrides on any op
Redis::set "rate-limit:user-42", "1",
    url => "rediss://prod:secret@host:6380/0", ex => 60, nx => 1

# compose from parts
val %prod = ( host => "prod.example.com", port => 6380, db => 1, password => "secret", tls => 1 )
Redis::get "foo", %prod

# or set once in the environment
$ENV{REDIS_URL} = "redis://localhost:6379/0"
val %conn = ( url => $ENV{REDIS_URL} )
Redis::set "k", "v", %conn

API reference (full surface)

All 166 connection-backed commands, grouped by Redis data type. Each name is a Redis::<name> function whose final argument is an optional %opts dict (connection fields plus per-command flags). Pure helpers that need no connection are listed separately.

KV & strings

get · set (opts: ex, px, nx, xx) · del · exists · expire · ttl (-2 missing, -1 no ttl) · type · incr / decr (opts: by) · incrbyfloat · mget · mset · keys (blocking) · scan (opts: match, count, limit) · sort (opts: by, get, alpha, desc, limit_*, store) · getset · getdel · getex (opts: ex, px, persist) · append · strlen · setex · setnx · getrange · setrange

Bitmaps

setbit · getbit · bitcount (opts: start, end) · bitop (AND|OR|XOR|NOT) · bitpos · lcs (longest common subsequence; opts: len)

Lists

lpush / rpush · lpop / rpop (opts: count) · lrange · llen · lindex · lset · linsert (opts: before) · lrem · ltrim · rpoplpush · lmove (opts: from, to) · lpos (opts: rank, count) · lmpop (opts: from LEFT|RIGHT, count)

Sets

sadd · srem · smembers · sismember · smismember · scard · spop (opts: count) · srandmember (opts: count) · smove · sinter / sunion / sdiff · sinterstore / sunionstore / sdiffstore · sintercard (opts: limit)

Hashes

hset · hget · hdel · hgetall · hkeys · hvals · hmget · hmset · hexists · hincrby (opts: float) · hincrbyfloat · hlen · hsetnx · hstrlen · hrandfield (opts: count, with_values)

Sorted sets

zadd · zrange (opts: withscores, rev) · zrem · zcard · zscore · zmscore · zincrby · zrank (opts: rev) · zcount (opts: min, max) · zrangebyscore · zrangebylex · zlexcount · zpopmin / zpopmax (opts: count) · zremrangebyrank · zremrangebyscore · zunionstore / zinterstore / zdiffstore (opts: weights, aggregate) · zunion / zinter / zdiff (opts: with_scores) · zrandmember (opts: count, with_scores) · zmpop (opts: from MIN|MAX, count)

Key management

rename · renamenx · persist · pexpire · pttl · expireat · pexpireat · expiretime · randomkey · touch · unlink (async del) · copy (opts: replace, destination_db) · object_encoding

HyperLogLog

pfadd · pfcount · pfmerge

Streams

xadd (opts: id, default "*") · xlen · xrange (opts: start, end, count, rev) · xdel · xtrim (opts: maxlen | minid) · xsetid · xread (opts: count, block) · xgroup_create (opts: id, mkstream) · xack · xinfo_stream

Geospatial

geoadd · geopos · geodist (opts: unit m/km/mi/ft) · geosearch (from_member | lon+lat; radius | width+height)

Scripting (Lua)

eval (opts: keys, args) · evalsha · script_load · script_exists

Cursor scans (iterate internally, return everything)

hscan · sscan · zscan (each: opts match, count)

Pub/sub

publish · pubsub_channels (opts: pattern) · pubsub_numsub · pubsub_numpat

Streaming SUBSCRIBE is on the roadmap, pending a stryke Unix-socket reader builtin — only publish ships in this release.

Pipeline / transaction

pipeline ([[CMD,…],…]; opts: transaction for MULTI/EXEC)

Server, admin & introspection

ping · version · info (opts: section; pair with Redis::parse_info) · dbsize · flushdb (require confirm => 1) · flushall (opts: async) · raw (arbitrary command argv) · time · config_get / config_set · memory_usage · echo · wait (opts: timeout) · lastsave · slowlog_get (opts: count) · slowlog_reset · client_list · client_info · acl_whoami · acl_list · acl_cat (opts: category) · object_idletime · object_refcount · object_freq

Full per-argument signatures and return shapes are in the README API reference.

Pure helpers (no connection)

These 18 functions run entirely client-side — no TCP round trip. They parse Redis reply text, manipulate connection URLs, match keys against Redis globs, compute cluster key slots, and order stream IDs.

parse_info($info)structure an INFO reply into { sections, fields, names } (values kept raw)
parse_client_info($info)structure a CLIENT INFO / CLIENT LIST reply into { clients:[…] }
parse_cluster_nodes($nodes)structure a CLUSTER NODES reply (id, host, port, flags, role, slots, …)
parse_url($url)redis[s]://…{ scheme, tls, user, password, host, port, db }
build_url(%opts)parts → redis[s]:// URL; inverse of parse_url
redact_url($url)mask the password (user:***@) so a URL is safe to log
glob_match($pattern, $key)Redis KEYS/SCAN glob matched client-side (faithful port of stringmatchlen)
glob_escape($value)backslash-escape * ? [ ] \ so a value matches literally
glob_unescape($escaped)inverse of glob_escape
glob_to_regex($pattern)Redis glob → anchored regex with the same semantics
cluster_keyslot($key)CLUSTER KEYSLOT: crc16(hash_tag(key)) % 16384, honors {…} tags
same_slot($a, $b)do two keys co-locate? (avoid CROSSSLOT in multi-key ops)
parse_stream_id($id)stream entry id <ms>-<seq>{ ms, seq, special }
build_stream_id(%opts)inverse of parse_stream_id
stream_id_range($start_ms, $end_ms)inclusive XRANGE window by time
compare_stream_id($a, $b)order two stream ids by (ms, seq)-1|0|1
next_stream_id($id)successor entry id (exclusive XRANGE bound)
prev_stream_id($id)predecessor entry id (exclusive XREVRANGE bound)

Use glob_match to filter keys locally without a round trip, and redact_url before logging any connection string.

Examples

Runnable .stk scripts live in examples/. Each is eval-guarded on Redis::ping so it exits 0 in CI even without a live server.

examples/kv.stksimple KV demo (set / get)
examples/structures.stklists, sets, hashes, sorted sets
examples/publish.stkemit a Pub/Sub message: publish.stk CHANNEL MESSAGE
examples/pipeline.stkTTL + MSET/MGET + counters (rate-limit-style idioms)
examples/discover.stkminimum-viable tour; round-trips on a process-unique key when a server is up
# run any example with a live server
REDIS_URL=redis://localhost s examples/kv.stk
REDIS_URL=redis://localhost s examples/structures.stk
s examples/publish.stk my-channel 'hello'

Tests & dev workflow

cargo test                                    # compiles the cdylib, no live calls
REDIS_URL=redis://localhost s test t/         # live round-trip suite

Test keys are scoped under stryke:test:$$: and cleaned at exit, so concurrent CI runs don't collide. Spin up a local server with brew services start redis or docker run --rm -p 6379:6379 redis:7.

make             # release build (target/release/libstryke_redis.{dylib,so})
make debug
make test
make install
make clean

Troubleshooting

Connection refusedNo server on the resolved address. Check $REDIS_URL, or start one (brew services start redis / docker run -p 6379:6379 redis:7).
Wrong host / dbConnection comes from %opts first, then $REDIS_URL, then 127.0.0.1:6379 db 0. Pass url => … or db => … on the op, or export REDIS_URL.
TLS handshake fails on a redis:// URLUse a rediss:// URL, or pass tls => 1 to force TLS without rewriting the scheme.
AUTH requiredSupply password => … (and username => … for ACL users) in %opts, or embed them in the URL.
flushdb / flushall seems to do nothingflushdb requires confirm => 1 — a guard against accidental wipes.
Op dies with a messageThe cdylib returns {"error": "<msg>"} on failure and the wrapper dies with it; the message is the Redis-side error verbatim.
Stale password printed in logsPass any connection URL through Redis::redact_url before logging it.

Why a package, not a builtin

redis Rust crate (sync) with rustls TLS. Single highest-leverage daily-use connector after Postgres / MySQL.

The stryke side is a thin .stk wrapper that calls redis__* FFI symbols on the cdylib; the heavy code lives in libstryke_redis.{dylib,so}, dlopened in-process on first use Redis. Core stryke is never linked against this package's deps.

FFI layer

Each Redis::* wrapper builds a JSON args dict and calls a sibling redis__* symbol resolved out of libstryke_redis.{dylib,so}. The cdylib is dlopened in-process on first use Redis and caches one redis::Connection per (url, db, tls, username, password) tuple in OnceCell<Mutex<HashMap>> for the life of the stryke process — back-to-back calls reuse the same TCP socket, no handshake-per-call. There are 184 exported redis__* symbols (one per public function), declared in the [ffi] table of stryke.toml and matched exactly by the extern "C" definitions in src/lib.rs.

Wire shape — JSON in, JSON out:

scalars                 → {"value": …}
lists / sets / values   → {"values": [...]} | {"members": [...]}
hashes                  → {"hash": {...}}
zset rows with scores   → {"pairs": [[member, score], ...]}
errors                  → {"error": "<msg>"}   # wrapper die()s with it

The returned CString is freed via the cdylib-exported stryke_free_cstring, wired automatically by stryke's rust_ffi::load_cdylib — closing the returned-allocation leak the inline-FFI v1 had.

Layout

stryke-redis/
├── Cargo.toml             # cdylib crate manifest (crate-type = ["cdylib"], publish = false)
├── src/
│   └── lib.rs            # cdylib — redis__* extern "C" exports + persistent conn cache
├── lib/                   # stryke .stk wrapper(s) — `use Redis`
├── stryke.toml            # stryke package manifest ([ffi] exports table)
├── t/                     # zunit-style tests
├── examples/              # runnable .stk examples
├── Makefile               # `make install` builds + installs
└── docs/                  # this site (GitHub Pages)

Sibling packages

Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo: