Cache
Persistent caching backed by SoliKV with automatic TTL expiration. Values survive restarts, are shared across server instances, and are namespaced under soli:cache: so they never collide with raw KV data.
How it works
- Values are JSON-serialized on
setand parsed back onget— works for any valueJSON.stringifywould handle. - Keys are transparently prefixed with
soli:cache:;Cache.keysreturns them with the prefix stripped. - TTL expiration is enforced by SoliKV — there is no Soli-side reaper. Default TTL is
3600s(1 hour). - Misses return
null.Cache.fetchturns the cache-aside pattern into one expression.
Configuration
Cache reuses the SoliKV connection. Configure via env vars or programmatically:
| Variable | Description | Default |
|---|---|---|
SOLIKV_RESP_HOST |
SoliKV host | localhost |
SOLIKV_RESP_PORT |
RESP port | 6380 |
SOLIKV_TOKEN |
Bearer token (optional) | — |
SOLIKV_RESP_HOST=localhost
SOLIKV_RESP_PORT=6380
SOLIKV_TOKEN=my-secret-token
Cache.configure("my-solikv-host", "my-secret-token")
Read & Write
Cache.set(key, value, ttl_seconds?)
Store a value, JSON-serialized. ttl_seconds defaults to 3600.
Cache.set("user:123", { "name": "Alice" })
Cache.set("session", data, 1800) # 30 minute TTL
Returns: null
Cache.get(key)
Retrieve a value. Returns null if the key is missing or expired.
user = Cache.get("user:123")
if user != null
println("Cached user: " + user["name"])
end
Cache.fetch(key, ttl?) do ... end
cache-aside
Returns the cached value on hit. On miss, runs the block, caches the result, and returns it. Without a block it behaves like Cache.get.
user = Cache.fetch("user:" + str(user_id), 300) do
User.find(user_id)
end
If the block raises, nothing is cached.
Cache.delete(key)
Remove a key. Returns true if it existed.
Cache.has(key)
Check whether a key exists. Returns Bool.
TTL Management
Cache.ttl(key)
Seconds until expiration, or null if the key is missing or has no TTL.
Cache.touch(key, ttl)
Reset the TTL of an existing key. Returns true on success, false if the key does not exist.
Cache.touch("user:123", 3600) # extend by 1 hour
Inspection & Bulk Operations
| Method | Description |
|---|---|
Cache.keys |
All cache keys with the soli:cache: prefix stripped. Returns Array. |
Cache.size |
Number of cached entries. Returns Int. |
Cache.clear |
Remove every key under the cache prefix. Other SoliKV data is untouched. |
Cache.clear_expired() |
No-op. SoliKV expires keys on its own — kept for API symmetry with file-based caches. |
Connection
Cache.configure(host, token?)
Override the SoliKV connection at runtime. Affects KV too — both share one client.
Cache.configure("kv.internal:6380", env("SOLIKV_TOKEN"))
Instance Form
Call cache() to get a thin wrapper instance. All instances share the same backing store — useful when you want to pass a cache object as a dependency rather than reaching for the static class.
c = cache()
c.set("greeting", "hello")
c.get("greeting") # => "hello"
Common Patterns
Cache-aside reads
Wrap an expensive read with Cache.fetch — one expression replaces the read-check-set dance.
user = Cache.fetch("user:" + str(user_id), 300) do
User.find(user_id)
end
fn get_user_cached(user_id) {
key = "user:" + str(user_id)
cached = Cache.get(key)
return cached if cached != null
user = User.find(user_id)
Cache.set(key, user, 300)
user
}
Write-through invalidation
Drop the cache entry whenever the underlying record changes — simpler than trying to keep them in sync.
class User {
static fn update(id, attrs) {
user = db.update("users", id, attrs)
Cache.delete("user:" + str(id))
user
}
}
Short-TTL fragment caching
Cache rendered fragments or aggregated views for a few seconds to absorb traffic spikes without going stale.
fn index {
stats = Cache.fetch("dashboard:stats", 30) do
compute_dashboard_stats()
end
return render("dashboard/index", { "stats": stats })
}