Skip to main content

kevy_embedded/
ops.rs

1//! Data-type methods on [`Store`] — string, hash, list, set, sorted set,
2//! plus the pub/sub `publish` / `subscribe` / `psubscribe` entry points.
3//!
4//! All of these are thin facades over `kevy_store::Store` (the keyspace)
5//! and `pubsub::PubsubBus` (the in-process bus); they hold the embedded
6//! mutex for the duration of the underlying call, then drop it. AOF
7//! logging + post-write eviction sweep run via `commit_write` from
8//! `store.rs`. Behaviour and ABI are unchanged from the v1.1.0 single-file
9//! layout — this module only exists to keep `store.rs` under the 500-LOC
10//! cap.
11
12use std::io;
13use std::time::Duration;
14
15use kevy_store::StoreError;
16
17use crate::pubsub::Subscription;
18#[cfg(not(target_arch = "wasm32"))]
19use crate::replica_glue::ensure_writable;
20use crate::store::{Store, commit_write, store_err};
21
22// wasm has no replica runner, so the read-only guard is unconditionally a
23// no-op. Mirrors `replica_glue::ensure_writable`'s signature so the rest of
24// this file is target-agnostic.
25#[cfg(target_arch = "wasm32")]
26fn ensure_writable(_s: &Store) -> io::Result<()> { Ok(()) }
27
28impl Store {
29    // ---- string ops -----------------------------------------------------
30
31    /// `SET key value` (no TTL, no NX/XX). Returns `true` always under the
32    /// embedded API (Redis semantics: SET overwrites; NX/XX vetoes would
33    /// return `false` but we don't expose those here — use [`Store::with`]
34    /// for the full surface).
35    pub fn set(&self, key: &[u8], value: &[u8]) -> io::Result<bool> {
36        ensure_writable(self)?;
37        let mut g = self.wshard(key);
38        let ok = g.store.set(key, value.to_vec(), None, false, false);
39        commit_write(&mut g, &[b"SET", key, value])?;
40        Ok(ok)
41    }
42
43    /// `SET key value PX ms` — overwrites + sets TTL. The AOF records an
44    /// **absolute** `PEXPIREAT` deadline (not the relative `ttl`) so the key
45    /// expires at the same wall-clock instant after a restart — a relative
46    /// `PEXPIRE` would be re-anchored to replay-time, resetting the TTL to a
47    /// fresh full duration on every restart (INC-2026-06-09).
48    pub fn set_with_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> io::Result<bool> {
49        ensure_writable(self)?;
50        let mut g = self.wshard(key);
51        let ok = g.store.set(key, value.to_vec(), Some(ttl), false, false);
52        let ms = ttl.as_millis().min(u128::from(u64::MAX)) as u64;
53        let deadline = kevy_store::now_unix_ms().saturating_add(ms);
54        commit_write(&mut g, &[b"SET", key, value])?;
55        commit_write(&mut g, &[b"PEXPIREAT", key, deadline.to_string().as_bytes()])?;
56        Ok(ok)
57    }
58
59    /// `GET key` — `Some(bytes)` on hit, `None` on miss or expired.
60    ///
61    /// With eviction off (`maxmemory == 0`, the default) this takes the **read**
62    /// lock and a non-mutating store lookup, so concurrent readers scale across
63    /// cores — the path a read-heavy embed cache lives on. With eviction on it
64    /// falls back to the exclusive lock + mutating get so each access still
65    /// stamps the LRU clock.
66    pub fn get(&self, key: &[u8]) -> io::Result<Option<Vec<u8>>> {
67        if self.config().maxmemory == 0 {
68            let g = self.rshard(key);
69            return Ok(g.store.get_shared(key).map_err(store_err)?.map(|c| c.into_owned()));
70        }
71        let mut g = self.wshard(key);
72        Ok(g.store.get(key).map_err(store_err)?.map(|c| c.into_owned()))
73    }
74
75    /// `DEL key1 [key2 ...]`. Returns the count of keys actually removed.
76    /// Keys fan out to their owning shards.
77    pub fn del(&self, keys: &[&[u8]]) -> io::Result<usize> {
78        ensure_writable(self)?;
79        let mut total = 0;
80        for k in keys {
81            let owned = vec![k.to_vec()];
82            let mut g = self.wshard(k);
83            let n = g.store.del(&owned);
84            if n > 0 {
85                total += n;
86                commit_write(&mut g, &[b"DEL", k])?;
87            }
88        }
89        Ok(total)
90    }
91
92    /// `EXISTS key1 [key2 ...]`. Count of existing keys (duplicates counted
93    /// multiple times, matching Redis).
94    pub fn exists(&self, keys: &[&[u8]]) -> io::Result<usize> {
95        let mut total = 0;
96        for k in keys {
97            total += self.wshard(k).store.exists(&[k.to_vec()]);
98        }
99        Ok(total)
100    }
101
102    /// `INCR key`. Returns the post-increment value.
103    pub fn incr(&self, key: &[u8]) -> io::Result<i64> {
104        self.incr_by(key, 1)
105    }
106
107    /// `INCRBY key delta`. Negative `delta` does DECR-style work.
108    pub fn incr_by(&self, key: &[u8], delta: i64) -> io::Result<i64> {
109        ensure_writable(self)?;
110        let mut g = self.wshard(key);
111        let n = g.store.incr_by(key, delta).map_err(store_err)?;
112        commit_write(&mut g, &[b"INCRBY", key, delta.to_string().as_bytes()])?;
113        Ok(n)
114    }
115
116    /// `EXPIRE key seconds`. Returns `true` if a key was touched. The AOF
117    /// records an absolute `PEXPIREAT` deadline (see [`Self::set_with_ttl`])
118    /// so the TTL survives a restart unchanged.
119    pub fn expire(&self, key: &[u8], ttl: Duration) -> io::Result<bool> {
120        ensure_writable(self)?;
121        let mut g = self.wshard(key);
122        let touched = g.store.expire(key, ttl);
123        if touched {
124            let ms = ttl.as_millis().min(u128::from(u64::MAX)) as u64;
125            let deadline = kevy_store::now_unix_ms().saturating_add(ms);
126            commit_write(&mut g, &[b"PEXPIREAT", key, deadline.to_string().as_bytes()])?;
127        }
128        Ok(touched)
129    }
130
131    /// `PERSIST key`. Returns `true` if a TTL was actually cleared.
132    pub fn persist(&self, key: &[u8]) -> io::Result<bool> {
133        ensure_writable(self)?;
134        let mut g = self.wshard(key);
135        let touched = g.store.persist(key);
136        if touched {
137            commit_write(&mut g, &[b"PERSIST", key])?;
138        }
139        Ok(touched)
140    }
141
142    /// Remaining TTL in ms (or Redis-style `-1`/`-2` for no-TTL/no-key).
143    pub fn ttl_ms(&self, key: &[u8]) -> i64 {
144        self.wshard(key).store.pttl(key)
145    }
146
147    /// `TYPE key` — `"string"`, `"hash"`, `"list"`, `"set"`, `"zset"`, or `"none"`.
148    pub fn type_of(&self, key: &[u8]) -> &'static str {
149        self.wshard(key).store.type_of(key)
150    }
151
152    /// `DBSIZE` — total live keys across all shards.
153    pub fn dbsize(&self) -> usize {
154        self.sum_shards(|i| i.store.dbsize())
155    }
156
157    /// `FLUSHALL` — empty every shard (each logs `FLUSHALL` so a replay reaches
158    /// the same empty state).
159    ///
160    /// Named `flushall` — **not** `flush` — to avoid colliding with
161    /// `Write::flush`'s "sync buffered writes to disk" meaning. This call
162    /// WIPES the store; durability needs no explicit call (each write appends
163    /// to the AOF, the shard's `BufWriter` lands per [`AppendFsync`] cadence
164    /// and on drop).
165    ///
166    /// [`AppendFsync`]: crate::AppendFsync
167    pub fn flushall(&self) -> io::Result<()> {
168        ensure_writable(self)?;
169        let r = self.try_for_each_shard(|inner| {
170            inner.store.flushall();
171            commit_write(inner, &[b"FLUSHALL"])
172        });
173        // v2.3 feed contract: FLUSHALL breaks stream continuity.
174        #[cfg(not(target_arch = "wasm32"))]
175        self.feed_bump_on_flush();
176        r
177    }
178
179    /// Deprecated alias for [`Self::flushall`]. The old name read like
180    /// `Write::flush` (sync-to-disk) but actually WIPES the store — a
181    /// data-loss footgun.
182    #[deprecated(
183        since = "1.2.0",
184        note = "renamed to `flushall`: `flush` collides with Write::flush (sync-to-disk); this WIPES the store"
185    )]
186    pub fn flush(&self) -> io::Result<()> {
187        self.flushall()
188    }
189
190    /// `MEMORY USAGE` for one key — `Some(bytes)` or `None` if absent.
191    pub fn key_bytes(&self, key: &[u8]) -> Option<u64> {
192        self.wshard(key).store.estimate_key_bytes(key)
193    }
194
195    /// Live `used_memory` estimate (summed across shards).
196    pub fn used_memory(&self) -> u64 {
197        self.sum_shards_u64(|i| i.store.used_memory())
198    }
199
200    /// `INFO`-style counter: total keys evicted by `maxmemory` (all shards).
201    pub fn evictions_total(&self) -> u64 {
202        self.sum_shards_u64(|i| i.store.evictions_total())
203    }
204
205    /// `INFO`-style counter: total keys expired (lazy + active reaper, all shards).
206    pub fn expired_keys_total(&self) -> u64 {
207        self.sum_shards_u64(|i| i.store.expired_keys_total())
208    }
209
210    // ---- hash ops -------------------------------------------------------
211
212    /// `HSET key field value [field value ...]`. Returns count newly added.
213    pub fn hset(&self, key: &[u8], pairs: &[(&[u8], &[u8])]) -> io::Result<usize> {
214        ensure_writable(self)?;
215        let mut g = self.wshard(key);
216        let owned: Vec<(Vec<u8>, Vec<u8>)> =
217            pairs.iter().map(|(f, v)| (f.to_vec(), v.to_vec())).collect();
218        let added = g.store.hset(key, &owned).map_err(store_err)?;
219        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
220        parts.push(b"HSET");
221        parts.push(key);
222        for (f, v) in pairs {
223            parts.push(f);
224            parts.push(v);
225        }
226        commit_write(&mut g, &parts)?;
227        Ok(added)
228    }
229
230    /// `HGET key field`. `None` if absent.
231    pub fn hget(&self, key: &[u8], field: &[u8]) -> io::Result<Option<Vec<u8>>> {
232        let mut g = self.wshard(key);
233        Ok(g.store
234            .hget(key, field)
235            .map_err(store_err)?
236            .map(<[u8]>::to_vec))
237    }
238
239    /// `HDEL key field [field ...]`. Returns count actually removed.
240    pub fn hdel(&self, key: &[u8], fields: &[&[u8]]) -> io::Result<usize> {
241        ensure_writable(self)?;
242        let mut g = self.wshard(key);
243        let owned: Vec<Vec<u8>> = fields.iter().map(|f| f.to_vec()).collect();
244        let removed = g.store.hdel(key, &owned).map_err(store_err)?;
245        if removed > 0 {
246            let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + fields.len());
247            parts.push(b"HDEL");
248            parts.push(key);
249            for f in fields {
250                parts.push(f);
251            }
252            commit_write(&mut g, &parts)?;
253        }
254        Ok(removed)
255    }
256
257    // ---- list ops -------------------------------------------------------
258
259    /// `LPUSH key value [value ...]`. Returns the new list length.
260    pub fn lpush(&self, key: &[u8], values: &[&[u8]]) -> io::Result<usize> {
261        push_helper(self, key, values, b"LPUSH", kevy_store::Store::lpush)
262    }
263
264    /// `RPUSH key value [value ...]`. Returns the new list length.
265    pub fn rpush(&self, key: &[u8], values: &[&[u8]]) -> io::Result<usize> {
266        push_helper(self, key, values, b"RPUSH", kevy_store::Store::rpush)
267    }
268
269    /// `LPOP key count`. Returns popped values from the head.
270    pub fn lpop(&self, key: &[u8], count: usize) -> io::Result<Vec<Vec<u8>>> {
271        pop_helper(self, key, count, false)
272    }
273
274    /// `RPOP key count`. Symmetric to `LPOP` from the tail.
275    pub fn rpop(&self, key: &[u8], count: usize) -> io::Result<Vec<Vec<u8>>> {
276        pop_helper(self, key, count, true)
277    }
278
279    /// `LLEN key`. Length of the list at `key`; 0 if absent.
280    pub fn llen(&self, key: &[u8]) -> io::Result<usize> {
281        self.wshard(key).store.llen(key).map_err(store_err)
282    }
283
284    // ---- set ops --------------------------------------------------------
285
286    /// `SADD key member [member ...]`. Returns count newly added.
287    pub fn sadd(&self, key: &[u8], members: &[&[u8]]) -> io::Result<usize> {
288        push_helper(self, key, members, b"SADD", kevy_store::Store::sadd)
289    }
290
291    /// `SREM key member [member ...]`. Returns count actually removed.
292    pub fn srem(&self, key: &[u8], members: &[&[u8]]) -> io::Result<usize> {
293        ensure_writable(self)?;
294        let mut g = self.wshard(key);
295        let owned: Vec<Vec<u8>> = members.iter().map(|m| m.to_vec()).collect();
296        let removed = g.store.srem(key, &owned).map_err(store_err)?;
297        if removed > 0 {
298            let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
299            parts.push(b"SREM");
300            parts.push(key);
301            for m in members {
302                parts.push(m);
303            }
304            commit_write(&mut g, &parts)?;
305        }
306        Ok(removed)
307    }
308
309    /// `SMEMBERS key`. Order implementation-defined; empty if absent.
310    pub fn smembers(&self, key: &[u8]) -> io::Result<Vec<Vec<u8>>> {
311        self.wshard(key).store.smembers(key).map_err(store_err)
312    }
313
314    /// `SCARD key`. Member count; 0 if absent.
315    pub fn scard(&self, key: &[u8]) -> io::Result<usize> {
316        self.wshard(key).store.scard(key).map_err(store_err)
317    }
318
319    // ---- zset ops -------------------------------------------------------
320
321    /// `ZADD key score member [score member ...]`. Returns count newly added.
322    pub fn zadd(&self, key: &[u8], pairs: &[(f64, &[u8])]) -> io::Result<usize> {
323        ensure_writable(self)?;
324        let mut g = self.wshard(key);
325        let owned: Vec<(f64, Vec<u8>)> =
326            pairs.iter().map(|(s, m)| (*s, m.to_vec())).collect();
327        let added = g.store.zadd(key, &owned).map_err(store_err)?;
328        let mut score_strs: Vec<Vec<u8>> = Vec::with_capacity(pairs.len());
329        for (s, _) in pairs {
330            score_strs.push(format!("{s}").into_bytes());
331        }
332        let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + pairs.len() * 2);
333        parts.push(b"ZADD");
334        parts.push(key);
335        for (i, (_, m)) in pairs.iter().enumerate() {
336            parts.push(&score_strs[i]);
337            parts.push(m);
338        }
339        commit_write(&mut g, &parts)?;
340        Ok(added)
341    }
342
343    /// `ZREM key member [member ...]`. Returns count actually removed.
344    pub fn zrem(&self, key: &[u8], members: &[&[u8]]) -> io::Result<usize> {
345        ensure_writable(self)?;
346        let mut g = self.wshard(key);
347        let owned: Vec<Vec<u8>> = members.iter().map(|m| m.to_vec()).collect();
348        let removed = g.store.zrem(key, &owned).map_err(store_err)?;
349        if removed > 0 {
350            let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + members.len());
351            parts.push(b"ZREM");
352            parts.push(key);
353            for m in members {
354                parts.push(m);
355            }
356            commit_write(&mut g, &parts)?;
357        }
358        Ok(removed)
359    }
360
361    /// `ZSCORE key member`. `Some(score)` if present.
362    pub fn zscore(&self, key: &[u8], member: &[u8]) -> io::Result<Option<f64>> {
363        self.wshard(key).store.zscore(key, member).map_err(store_err)
364    }
365
366    /// `ZCARD key`. Member count; 0 if absent.
367    pub fn zcard(&self, key: &[u8]) -> io::Result<usize> {
368        self.wshard(key).store.zcard(key).map_err(store_err)
369    }
370
371    // ---- pub/sub --------------------------------------------------------
372
373    /// `PUBLISH channel payload`. Delivers `payload` to every subscriber on
374    /// `channel` (direct + pattern matches) inside this process. Returns
375    /// the count of receivers the message reached.
376    pub fn publish(&self, channel: &[u8], payload: &[u8]) -> usize {
377        // Clone matching senders under the lock, then release before
378        // send() so a slow receiver can't stall unrelated traffic.
379        let plans = {
380            // Pub/sub is process-wide; the bus lives on shard 0.
381            let g = self.lock();
382            g.bus.collect_delivery(channel, payload)
383        };
384        let mut count = 0;
385        for (frame, sender) in plans {
386            if sender.send(frame).is_ok() {
387                count += 1;
388            }
389        }
390        count
391    }
392
393    /// Open a [`Subscription`] subscribed to `channels`. Drop the handle
394    /// to unsubscribe from everything atomically. Pass `&[]` to start
395    /// with no subscriptions and add some later via
396    /// [`Subscription::subscribe`] / [`Subscription::psubscribe`].
397    pub fn subscribe(&self, channels: &[&[u8]]) -> Subscription {
398        let mut sub = Subscription::new(self.inner_handle(), self.guard_handle());
399        if !channels.is_empty() {
400            sub.subscribe(channels);
401        }
402        sub
403    }
404
405    /// Convenience: open a [`Subscription`] starting on pattern subscriptions.
406    pub fn psubscribe(&self, patterns: &[&[u8]]) -> Subscription {
407        let mut sub = Subscription::new(self.inner_handle(), self.guard_handle());
408        if !patterns.is_empty() {
409            sub.psubscribe(patterns);
410        }
411        sub
412    }
413}
414
415// ─────────────────────────────────────────────────────────────────────────
416// Shared list/set push + list pop helpers. `&Store` so we can lock + AOF-log.
417// ─────────────────────────────────────────────────────────────────────────
418
419fn push_helper<F>(
420    s: &Store,
421    key: &[u8],
422    values: &[&[u8]],
423    verb: &'static [u8],
424    op: F,
425) -> io::Result<usize>
426where
427    F: FnOnce(&mut kevy_store::Store, &[u8], &[Vec<u8>]) -> Result<usize, StoreError>,
428{
429    ensure_writable(s)?;
430    let mut g = s.wshard(key);
431    let owned: Vec<Vec<u8>> = values.iter().map(|v| v.to_vec()).collect();
432    let n = op(&mut g.store, key, &owned).map_err(store_err)?;
433    let mut parts: Vec<&[u8]> = Vec::with_capacity(2 + values.len());
434    parts.push(verb);
435    parts.push(key);
436    for v in values {
437        parts.push(v);
438    }
439    commit_write(&mut g, &parts)?;
440    Ok(n)
441}
442
443fn pop_helper(s: &Store, key: &[u8], count: usize, from_tail: bool) -> io::Result<Vec<Vec<u8>>> {
444    ensure_writable(s)?;
445    let mut g = s.wshard(key);
446    let popped = if from_tail {
447        g.store.rpop(key, count).map_err(store_err)?
448    } else {
449        g.store.lpop(key, count).map_err(store_err)?
450    };
451    if !popped.is_empty() {
452        let verb: &[u8] = if from_tail { b"RPOP" } else { b"LPOP" };
453        let count_str = popped.len().to_string();
454        let parts: [&[u8]; 3] = [verb, key, count_str.as_bytes()];
455        commit_write(&mut g, &parts)?;
456    }
457    Ok(popped)
458}