Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,27 @@ jobs:
generate_release_notes: true
files: release/*

npm:
name: publish ember-ts
runs-on: ubuntu-latest
needs: release
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: build and publish
working-directory: clients/ember-ts
run: |
npm install
npm run build
npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

homebrew:
name: update homebrew formula
runs-on: ubuntu-latest
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ a low-latency, memory-efficient, distributed cache written in Rust. designed to
- **resp3 protocol** — full compatibility with `redis-cli` and existing Redis clients
- **string commands** — GET, SET (with NX/XX/EX/PX), MGET, MSET, MSETNX, INCR, DECR, INCRBY, DECRBY, INCRBYFLOAT, APPEND, STRLEN, GETRANGE, SETRANGE, GETSET, GETDEL, GETEX
- **list operations** — LPUSH, RPUSH, LPOP, RPOP, LRANGE, LLEN, LINDEX, LSET, LTRIM, LINSERT, LREM, LPOS, LMOVE, LMPOP, BLPOP, BRPOP
- **sorted sets** — ZADD (with NX/XX/GT/LT/CH), ZREM, ZSCORE, ZRANK, ZREVRANK, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZINCRBY, ZCOUNT, ZPOPMIN, ZPOPMAX, ZCARD, ZSCAN, ZUNION, ZINTER, ZDIFF, ZMPOP, ZRANDMEMBER
- **hashes** — HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HINCRBY, HKEYS, HVALS, HMGET, HSCAN, HRANDFIELD
- **sorted sets** — ZADD (with NX/XX/GT/LT/CH), ZREM, ZSCORE, ZRANK, ZREVRANK, ZRANGE, ZREVRANGE, ZRANGEBYSCORE, ZREVRANGEBYSCORE, ZINCRBY, ZCOUNT, ZPOPMIN, ZPOPMAX, ZCARD, ZSCAN, ZUNION, ZINTER, ZDIFF, ZUNIONSTORE, ZINTERSTORE, ZDIFFSTORE, ZMPOP, ZRANDMEMBER
- **hashes** — HSET, HGET, HGETALL, HDEL, HEXISTS, HLEN, HINCRBY, HINCRBYFLOAT, HKEYS, HVALS, HMGET, HSCAN, HRANDFIELD
- **sets** — SADD, SREM, SMEMBERS, SISMEMBER, SCARD, SMISMEMBER, SUNION, SINTER, SDIFF, SUNIONSTORE, SINTERSTORE, SDIFFSTORE, SRANDMEMBER, SPOP, SSCAN, SMOVE, SINTERCARD
- **bitmaps** — GETBIT, SETBIT, BITCOUNT, BITPOS, BITOP
- **key commands** — DEL, UNLINK, EXISTS, EXPIRE, EXPIREAT, EXPIRETIME, TTL, PEXPIRE, PEXPIREAT, PEXPIRETIME, PTTL, PERSIST, TYPE, SCAN, KEYS, RENAME, COPY, TOUCH, RANDOMKEY, SORT, OBJECT ENCODING/REFCOUNT, WAIT
- **server commands** — PING, ECHO, INFO, DBSIZE, FLUSHDB, FLUSHALL, MEMORY USAGE, BGSAVE, BGREWRITEAOF, AUTH, QUIT, CONFIG GET/SET/REWRITE, SLOWLOG, CLIENT ID/SETNAME/GETNAME/LIST, TIME, LASTSAVE, ROLE, MONITOR
- **server commands** — PING, ECHO, INFO, DBSIZE, FLUSHDB, FLUSHALL, MEMORY USAGE, BGSAVE, BGREWRITEAOF, AUTH, QUIT, CONFIG GET/SET/REWRITE, SLOWLOG, CLIENT ID/SETNAME/GETNAME/LIST, TIME, LASTSAVE, ROLE, MONITOR, COMMAND/COMMAND COUNT/COMMAND INFO/COMMAND DOCS
- **transactions** — MULTI, EXEC, DISCARD, WATCH/UNWATCH for optimistic locking
- **acl** — per-user command permissions and key pattern restrictions: ACL SETUSER, GETUSER, DELUSER, LIST, WHOAMI, CAT, USERS
- **pub/sub** — SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, plus PUBSUB introspection
Expand All @@ -46,6 +46,25 @@ a low-latency, memory-efficient, distributed cache written in Rust. designed to
- **interactive CLI** — `ember-cli` with REPL, syntax highlighting, tab-completion, inline hints, cluster subcommands, and built-in benchmark
- **graceful shutdown** — drains active connections on SIGINT/SIGTERM before exiting

## quickest start

```bash
# docker — no install needed
docker run -p 6379:6379 ghcr.io/kacy/ember:latest
redis-cli ping
# => PONG
```

or with docker compose for a persistent single-node setup:

```bash
curl -O https://raw.githubusercontent.com/kacy/ember/main/docker-compose.yml
docker compose up -d
redis-cli ping
```

---

## not supported

ember is purpose-built for caching. some Redis features are intentionally excluded:
Expand Down
30 changes: 30 additions & 0 deletions crates/ember-cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ pub static COMMANDS: &[CommandInfo] = &[
group: "connection",
summary: "manage client connections",
},
CommandInfo {
name: "COMMAND",
args: "[COUNT | INFO name [name ...] | DOCS name [name ...]]",
group: "connection",
summary: "get an array of server command metadata",
},
CommandInfo {
name: "ECHO",
args: "message",
Expand Down Expand Up @@ -449,6 +455,12 @@ pub static COMMANDS: &[CommandInfo] = &[
group: "hash",
summary: "increment the integer value of a hash field",
},
CommandInfo {
name: "HINCRBYFLOAT",
args: "key field increment",
group: "hash",
summary: "increment the float value of a hash field",
},
CommandInfo {
name: "HKEYS",
args: "key",
Expand Down Expand Up @@ -553,6 +565,18 @@ pub static COMMANDS: &[CommandInfo] = &[
group: "sorted_set",
summary: "get the number of members in a sorted set",
},
CommandInfo {
name: "ZDIFFSTORE",
args: "destkey numkeys key [key ...]",
group: "sorted_set",
summary: "subtract multiple sorted sets and store the result in a new key",
},
CommandInfo {
name: "ZINTERSTORE",
args: "destkey numkeys key [key ...]",
group: "sorted_set",
summary: "intersect multiple sorted sets and store the result in a new key",
},
CommandInfo {
name: "ZMPOP",
args: "numkeys key [key ...] MIN|MAX [COUNT n]",
Expand Down Expand Up @@ -595,6 +619,12 @@ pub static COMMANDS: &[CommandInfo] = &[
group: "sorted_set",
summary: "get the score of a member in a sorted set",
},
CommandInfo {
name: "ZUNIONSTORE",
args: "destkey numkeys key [key ...]",
group: "sorted_set",
summary: "add multiple sorted sets and store the result in a new key",
},
// --- server ---
CommandInfo {
name: "ACL",
Expand Down
72 changes: 72 additions & 0 deletions crates/ember-core/src/keyspace/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,78 @@ impl Keyspace {
Ok(new_val)
}

/// Increments a field's float value by the given amount.
///
/// If the field doesn't exist, it is created with the increment as its value.
/// The stored value must be a valid float or the call returns an error.
/// Returns the new value as a formatted string.
pub fn hincrbyfloat(&mut self, key: &str, field: &str, delta: f64) -> Result<String, IncrFloatError> {
self.remove_if_expired(key);

let is_new = match self.entries.get(key) {
None => true,
Some(e) if matches!(e.value, Value::Hash(_)) => false,
Some(_) => return Err(IncrFloatError::WrongType),
};

// generous float string length estimate
let val_str_len = 32usize;
let estimated_increase = if is_new {
memory::ENTRY_OVERHEAD
+ key.len()
+ memory::PACKED_HASH_BASE_OVERHEAD
+ field.len()
+ val_str_len
+ memory::PACKED_HASH_ENTRY_OVERHEAD
} else {
field.len() + val_str_len + memory::PACKED_HASH_ENTRY_OVERHEAD
};

if !self.enforce_memory_limit(estimated_increase) {
return Err(IncrFloatError::OutOfMemory);
}

if is_new {
let value = Value::Hash(Box::default());
self.memory.add(key, &value);
let entry = Entry::new(value, None);
self.entries.insert(CompactString::from(key), entry);
self.bump_version(key);
}

let Some(entry) = self.entries.get_mut(key) else {
return Err(IncrFloatError::WrongType);
};
let old_entry_size = entry.entry_size(key);

let Value::Hash(ref mut hash) = entry.value else {
return Err(IncrFloatError::WrongType);
};
let current = match hash.get(field) {
Some(data) => {
let s = std::str::from_utf8(data).map_err(|_| IncrFloatError::NotAFloat)?;
s.parse::<f64>().map_err(|_| IncrFloatError::NotAFloat)?
}
None => 0.0,
};
let new_val = current + delta;
if new_val.is_nan() || new_val.is_infinite() {
return Err(IncrFloatError::NanOrInfinity);
}

let formatted = format_float(new_val);
hash.insert(field.into(), Bytes::from(formatted.clone()));
entry.touch(self.track_access);

let new_value_size = memory::value_size(&entry.value);
entry.cached_value_size = new_value_size as u32;
let new_entry_size = key.len() + new_value_size + memory::ENTRY_OVERHEAD;
self.memory.adjust(old_entry_size, new_entry_size);
self.bump_version(key);

Ok(formatted)
}

/// Returns all field names in a hash.
pub fn hkeys(&mut self, key: &str) -> Result<Vec<String>, WrongType> {
let Some(entry) = self.get_live_entry(key) else {
Expand Down
Loading
Loading