Pub/sub publish (subscribe is streaming
August 2, 2026 · View on GitHub
███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝
███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗
╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝
███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝
[ r e d i s ]
[REDIS / VALKEY CLIENT FOR STRYKE // KV + LISTS + SETS + HASHES + ZSETS + STREAMS + GEO + SCRIPTING + BITMAPS + HLL + PUB/SUB + PIPELINE + SCAN]
"In-memory state, one pipe away."
Redis / Valkey client for stryke. KV, lists, sets, hashes, sorted sets, streams, geospatial, scripting, bitmaps, HyperLogLog, pub/sub publish, pipeline, scan, server admin. Opt-in package tier.
strykelang · MenkeTechnologiesMeta · stryke-kafka · stryke-mongo · stryke-postgres · stryke-demo
Read the Docs · Engineering Report
Table of Contents
- [0x00] Why this is one of the most useful stryke packages
- [0x01] Install
- [0x02] Quick start
- [0x03] Connection options
- [0x04] API reference (selected)
- [0x05] FFI layer
- [0x06] Tests
- [0x07] Dev workflow
- [0x08] Layout
- [0x09] Roadmap
- [0xFF] License
[0x00] Why this is one of the most useful stryke packages
A Redis client is the single highest-leverage missing daily-use integration. Caches, queues, deduplication sets, leaderboards, rate limiters, pub/sub fanout — all of it lives in Redis or a Redis-compatible key-value store (Valkey, KeyDB, Dragonfly). This package gives stryke direct access in one syscall per operation:
use Redis
Redis::set "rate-limit:user-42", "1", ex => 60, nx => 1
[0x01] Install
From a release (no rustc needed on the consumer machine — works from any directory, no project needed):
s pkg install -g github.com/MenkeTechnologies/stryke-redis
From a local checkout (publisher / contributor workflow — builds the
cdylib via cargo, then installs into ~/.stryke/store/redis@<version>/):
cd ~/projects/stryke-redis
cargo build --release
s pkg install -g .
Or:
make install
-g installs globally with no project requirement. s pkg install
without -g resolves the current project's stryke.toml deps and is
unrelated to installing this package itself.
[0x02] Quick start
use Redis
# Connection is detected from $REDIS_URL or 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 # 1h
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 (subscribe is streaming — coming in v2 once stryke has
# a unix-socket reader builtin).
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
Connection overrides on every public fn (%opts):
val %prod = (
url => "rediss://default:secret@prod.example.com:6379/0",
)
Redis::get "foo", %prod
Or compose from parts: host, port, password, username, db, tls.
[0x03] Connection options
Every Redis::* op accepts %opts as its final argument. Connection
fields the cdylib understands (matching the v1 helper-binary flags):
url → redis://… or rediss://… (TLS)
host → 127.0.0.1
port → 6379
db → 0
username → ""
password → ""
tls → 1 / 0 — force TLS without redis:// → rediss:// rewrite
Inline:
Redis::set "rate-limit:user-42", "1",
url => "rediss://prod:secret@host:6380/0", ex => 60, nx => 1
Or set once in the environment:
export REDIS_URL=redis://localhost:6379/0
use Redis
$ENV{REDIS_URL} = "redis://localhost:6379/0"
val %conn = (url => $ENV{REDIS_URL})
Redis::set "k", "v", %conn
The cdylib caches one redis::Connection per (url, db, auth) tuple
for the life of the process — back-to-back calls with the same connection
options reuse the same TCP socket. No handshake-per-call.
[0x04] API reference (selected)
# KV
Redis::get $key, %opts → $value | undef
Redis::set $key, $value, %opts → 1 | "" # opts: ex, px, nx, xx
Redis::del $keys_or_aref, %opts → $count
Redis::exists $keys_or_aref, %opts → $count
Redis::expire $key, $seconds, %opts → 1 | 0
Redis::ttl $key, %opts → $seconds # -2 missing, -1 no ttl
Redis::type $key, %opts → "string"|"list"|"set"|"zset"|"hash"|"stream"|"none"
Redis::incr $key, %opts → $new # opts: by
Redis::decr $key, %opts → $new # opts: by
Redis::mget \@keys, %opts → @values
Redis::mset \%pairs_or_aref, %opts → \%resp
Redis::keys $pattern, %opts → @keys # ⚠️ blocking
Redis::scan %opts → @keys # opts: match, count, limit
# Lists
Redis::lpush $key, $val_or_aref, %opts → $len
Redis::rpush $key, $val_or_aref, %opts → $len
Redis::lpop $key, %opts → $value | @values # opts: count
Redis::rpop $key, %opts → $value | @values
Redis::lrange $key, $start, $stop, %opts → @values
Redis::llen $key, %opts → $len
# Sets
Redis::sadd $key, $m_or_aref, %opts → $added
Redis::srem $key, $m_or_aref, %opts → $removed
Redis::smembers $key, %opts → @members
Redis::sismember $key, $member, %opts → 1 | 0
Redis::scard $key, %opts → $size
# Hashes
Redis::hset $key, $field, $value, %opts → 1 | 0
Redis::hget $key, $field, %opts → $value | undef
Redis::hdel $key, $fields_or_aref, %opts → $count
Redis::hgetall $key, %opts → \%hash
Redis::hkeys $key, %opts → @fields
Redis::hvals $key, %opts → @values
Redis::hmget $key, \@fields, %opts → @values
Redis::hmset $key, \%pairs_or_aref, %opts → \%resp
# Sorted sets
Redis::zadd $key, \%pairs_or_aref, %opts → $added
# { member => score, … } or [score, member, …]
Redis::zrange $key, $start, $stop, %opts → @values | @{ {member, score}, … }
# opts: withscores, rev
Redis::zrem $key, $m_or_aref, %opts → $removed
Redis::zcard $key, %opts → $size
Redis::zscore $key, $member, %opts → $score | undef
# Key management
Redis::rename $key, $newkey, %opts → 1 # renamenx → 1|0
Redis::persist $key, %opts → 1 | 0
Redis::pexpire $key, $millis, %opts → 1 | 0 # pttl → $ms; expiretime → $unix_s
Redis::expireat $key, $unix_seconds, %opts → 1 # pexpireat → ms variant
Redis::touch $keys_or_aref, %opts → $count # unlink → async del
Redis::copy $src, $dst, %opts → 1 | 0 # opts: replace, destination_db
Redis::randomkey %opts → $key | undef
Redis::object_encoding $key, %opts → $encoding | undef
Redis::sort $key, %opts → @elements | $stored # opts: by, get(aref), alpha, desc, limit_*, store
# String extras + bitmaps
Redis::getset $key, $value, %opts → $old # getdel → get+del
Redis::append $key, $value, %opts → $len # strlen → $len
Redis::setex $key, $seconds, $value, %opts → 1 # opts: px (ms)
Redis::setnx $key, $value, %opts → 1 | 0
Redis::getrange $key, $start, $end, %opts → $substr # setrange → $len
Redis::incrbyfloat $key, $by, %opts → $new
Redis::setbit $key, $offset, $bit, %opts → $prior # getbit → bit
Redis::bitcount $key, %opts → $n # opts: start, end
Redis::bitop $op, $dst, $keys_or_aref, %opts → $len # AND|OR|XOR|NOT
Redis::bitpos $key, $bit, %opts → $pos | -1 # opts: start, end (byte offsets)
Redis::lcs $key1, $key2, %opts → $lcs | $len # opts: len (longest common subsequence)
# List extras
Redis::lindex $key, $index, %opts → $value | undef
Redis::lset $key, $index, $value, %opts → 1
Redis::linsert $key, $pivot, $value, %opts → $len # opts: before
Redis::lrem $key, $count, $value, %opts → $removed
Redis::ltrim $key, $start, $stop, %opts → 1
Redis::rpoplpush $src, $dst, %opts → $value # lmove → opts: from, to
# Hash extras
Redis::hexists $key, $field, %opts → 1 | 0
Redis::hincrby $key, $field, $by, %opts → $new # opts: float
Redis::hlen $key, %opts → $count
Redis::hsetnx $key, $field, $value, %opts → 1 | 0
Redis::hstrlen $key, $field, %opts → $len # length of the field's value
Redis::hrandfield $key, %opts → $field | @fields | @pairs # opts: count, with_values
Redis::hincrbyfloat $key, $field, $by, %opts → $new # fractional hash increment
# Set algebra
Redis::spop $key, %opts → $member | @members # opts: count
Redis::srandmember $key, %opts → $member | @members # opts: count
Redis::smove $src, $dst, $member, %opts → 1 | 0
Redis::sinter $keys_or_aref, %opts → @members # sunion, sdiff
Redis::sinterstore $dst, $keys_or_aref, %opts → $card # sunionstore, sdiffstore
# Sorted set extras
Redis::zincrby $key, $by, $member, %opts → $new
Redis::zrank $key, $member, %opts → $rank | undef # opts: rev
Redis::zcount $key, %opts → $n # opts: min, max
Redis::zrangebyscore $key, %opts → @values | @pairs # opts: min,max,with_scores,rev,limit_*
Redis::zrangebylex $key, %opts → @values # opts: min,max,rev,limit_* (bounds: [a (b - +)
Redis::zlexcount $key, %opts → $n # opts: min, max (lexicographic)
Redis::zpopmin $key, %opts → @pairs # zpopmax; opts: count
Redis::zremrangebyrank $key, $start, $stop, %opts → $removed
Redis::zremrangebyscore $key, %opts → $removed # opts: min, max
Redis::zmscore $key, $members_or_aref, %opts → @scores
Redis::zunionstore $dst, $keys_or_aref, %opts → $card # opts: weights, aggregate; zinterstore
Redis::zdiffstore $dst, $keys_or_aref, %opts → $card
Redis::zunion $keys_or_aref, %opts → @members | @pairs # opts: weights, aggregate, with_scores; zinter
Redis::zdiff $keys_or_aref, %opts → @members | @pairs # opts: with_scores
Redis::zrandmember $key, %opts → $member | @members | @pairs # opts: count, with_scores
# HyperLogLog
Redis::pfadd $key, $elements_or_aref, %opts → 1 | 0
Redis::pfcount $keys_or_aref, %opts → $approx_card
Redis::pfmerge $dst, $sources_or_aref, %opts → 1
# Streams
Redis::xadd $key, \%fields, %opts → $id # opts: id (default "*")
Redis::xlen $key, %opts → $count
Redis::xrange $key, %opts → @entries # opts: start,end,count,rev
Redis::xdel $key, $ids_or_aref, %opts → $removed
Redis::xtrim $key, %opts → $removed # opts: maxlen | minid
Redis::xsetid $key, $id, %opts → 1 # set the stream's last-generated id
Redis::xread \%streams, %opts → \%resp # {key=>id}; opts: count, block
# Geospatial
Redis::geoadd $key, [[lon,lat,name],…], %opts → $added
Redis::geopos $key, $members_or_aref, %opts → @[lon,lat]
Redis::geodist $key, $m1, $m2, %opts → $dist # opts: unit (m/km/mi/ft)
Redis::geosearch $key, %opts → @results # from_member|lon+lat; radius|width+height
# Scripting
Redis::eval $script, %opts → $result # opts: keys, args (arefs)
Redis::evalsha $sha, %opts → $result # opts: keys, args
Redis::script_load $script, %opts → $sha1
Redis::script_exists $shas_or_aref, %opts → @bools
# Pub/sub + server
Redis::publish $channel, $message, %opts → $subscriber_count
Redis::pubsub_channels %opts → @channels # opts: pattern
Redis::pubsub_numsub $channels_or_aref, %opts → \%counts
Redis::pubsub_numpat %opts → $count # unique pattern subscriptions
Redis::info %opts → $info # raw INFO string; opts: section (parse with Redis::parse_info)
Redis::dbsize %opts → $count
Redis::flushdb %opts → \%resp # require confirm => 1
Redis::flushall %opts → 1 # opts: async
Redis::time %opts → ($seconds, $micros)
Redis::config_get $parameter, %opts → \%config # config_set → 1
Redis::memory_usage $key, %opts → $bytes | undef
Redis::echo $message, %opts → $message
Redis::ping %opts → 1 | ""
Redis::raw \@argv, %opts → \%resp # arbitrary command
# Cursor scans (iterate internally; return everything)
Redis::hscan $key, %opts → %hash # opts: match, count
Redis::sscan $key, %opts → @members # opts: match, count
Redis::zscan $key, %opts → @pairs # opts: match, count
# Stream consumer groups
Redis::xgroup_create $key, $group, %opts → 1 # opts: id (default $), mkstream
Redis::xack $key, $group, $ids_or_aref, %opts → $count
Redis::xinfo_stream $key, %opts → \%info
# Pipeline / transaction
Redis::pipeline [[CMD,…],…], %opts → @results # opts: transaction (MULTI/EXEC)
# Server admin + introspection
Redis::wait $numreplicas, %opts → $acked # opts: timeout (ms)
Redis::lastsave %opts → $unix_time
Redis::slowlog_get %opts → \@entries # opts: count
Redis::slowlog_reset %opts → 1
Redis::client_list %opts → $text # one line per client
Redis::client_info %opts → $text
Redis::acl_whoami %opts → $username
Redis::acl_list %opts → @rules
Redis::acl_cat %opts → @categories # opts: category
Redis::object_idletime $key, %opts → $seconds | undef
Redis::object_refcount $key, %opts → $n | undef
Redis::object_freq $key, %opts → $freq | undef # LFU access counter (requires lfu maxmemory-policy)
# Redis 6.2 / 7.x
Redis::getex $key, %opts → $value | undef # opts: ex, px, persist
Redis::smismember $key, $members_or_aref, %opts → @bools
Redis::sintercard $keys_or_aref, %opts → $n # opts: limit
Redis::lpos $key, $element, %opts → $idx | @idxs # opts: rank, count
Redis::lmpop $keys_or_aref, %opts → [key, [elems]] | undef # opts: from (LEFT|RIGHT), count
Redis::zmpop $keys_or_aref, %opts → [key, [[m,score]]] | undef # opts: from (MIN|MAX), count
Pure helpers (no connection)
Redis::parse_info($info) → { sections:{Section:{field:value}}, fields:{field:value}, names:[...] } # structure an INFO reply (values kept raw)
Redis::parse_client_info($info) → { clients:[{field:value}] } # structure a CLIENT INFO/LIST reply (space-separated field=value per line)
Redis::parse_cluster_nodes($nodes) → { nodes:[{id,host,port,cport,hostname,flags,role,master,slots:[[s,e]],importing,migrating,…}] } # structure a CLUSTER NODES reply
Redis::parse_url($url) → { scheme, tls, user, password, host, port, db } # redis[s]://…
Redis::build_url(%opts) → $url # parts → redis[s]:// URL; inverse of parse_url (opts: host, port, db, user, password, tls)
Redis::redact_url($url) → $url # mask the password (user:***@) so the URL is safe to log; rest preserved
Redis::glob_match($pattern, $key) → 1 | "" # Redis KEYS/SCAN glob (* ? [a-z] [^…] \), matched client-side
Redis::glob_escape($value) → $escaped # backslash-escape * ? [ ] \ so glob_match treats $value literally
Redis::glob_unescape($escaped) → $value # inverse of glob_escape: \* \? \[ \] \\ → literal chars (single left-to-right scan)
Redis::glob_to_regex($pattern) → $regex # Redis glob → anchored regex (* → .*, ? → ., [..] class, \x literal); same semantics as glob_match
Redis::cluster_keyslot($key) → { key, slot, hash_tag } # CLUSTER KEYSLOT: crc16(hash_tag(key)) % 16384, honors {…} hash tags
Redis::same_slot($a, $b) → { a, b, slot_a, slot_b, same_slot } # do two keys co-locate (avoid CROSSSLOT in multi-key ops)?
Redis::parse_stream_id($id) → { ms, seq, special } # stream entry id <ms>-<seq>; special - + $ * → min/max/last/auto
Redis::build_stream_id(%opts) → $id # { ms, seq? } or { special } → stream entry id; inverse of parse_stream_id
Redis::stream_id_range($start_ms, $end_ms) → \%{ start, end, start_ms, end_ms } # inclusive XRANGE window by time: "<start_ms>-0" .. "<end_ms>-<u64 max>" (every entry in end_ms included)
Redis::compare_stream_id($a, $b) → -1|0|1 # order two stream ids by (ms, seq); bare <ms> → seq 0; - + below/above; $ * die
Redis::next_stream_id($id) → $id # successor of a concrete entry id (exclusive XRANGE bound); seq+1, rolls <ms>-MAX → <ms+1>-0
Redis::prev_stream_id($id) → $id # predecessor (exclusive XREVRANGE bound); seq-1, borrows <ms>-0 → <ms-1>-MAX; mirror of next_stream_id
glob_match is a faithful port of Redis's stringmatchlen — use it to filter
keys locally without a round trip.
[0x05] 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 (via stryke's
pkg::commands::try_load_ffi_for resolver hook) and caches one
redis::Connection per (url, db, auth) tuple in
OnceCell<Mutex<HashMap>> for the life of the stryke process.
Wire shape:
- Scalars →
{"value": …} - Lists / sets / values →
{"values": [...]}or{"members": [...]} - Hashes →
{"hash": {...}} - Zset rows with scores →
{"pairs": [[member, score], ...]} - Errors →
{"error": "<msg>"}— the wrapperdies with it
Set STRYKE_REDIS_DEBUG=1 to log every call's request JSON to stderr.
[0x06] Tests
cargo test # compiles, 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.
Local test server:
brew install redis # macOS
brew services start redis
# or
docker run --rm -p 6379:6379 redis:7
[0x07] Dev workflow
make # release build
make debug
make test
make install
make clean
[0x08] Layout
stryke-redis/
stryke.toml # stryke package manifest ([ffi] table)
Cargo.toml # cdylib crate manifest
Makefile
src/lib.rs # cdylib — redis__* extern "C" exports + persistent conn cache
lib/
Redis.stk # `use Redis` — thin wrapper around the FFI symbols
t/
test_redis.stk # live round-trip test suite
test_stryke_redis_surface.stk # wrapper-completeness pin
tests/ # Rust contract test + repo lint gates
examples/
kv.stk
structures.stk
publish.stk
pipeline.stk
discover.stk
docs/
index.html # docs site
report.html
.github/workflows/
ci.yml # check + fmt + clippy + cargo test + doc
release.yml # cross-compile + GH release on tag push
[0x09] Roadmap
| Shipped | Not yet |
|---|---|
| All core data types + admin | Streaming SUBSCRIBE once stryke has Unix-socket builtin |
| Streams, geo, Lua scripting, bitmaps, HyperLogLog | RESP3 + RedisJSON / RedisSearch / RedisGraph |
Pipelined ops + transactions (Redis::pipeline, transaction => 1) | Redis 8 Vector Sets + Bloom |
| In-process cdylib — no subprocess per call | |
Connection cached per (url, db, auth) for the process lifetime | |
String/binary values, redis 1.x sync API |
[0xFF] License
MIT.