From 6053907934ce5b3a71c5273800e56df8cbc1e20f Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 9 Oct 2026 01:14:45 -0400 Subject: [PATCH 1/4] Make all valkey/store methods async The all wrap network calls, which themselves will be async. wasi:keyvalue is entierly sync, so wit_bindgen::block_on is used. Signed-off-by: Scott Andrews --- components/as-keyvalue/src/lib.rs | 26 +- components/dep-cli/src/main.rs | 96 +++++--- components/ops/src/lib.rs | 392 +++++++++++++++++------------- wit/store.wit | 68 +++--- 4 files changed, 338 insertions(+), 244 deletions(-) diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index 4b9b114..ec18cb9 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -43,7 +43,7 @@ impl StoreGuest for KeyvalueToValkey { }, client_name: None, }; - let connection = valkey::connect(&hostname, port, Some(&opts))?; + let connection = wit_bindgen::block_on(valkey::connect(hostname, port, Some(opts)))?; let key_prefix = config::get(KEY_PREFIX_KEY)?.unwrap_or(KEY_PREFIX_DEFAULT.to_string()); let hash_key = format!("{key_prefix}{identifier}"); @@ -62,7 +62,7 @@ struct KeyvalueToValkeyBucket { impl GuestBucket for KeyvalueToValkeyBucket { fn get(&self, key: String) -> Result>, Error> { - match self.connection.hget(&self.hash_key, &key)? { + match wit_bindgen::block_on(self.connection.hget(self.hash_key.clone(), key))? { Some(value) => Ok(Some(value.as_bytes().to_vec())), None => Ok(None), } @@ -70,15 +70,23 @@ impl GuestBucket for KeyvalueToValkeyBucket { fn set(&self, key: String, value: Vec) -> Result<(), Error> { let value = String::from_utf8(value).map_err(|e| Error::Other(e.to_string()))?; - Ok(self.connection.hset(&self.hash_key, &key, &value)?) + Ok(wit_bindgen::block_on(self.connection.hset( + self.hash_key.clone(), + key, + value, + ))?) } fn delete(&self, key: String) -> Result<(), Error> { - Ok(self.connection.hdel(&self.hash_key, &key)?) + Ok(wit_bindgen::block_on( + self.connection.hdel(self.hash_key.clone(), key), + )?) } fn exists(&self, key: String) -> Result { - Ok(self.connection.hexists(&self.hash_key, &key)?) + Ok(wit_bindgen::block_on( + self.connection.hexists(self.hash_key.clone(), key), + )?) } fn list_keys(&self, cursor: Option) -> Result { @@ -88,7 +96,7 @@ impl GuestBucket for KeyvalueToValkeyBucket { Ok(KeyResponse { cursor: None, - keys: self.connection.hkeys(&self.hash_key)?, + keys: wit_bindgen::block_on(self.connection.hkeys(self.hash_key.clone()))?, }) } } @@ -99,7 +107,11 @@ impl AtomicsGuest for KeyvalueToValkey { fn increment(bucket: BucketBorrow<'_>, key: String, delta: i64) -> Result { let bucket: &KeyvalueToValkeyBucket = bucket.get(); - Ok(bucket.connection.hincrby(&bucket.hash_key, &key, delta)?) + Ok(wit_bindgen::block_on(bucket.connection.hincrby( + bucket.hash_key.clone(), + key, + delta, + ))?) } fn swap(_cas: Cas, _value: Vec) -> Result<(), CasError> { diff --git a/components/dep-cli/src/main.rs b/components/dep-cli/src/main.rs index c48e855..f452dc0 100644 --- a/components/dep-cli/src/main.rs +++ b/components/dep-cli/src/main.rs @@ -320,7 +320,7 @@ enum ACLCommands { } fn main() { - match exec() { + match wit_bindgen::block_on(exec()) { Err(e) => { println!("Error: {e}"); process::exit(1); @@ -329,7 +329,7 @@ fn main() { } } -fn exec() -> Result<(), Error> { +async fn exec() -> Result<(), Error> { let cli = Cli::parse(); let opts = HelloOpts { @@ -340,7 +340,7 @@ fn exec() -> Result<(), Error> { }, client_name: cli.client_name, }; - let connection = connect(&cli.host, cli.port, Some(&opts))?; + let connection = connect(cli.host, cli.port, Some(opts)).await?; match &cli.command { Commands::SEND { cmd } => { @@ -348,41 +348,43 @@ fn exec() -> Result<(), Error> { .iter() .map(|c| Value::BulkString(c.to_string())) .collect(); - let response = connection.send(&cmd)?; + let response = connection.send(cmd).await?; println!("{response}"); } Commands::ACL(aclargs) => match &aclargs.command { ACLCommands::DELUSER { username } => { - connection.acl_deluser(&username)?; + connection.acl_deluser(username.to_string()).await?; println!("Deleted user {username}"); } ACLCommands::GENPASS => { - let pass = connection.acl_genpass()?; + let pass = connection.acl_genpass().await?; println!("{pass}"); } ACLCommands::SETUSER { username, rules } => { - connection.acl_setuser(username, rules.as_slice())?; + connection + .acl_setuser(username.to_string(), rules.to_vec()) + .await?; println!("Set user {username}"); } }, Commands::DEL { key } => { - connection.del(key)?; + connection.del(key.to_string()).await?; println!("Deleted {key}"); } - Commands::EXISTS { key } => match connection.exists(key)? { + Commands::EXISTS { key } => match connection.exists(key.to_string()).await? { true => println!("true"), false => println!("false"), }, - Commands::GET { key } => match connection.get(key)? { + Commands::GET { key } => match connection.get(key.to_string()).await? { Some(value) => println!("{}", value), None => println!(""), }, Commands::HDEL { key, field } => { - connection.hdel(key, field)?; + connection.hdel(key.to_string(), field.to_string()).await?; println!("Deleted {field}"); } Commands::HELLO => { - for (key, value) in connection.hello(None)? { + for (key, value) in connection.hello(None).await? { match value { Value::Null => println!("{key}: "), Value::String(value) => println!("{key}: {value}"), @@ -410,16 +412,21 @@ fn exec() -> Result<(), Error> { } } } - Commands::HEXISTS { key, field } => match connection.hexists(key, field)? { + Commands::HEXISTS { key, field } => match connection + .hexists(key.to_string(), field.to_string()) + .await? + { true => println!("true"), false => println!("false"), }, - Commands::HGET { key, field } => match connection.hget(key, field)? { - Some(value) => println!("{}", value), - None => println!(""), - }, + Commands::HGET { key, field } => { + match connection.hget(key.to_string(), field.to_string()).await? { + Some(value) => println!("{}", value), + None => println!(""), + } + } Commands::HGETALL { key } => { - let fields = connection.hgetall(key)?; + let fields = connection.hgetall(key.to_string()).await?; if fields.len() == 0 { println!(""); } @@ -432,7 +439,9 @@ fn exec() -> Result<(), Error> { field, increment, } => { - let value = connection.hincrby(key, field, *increment)?; + let value = connection + .hincrby(key.to_string(), field.to_string(), *increment) + .await?; println!("{}", value); } Commands::HINCRBYFLOAT { @@ -440,20 +449,24 @@ fn exec() -> Result<(), Error> { field, increment, } => { - let value = connection.hincrbyfloat(key, field, *increment)?; + let value = connection + .hincrbyfloat(key.to_string(), field.to_string(), *increment) + .await?; println!("{}", value); } Commands::HKEYS { key } => { - for key in connection.hkeys(key)? { + for key in connection.hkeys(key.to_string()).await? { println!("- {key}"); } } Commands::HLEN { key } => { - let len = connection.hlen(key)?; + let len = connection.hlen(key.to_string()).await?; println!("{len}"); } Commands::HMGET { key, fields } => { - let fields = connection.hmget(key, fields)?; + let fields = connection + .hmget(key.to_string().to_string(), fields.to_vec()) + .await?; for field in fields { match field { None => println!("- "), @@ -471,7 +484,7 @@ fn exec() -> Result<(), Error> { .chunks(2) .map(|c| (c[0].clone(), c[1].clone())) .collect(); - connection.hmset(key, &fields)?; + connection.hmset(key.to_string(), fields.clone()).await?; println!("Set {} field(s)", fields.len()); } Commands::HRANDFIELD { @@ -483,7 +496,7 @@ fn exec() -> Result<(), Error> { count: *count, with_values: *with_values, }; - let fields = connection.hrandfield(key, Some(opts))?; + let fields = connection.hrandfield(key.to_string(), Some(opts)).await?; match fields { None => println!(""), Some(fields) => { @@ -508,7 +521,9 @@ fn exec() -> Result<(), Error> { count: *count, no_values: *no_values, }; - let (cursor, fields) = connection.hscan(key, cursor.as_deref(), Some(opts))?; + let (cursor, fields) = connection + .hscan(key.to_string(), cursor.clone(), Some(opts.clone())) + .await?; if let Some(cursor) = cursor { println!("(cursor) {cursor}"); } @@ -523,21 +538,28 @@ fn exec() -> Result<(), Error> { } } Commands::HSET { key, field, value } => { - connection.hset(key, field, value)?; + connection + .hset(key.to_string(), field.to_string(), value.to_string()) + .await?; println!("Set {field}"); } Commands::HSETNX { key, field, value } => { - match connection.hsetnx(key, field, value)? { + match connection + .hsetnx(key.to_string(), field.to_string(), value.to_string()) + .await? + { false => println!("Field already set"), true => println!("Set {field}"), }; } Commands::HSTRLEN { key, field } => { - let len = connection.hstrlen(key, field)?; + let len = connection + .hstrlen(key.to_string(), field.to_string()) + .await?; println!("{len}"); } Commands::HVALS { key } => { - let values = connection.hvals(key)?; + let values = connection.hvals(key.to_string()).await?; if values.len() == 0 { println!(""); } @@ -546,24 +568,28 @@ fn exec() -> Result<(), Error> { } } Commands::INCR { key } => { - let value = connection.incr(key)?; + let value = connection.incr(key.to_string()).await?; println!("{}", value); } Commands::INCRBY { key, increment } => { - let value = connection.incrby(key, increment.clone())?; + let value = connection + .incrby(key.to_string(), increment.clone()) + .await?; println!("{}", value); } Commands::KEYS { pattern } => { - for key in connection.keys(pattern)? { + for key in connection.keys(pattern.to_string()).await? { println!("{key}"); } } Commands::PUBLISH { channel, message } => { - let value = connection.publish(channel, message)?; + let value = connection + .publish(channel.to_string(), message.to_string()) + .await?; println!("{}", value); } Commands::SET { key, value } => { - connection.set(key, value)?; + connection.set(key.to_string(), value.to_string()).await?; println!("Set {key}"); } } diff --git a/components/ops/src/lib.rs b/components/ops/src/lib.rs index 08cc8ed..04ba549 100644 --- a/components/ops/src/lib.rs +++ b/components/ops/src/lib.rs @@ -24,7 +24,7 @@ pub mod resp; struct ValkeyOps; impl ValkeyOps { - fn open(address: IpSocketAddress) -> Result { + async fn open(address: IpSocketAddress) -> Result { let socket = match address { IpSocketAddress::Ipv4(_) => create_tcp_socket(IpAddressFamily::Ipv4)?, IpSocketAddress::Ipv6(_) => create_tcp_socket(IpAddressFamily::Ipv6)?, @@ -40,7 +40,10 @@ impl ValkeyOps { }) } - fn resolve_ip_socket_addresses(host: &str, port: u16) -> Result, Error> { + async fn resolve_ip_socket_addresses( + host: &str, + port: u16, + ) -> Result, Error> { let ip_addresses = match host.parse() { Ok(IpAddr::V4(addr)) => { // host is an ipv4 address @@ -65,7 +68,7 @@ impl ValkeyOps { } Err(_) => { // resolve as a hostname - Self::resolve_ip_addresses(host)? + Self::resolve_ip_addresses(host).await? } }; @@ -91,7 +94,7 @@ impl ValkeyOps { Ok(socket_addresses) } - fn resolve_ip_addresses(host: &str) -> Result, Error> { + async fn resolve_ip_addresses(host: &str) -> Result, Error> { let network = instance_network(); let address_stream = resolve_addresses(&network, host)?; let mut addresses = vec![]; @@ -109,23 +112,21 @@ impl ValkeyOps { impl StoreGuest for ValkeyOps { type Connection = ValkeyConnection; - fn connect(host: String, port: u16, opts: Option) -> Result { - let connection = Self::resolve_ip_socket_addresses(&host, port)? - .into_iter() - .find_map(|addr| match Self::open(addr) { + async fn connect( + host: String, + port: u16, + opts: Option, + ) -> Result { + for addr in Self::resolve_ip_socket_addresses(&host, port).await? { + if let Ok(conn) = Self::open(addr).await { // check the connection is alive - Ok(conn) => match conn.hello(opts.clone()) { - Ok(_) => Some(conn), - // TODO distinguish between IO and Valkey errors - Err(_) => None, - }, - // TODO distinguish between IO and Valkey errors - Err(_) => None, - }); - match connection { - Some(connection) => Ok(Connection::new(connection)), - None => Err(Error::Client(format!("unable to connect to {host}:{port}"))), + if conn.hello(opts.clone()).await.is_ok() { + // it's alive + return Ok(Connection::new(conn)); + } + } } + Err(Error::Client(format!("unable to connect to {host}:{port}"))) } } @@ -136,7 +137,7 @@ struct ValkeyConnection { } impl GuestConnection for ValkeyConnection { - fn send(&self, command: Vec) -> Result { + async fn send(&self, command: Vec) -> Result { let request = encode(Value::Array( command.into_iter().map(|c| c.into()).collect(), )); @@ -151,15 +152,17 @@ impl GuestConnection for ValkeyConnection { decode(response).map(|r| r.into()) } - fn acl_deluser(&self, username: String) -> Result<(), Error> { + async fn acl_deluser(&self, username: String) -> Result<(), Error> { // https://valkey.io/commands/acl-deluser/ // ACL DELUSER username [ username ... ] - let response = self.send(vec![ - Value::BulkString("ACL".to_string()), - Value::BulkString("DELUSER".to_string()), - Value::BulkString(username), - ])?; + let response = self + .send(vec![ + Value::BulkString("ACL".to_string()), + Value::BulkString("DELUSER".to_string()), + Value::BulkString(username), + ]) + .await?; match response { Value::Integer(_) => Ok(()), Value::Error(err) => Err(Error::Valkey(err))?, @@ -170,14 +173,16 @@ impl GuestConnection for ValkeyConnection { } } - fn acl_genpass(&self) -> Result { + async fn acl_genpass(&self) -> Result { // https://valkey.io/commands/acl-genpass/ // ACL GENPASS [ bits ] - let response = self.send(vec![ - Value::BulkString("ACL".to_string()), - Value::BulkString("GENPASS".to_string()), - ])?; + let response = self + .send(vec![ + Value::BulkString("ACL".to_string()), + Value::BulkString("GENPASS".to_string()), + ]) + .await?; match response { Value::BulkString(pass) => Ok(pass), Value::Error(err) => Err(Error::Valkey(err))?, @@ -188,7 +193,7 @@ impl GuestConnection for ValkeyConnection { } } - fn acl_setuser(&self, username: String, rules: Vec) -> Result<(), Error> { + async fn acl_setuser(&self, username: String, rules: Vec) -> Result<(), Error> { // https://valkey.io/commands/acl-setuser/ // ACL SETUSER username [ rule ] [ [ rule ] ... ] @@ -200,7 +205,7 @@ impl GuestConnection for ValkeyConnection { for rule in rules { command.push(Value::BulkString(rule)); } - let response = self.send(command)?; + let response = self.send(command).await?; match response { Value::String(msg) => match msg.as_str() { "OK" => Ok(()), @@ -214,15 +219,17 @@ impl GuestConnection for ValkeyConnection { } } - fn auth(&self, username: String, password: String) -> Result<(), Error> { + async fn auth(&self, username: String, password: String) -> Result<(), Error> { // https://valkey.io/commands/auth/ // AUTH [ username ] password - let response = self.send(vec![ - Value::BulkString("AUTH".to_string()), - Value::BulkString(username), - Value::BulkString(password), - ])?; + let response = self + .send(vec![ + Value::BulkString("AUTH".to_string()), + Value::BulkString(username), + Value::BulkString(password), + ]) + .await?; match response { Value::String(msg) => match msg.as_str() { "OK" => Ok(()), @@ -236,15 +243,17 @@ impl GuestConnection for ValkeyConnection { } } - fn del(&self, key: String) -> Result<(), Error> { + async fn del(&self, key: String) -> Result<(), Error> { // https://valkey.io/commands/del/ // DEL key [ key ... ] // TODO handle multiple keys - let response = self.send(vec![ - Value::BulkString("DEL".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("DEL".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Integer(_) => Ok(()), Value::Error(err) => Err(Error::Valkey(err))?, @@ -255,15 +264,17 @@ impl GuestConnection for ValkeyConnection { } } - fn exists(&self, key: String) -> Result { + async fn exists(&self, key: String) -> Result { // https://valkey.io/commands/exists/ // EXISTS key [ key ... ] // TODO handle multiple keys - let response = self.send(vec![ - Value::BulkString("EXISTS".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("EXISTS".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Integer(0) => Ok(false), Value::Integer(1) => Ok(true), @@ -275,14 +286,16 @@ impl GuestConnection for ValkeyConnection { } } - fn get(&self, key: String) -> Result, Error> { + async fn get(&self, key: String) -> Result, Error> { // https://valkey.io/commands/get/ // GET key - let response = self.send(vec![ - Value::BulkString("GET".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("GET".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::BulkString(value) => Ok(Some(value)), Value::Null => Ok(None), @@ -294,15 +307,17 @@ impl GuestConnection for ValkeyConnection { } } - fn hdel(&self, key: String, field: String) -> Result<(), Error> { + async fn hdel(&self, key: String, field: String) -> Result<(), Error> { // https://valkey.io/commands/hdel/ // HDEL key field [ field ... ] - let response = self.send(vec![ - Value::BulkString("HDEL".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ])?; + let response = self + .send(vec![ + Value::BulkString("HDEL".to_string()), + Value::BulkString(key), + Value::BulkString(field), + ]) + .await?; match response { Value::Integer(_) => Ok(()), Value::Error(err) => Err(Error::Valkey(err))?, @@ -313,7 +328,7 @@ impl GuestConnection for ValkeyConnection { } } - fn hello(&self, opts: Option) -> Result, Error> { + async fn hello(&self, opts: Option) -> Result, Error> { // https://valkey.io/commands/hello/ // HELLO [ protover [ AUTH username password ] [ SETNAME clientname ] ] @@ -343,7 +358,7 @@ impl GuestConnection for ValkeyConnection { cmd.push(Value::BulkString(client_name)); } } - let response = self.send(cmd)?; + let response = self.send(cmd).await?; match response { // convert RESP2 array Value::Array(items) => { @@ -379,15 +394,17 @@ impl GuestConnection for ValkeyConnection { } } - fn hexists(&self, key: String, field: String) -> Result { + async fn hexists(&self, key: String, field: String) -> Result { // https://valkey.io/commands/hexists/ // HEXISTS key field - let response = self.send(vec![ - Value::BulkString("HEXISTS".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ])?; + let response = self + .send(vec![ + Value::BulkString("HEXISTS".to_string()), + Value::BulkString(key), + Value::BulkString(field), + ]) + .await?; match response { Value::Integer(0) => Ok(false), Value::Integer(1) => Ok(true), @@ -399,15 +416,17 @@ impl GuestConnection for ValkeyConnection { } } - fn hget(&self, key: String, field: String) -> Result, Error> { + async fn hget(&self, key: String, field: String) -> Result, Error> { // https://valkey.io/commands/hget/ // HGET key field - let response = self.send(vec![ - Value::BulkString("HGET".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ])?; + let response = self + .send(vec![ + Value::BulkString("HGET".to_string()), + Value::BulkString(key), + Value::BulkString(field), + ]) + .await?; match response { Value::BulkString(value) => Ok(Some(value)), Value::Null => Ok(None), @@ -419,14 +438,16 @@ impl GuestConnection for ValkeyConnection { } } - fn hgetall(&self, key: String) -> Result, Error> { + async fn hgetall(&self, key: String) -> Result, Error> { // https://valkey.io/commands/hgetall/ // HGETALL key - let response = self.send(vec![ - Value::BulkString("HGETALL".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("HGETALL".to_string()), + Value::BulkString(key), + ]) + .await?; match response { // RESP2 Value::Array(items) => { @@ -468,16 +489,18 @@ impl GuestConnection for ValkeyConnection { } } - fn hincrby(&self, key: String, field: String, increment: i64) -> Result { + async fn hincrby(&self, key: String, field: String, increment: i64) -> Result { // https://valkey.io/commands/hincrby/ // HINCRBY key field increment - let response = self.send(vec![ - Value::BulkString("HINCRBY".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(increment.to_string()), - ])?; + let response = self + .send(vec![ + Value::BulkString("HINCRBY".to_string()), + Value::BulkString(key), + Value::BulkString(field), + Value::BulkString(increment.to_string()), + ]) + .await?; match response { Value::Integer(value) => Ok(value), Value::Error(err) => Err(Error::Valkey(err))?, @@ -488,16 +511,23 @@ impl GuestConnection for ValkeyConnection { } } - fn hincrbyfloat(&self, key: String, field: String, increment: f64) -> Result { + async fn hincrbyfloat( + &self, + key: String, + field: String, + increment: f64, + ) -> Result { // https://valkey.io/commands/hincrbyfloat/ // HINCRBYFLOAT key field increment - let response = self.send(vec![ - Value::BulkString("HINCRBYFLOAT".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(increment.to_string()), - ])?; + let response = self + .send(vec![ + Value::BulkString("HINCRBYFLOAT".to_string()), + Value::BulkString(key), + Value::BulkString(field), + Value::BulkString(increment.to_string()), + ]) + .await?; match response { Value::BulkString(value) => Ok(value), Value::Error(err) => Err(Error::Valkey(err))?, @@ -508,14 +538,16 @@ impl GuestConnection for ValkeyConnection { } } - fn hkeys(&self, key: String) -> Result, Error> { + async fn hkeys(&self, key: String) -> Result, Error> { // https://valkey.io/commands/hkeys/ // HKEYS key - let response = self.send(vec![ - Value::BulkString("HKEYS".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("HKEYS".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Array(values) => { let mut keys = vec![]; @@ -539,14 +571,16 @@ impl GuestConnection for ValkeyConnection { } } - fn hlen(&self, key: String) -> Result { + async fn hlen(&self, key: String) -> Result { // https://valkey.io/commands/hlen/ // HLEN key - let response = self.send(vec![ - Value::BulkString("HLEN".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("HLEN".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Integer(value) => Ok(value as u64), Value::Error(err) => Err(Error::Valkey(err))?, @@ -557,7 +591,7 @@ impl GuestConnection for ValkeyConnection { } } - fn hmget(&self, key: String, fields: Vec) -> Result>, Error> { + async fn hmget(&self, key: String, fields: Vec) -> Result>, Error> { // https://valkey.io/commands/hmget/ // HMGET key field [ field ... ] @@ -568,7 +602,7 @@ impl GuestConnection for ValkeyConnection { for field in fields { cmd.push(Value::BulkString(field)); } - let response = self.send(cmd)?; + let response = self.send(cmd).await?; match response { Value::Array(items) => { let mut values = vec![]; @@ -592,7 +626,7 @@ impl GuestConnection for ValkeyConnection { } } - fn hmset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), Error> { + async fn hmset(&self, key: String, fields: Vec<(String, String)>) -> Result<(), Error> { // https://valkey.io/commands/hmset/ // HMSET key field value [ field value ... ] @@ -604,7 +638,7 @@ impl GuestConnection for ValkeyConnection { cmd.push(Value::BulkString(field)); cmd.push(Value::BulkString(value)); } - let response = self.send(cmd)?; + let response = self.send(cmd).await?; match response { Value::String(msg) => match msg.as_str() { "OK" => Ok(()), @@ -618,7 +652,7 @@ impl GuestConnection for ValkeyConnection { } } - fn hrandfield( + async fn hrandfield( &self, key: String, opts: Option, @@ -645,7 +679,7 @@ impl GuestConnection for ValkeyConnection { } } } - let response = self.send(cmd)?; + let response = self.send(cmd).await?; match response { Value::BulkString(value) => Ok(Some(vec![(value, None)])), Value::Array(items) => match items.len() { @@ -700,7 +734,7 @@ impl GuestConnection for ValkeyConnection { } } - fn hscan( + async fn hscan( &self, key: String, cursor: Option, @@ -729,7 +763,7 @@ impl GuestConnection for ValkeyConnection { } } } - let response = self.send(cmd)?; + let response = self.send(cmd).await?; match response { Value::Array(items) => { let cursor = match items[0].clone().into() { @@ -798,16 +832,18 @@ impl GuestConnection for ValkeyConnection { } } - fn hset(&self, key: String, field: String, value: String) -> Result<(), Error> { + async fn hset(&self, key: String, field: String, value: String) -> Result<(), Error> { // https://valkey.io/commands/hset/ // HSET key field value [ field value ... ] - let response = self.send(vec![ - Value::BulkString("HSET".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(value), - ])?; + let response = self + .send(vec![ + Value::BulkString("HSET".to_string()), + Value::BulkString(key), + Value::BulkString(field), + Value::BulkString(value), + ]) + .await?; match response { Value::Integer(_) => Ok(()), Value::Error(err) => Err(Error::Valkey(err))?, @@ -818,16 +854,18 @@ impl GuestConnection for ValkeyConnection { } } - fn hsetnx(&self, key: String, field: String, value: String) -> Result { + async fn hsetnx(&self, key: String, field: String, value: String) -> Result { // https://valkey.io/commands/hsetnx/ // HSETNX key field value - let response = self.send(vec![ - Value::BulkString("HSETNX".to_string()), - Value::BulkString(key), - Value::BulkString(field), - Value::BulkString(value), - ])?; + let response = self + .send(vec![ + Value::BulkString("HSETNX".to_string()), + Value::BulkString(key), + Value::BulkString(field), + Value::BulkString(value), + ]) + .await?; match response { Value::Integer(0) => Ok(false), Value::Integer(1) => Ok(true), @@ -839,15 +877,17 @@ impl GuestConnection for ValkeyConnection { } } - fn hstrlen(&self, key: String, field: String) -> Result { + async fn hstrlen(&self, key: String, field: String) -> Result { // https://valkey.io/commands/hstrlen/ // HSTRLEN key field - let response = self.send(vec![ - Value::BulkString("HSTRLEN".to_string()), - Value::BulkString(key), - Value::BulkString(field), - ])?; + let response = self + .send(vec![ + Value::BulkString("HSTRLEN".to_string()), + Value::BulkString(key), + Value::BulkString(field), + ]) + .await?; match response { Value::Integer(len) => Ok(len as u64), Value::Error(err) => Err(Error::Valkey(err))?, @@ -858,14 +898,16 @@ impl GuestConnection for ValkeyConnection { } } - fn hvals(&self, key: String) -> Result, Error> { + async fn hvals(&self, key: String) -> Result, Error> { // https://valkey.io/commands/hvals/ // HVALS key - let response = self.send(vec![ - Value::BulkString("HVALS".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("HVALS".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Array(items) => { let mut fields = vec![]; @@ -885,14 +927,16 @@ impl GuestConnection for ValkeyConnection { } } - fn incr(&self, key: String) -> Result { + async fn incr(&self, key: String) -> Result { // https://valkey.io/commands/incr/ // INCR key - let response = self.send(vec![ - Value::BulkString("INCR".to_string()), - Value::BulkString(key), - ])?; + let response = self + .send(vec![ + Value::BulkString("INCR".to_string()), + Value::BulkString(key), + ]) + .await?; match response { Value::Integer(value) => Ok(value), Value::Error(err) => Err(Error::Valkey(err))?, @@ -903,15 +947,17 @@ impl GuestConnection for ValkeyConnection { } } - fn incrby(&self, key: String, increment: i64) -> Result { + async fn incrby(&self, key: String, increment: i64) -> Result { // https://valkey.io/commands/incrby/ // INCRBY key increment - let response = self.send(vec![ - Value::BulkString("INCRBY".to_string()), - Value::BulkString(key), - Value::BulkString(increment.to_string()), - ])?; + let response = self + .send(vec![ + Value::BulkString("INCRBY".to_string()), + Value::BulkString(key), + Value::BulkString(increment.to_string()), + ]) + .await?; match response { Value::Integer(value) => Ok(value), Value::Error(err) => Err(Error::Valkey(err))?, @@ -922,14 +968,16 @@ impl GuestConnection for ValkeyConnection { } } - fn keys(&self, pattern: String) -> Result, Error> { + async fn keys(&self, pattern: String) -> Result, Error> { // https://valkey.io/commands/keys/ // KEYS pattern - let response = self.send(vec![ - Value::BulkString("KEYS".to_string()), - Value::BulkString(pattern), - ])?; + let response = self + .send(vec![ + Value::BulkString("KEYS".to_string()), + Value::BulkString(pattern), + ]) + .await?; match response { Value::Array(values) => { let mut keys = vec![]; @@ -953,12 +1001,14 @@ impl GuestConnection for ValkeyConnection { } } - fn ping(&self) -> Result<(), Error> { + async fn ping(&self) -> Result<(), Error> { // https://valkey.io/commands/ping/ // PING [ message ] // TODO support command options - let response = self.send(vec![Value::BulkString("PING".to_string())])?; + let response = self + .send(vec![Value::BulkString("PING".to_string())]) + .await?; match response { Value::String(msg) => match msg.as_str() { "PONG" => Ok(()), @@ -972,11 +1022,13 @@ impl GuestConnection for ValkeyConnection { } } - fn quit(&self) -> Result<(), Error> { + async fn quit(&self) -> Result<(), Error> { // https://valkey.io/commands/quit/ // QUIT - let response = self.send(vec![Value::BulkString("QUIT".to_string())])?; + let response = self + .send(vec![Value::BulkString("QUIT".to_string())]) + .await?; match response { Value::String(msg) => match msg.as_str() { "OK" => Ok(()), @@ -990,15 +1042,17 @@ impl GuestConnection for ValkeyConnection { } } - fn publish(&self, channel: String, message: String) -> Result { + async fn publish(&self, channel: String, message: String) -> Result { // https://valkey.io/commands/publish/ // PUBLISH channel message - let response = self.send(vec![ - Value::BulkString("PUBLISH".to_string()), - Value::BulkString(channel), - Value::BulkString(message), - ])?; + let response = self + .send(vec![ + Value::BulkString("PUBLISH".to_string()), + Value::BulkString(channel), + Value::BulkString(message), + ]) + .await?; match response { Value::Integer(value) => Ok(value), Value::Error(err) => Err(Error::Valkey(err))?, @@ -1009,7 +1063,7 @@ impl GuestConnection for ValkeyConnection { } } - fn set(&self, key: String, value: String) -> Result<(), Error> { + async fn set(&self, key: String, value: String) -> Result<(), Error> { // https://valkey.io/commands/set/ // SET key value // [ NX | XX | IFEQ comparison-value ] @@ -1017,11 +1071,13 @@ impl GuestConnection for ValkeyConnection { // [ EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL ] // TODO support command options - let response = self.send(vec![ - Value::BulkString("SET".to_string()), - Value::BulkString(key), - Value::BulkString(value), - ])?; + let response = self + .send(vec![ + Value::BulkString("SET".to_string()), + Value::BulkString(key), + Value::BulkString(value), + ]) + .await?; match response { Value::String(msg) => match msg.as_str() { "OK" => Ok(()), diff --git a/wit/store.wit b/wit/store.wit index d7958a3..6c8c21f 100644 --- a/wit/store.wit +++ b/wit/store.wit @@ -27,41 +27,41 @@ interface store { no-values: option, } - resource connection{ - send: func(command: list) -> result; + resource connection { + send: async func(command: list) -> result; - acl-deluser: func(username: string) -> result<_, error>; - acl-genpass: func() -> result; - acl-setuser: func(username: string, rules: list) -> result<_, error>; - auth: func(username: string, password: string) -> result<_, error>; - del: func(key: string) -> result<_, error>; - exists: func(key: string) -> result; - get: func(key: string) -> result, error>; - hdel: func(key: string, field: string) -> result<_, error>; - hello: func(opts: option) -> result>, error>; - hexists: func(key: string, field: string) -> result; - hget: func(key: string, field: string) -> result, error>; - hgetall: func(key: string) -> result>, error>; - hincrby: func(key: string, field: string, increment: s64) -> result; - hincrbyfloat: func(key: string, field: string, increment: f64) -> result; - hkeys: func(key: string) -> result, error>; - hlen: func(key: string) -> result; - hmget: func(key: string, fields: list) -> result>, error>; - hmset: func(key: string, fields: list>) -> result<_, error>; - hrandfield: func(key: string, opts: option) -> result>>>, error>; - hscan: func(key: string, cursor: option, opts: option) -> result, list>>>, error>; - hset: func(key: string, field: string, value: string) -> result<_, error>; - hsetnx: func(key: string, field: string, value: string) -> result; - hstrlen: func(key: string, field: string) -> result; - hvals: func(key: string) -> result, error>; - incr: func(key: string) -> result; - incrby: func(key: string, increment: s64) -> result; - keys: func(pattern: string) -> result, error>; - ping: func() -> result<_, error>; - publish: func(channel: string, message: string) -> result; - quit: func() -> result<_, error>; - set: func(key: string, value: string) -> result<_, error>; + acl-deluser: async func(username: string) -> result<_, error>; + acl-genpass: async func() -> result; + acl-setuser: async func(username: string, rules: list) -> result<_, error>; + auth: async func(username: string, password: string) -> result<_, error>; + del: async func(key: string) -> result<_, error>; + exists: async func(key: string) -> result; + get: async func(key: string) -> result, error>; + hdel: async func(key: string, field: string) -> result<_, error>; + hello: async func(opts: option) -> result>, error>; + hexists: async func(key: string, field: string) -> result; + hget: async func(key: string, field: string) -> result, error>; + hgetall: async func(key: string) -> result>, error>; + hincrby: async func(key: string, field: string, increment: s64) -> result; + hincrbyfloat: async func(key: string, field: string, increment: f64) -> result; + hkeys: async func(key: string) -> result, error>; + hlen: async func(key: string) -> result; + hmget: async func(key: string, fields: list) -> result>, error>; + hmset: async func(key: string, fields: list>) -> result<_, error>; + hrandfield: async func(key: string, opts: option) -> result>>>, error>; + hscan: async func(key: string, cursor: option, opts: option) -> result, list>>>, error>; + hset: async func(key: string, field: string, value: string) -> result<_, error>; + hsetnx: async func(key: string, field: string, value: string) -> result; + hstrlen: async func(key: string, field: string) -> result; + hvals: async func(key: string) -> result, error>; + incr: async func(key: string) -> result; + incrby: async func(key: string, increment: s64) -> result; + keys: async func(pattern: string) -> result, error>; + ping: async func() -> result<_, error>; + publish: async func(channel: string, message: string) -> result; + quit: async func() -> result<_, error>; + set: async func(key: string, value: string) -> result<_, error>; } - connect: func(host: string, port: u16, opts: option) -> result; + connect: async func(host: string, port: u16, opts: option) -> result; } From 35d0999a5a11352335dc0ea6dfd4df985d172381 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 9 Oct 2026 01:46:25 -0400 Subject: [PATCH 2/4] drop wasm32-wasip2 Signed-off-by: Scott Andrews --- components/dep-cli/Cargo.toml | 3 + components/dep-cli/src/{main.rs => lib.rs} | 68 ++++++++++++++++++---- components/ops/src/lib.rs | 10 ++-- components/wit/worlds.wit | 3 +- components/wkg.lock | 5 ++ make/components.mk | 15 ----- rust-toolchain.toml | 2 +- 7 files changed, 72 insertions(+), 34 deletions(-) rename components/dep-cli/src/{main.rs => lib.rs} (92%) diff --git a/components/dep-cli/Cargo.toml b/components/dep-cli/Cargo.toml index e22fb64..c2b0dca 100644 --- a/components/dep-cli/Cargo.toml +++ b/components/dep-cli/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2021" license = "Apache-2.0" +[lib] +crate-type = ["cdylib"] + [dependencies] clap = { version = "4.6.1", features = ["derive"] } wit-bindgen = { workspace = true } diff --git a/components/dep-cli/src/main.rs b/components/dep-cli/src/lib.rs similarity index 92% rename from components/dep-cli/src/main.rs rename to components/dep-cli/src/lib.rs index f452dc0..638d917 100644 --- a/components/dep-cli/src/main.rs +++ b/components/dep-cli/src/lib.rs @@ -3,7 +3,59 @@ use componentized::valkey::{ resp::{self, Value}, store::{connect, Error, HelloOpts, HrandfieldOpts, HscanOpts}, }; -use std::{fmt, process}; +use exports::wasi::cli0_3_0::run::Guest; +use std::fmt; +use wasi::cli0_3_0::{environment::get_arguments, stderr, stdout, types::ErrorCode}; +use wit_bindgen::{FutureReader, StreamReader}; + +/// Prints a line to stdout, like `std::println!`, which has no stdout on wasm32-unknown-unknown. +/// The write is awaited, so it is only usable in an async function. +macro_rules! println { + () => { + $crate::write_to(stdout::write_via_stream, String::from("\n")).await + }; + ($($arg:tt)*) => { + $crate::write_to(stdout::write_via_stream, format!("{}\n", format_args!($($arg)*))).await + }; +} + +/// Prints a line to stderr, like `std::eprintln!`, which has no stderr on wasm32-unknown-unknown. +/// The write is awaited, so it is only usable in an async function. +macro_rules! eprintln { + () => { + $crate::write_to(stderr::write_via_stream, String::from("\n")).await + }; + ($($arg:tt)*) => { + $crate::write_to(stderr::write_via_stream, format!("{}\n", format_args!($($arg)*))).await + }; +} + +/// Writes the value to the target, panics when it can't be written, like `std::println!`. +async fn write_to( + write_via_stream: fn(StreamReader) -> FutureReader>, + value: String, +) { + let (mut writer, reader) = wit_stream::new(); + let result = write_via_stream(reader); + writer.write_all(value.into_bytes()).await; + // the host finishes writing once the writable end is dropped + drop(writer); + if let Err(e) = result.await { + panic!("failed printing: {e:?}"); + } +} + +impl Guest for Cli { + async fn run() -> Result<(), ()> { + match exec().await { + Err(e) => { + eprintln!("Error: {e}"); + Err(()) + } + _ => Ok(()), + } + } +} #[derive(Parser)] #[command(name = "cli", version, about, long_about = None)] @@ -319,18 +371,8 @@ enum ACLCommands { }, } -fn main() { - match wit_bindgen::block_on(exec()) { - Err(e) => { - println!("Error: {e}"); - process::exit(1); - } - _ => {} - } -} - async fn exec() -> Result<(), Error> { - let cli = Cli::parse(); + let cli = Cli::parse_from(get_arguments().iter()); let opts = HelloOpts { proto_ver: Some(cli.proto_ver.to_string()), @@ -728,3 +770,5 @@ wit_bindgen::generate!({ path: "../wit", generate_all }); + +export!(Cli); diff --git a/components/ops/src/lib.rs b/components/ops/src/lib.rs index 04ba549..8aa2c56 100644 --- a/components/ops/src/lib.rs +++ b/components/ops/src/lib.rs @@ -10,13 +10,13 @@ use resp::{decode, encode}; use std::net::IpAddr; use std::vec; use wasi::io::streams::{InputStream, OutputStream, StreamError}; -use wasi::sockets::instance_network::instance_network; -use wasi::sockets::ip_name_lookup::resolve_addresses; -use wasi::sockets::network::{ +use wasi::sockets0_2_6::instance_network::instance_network; +use wasi::sockets0_2_6::ip_name_lookup::resolve_addresses; +use wasi::sockets0_2_6::network::{ ErrorCode, IpAddress, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, }; -use wasi::sockets::tcp::TcpSocket; -use wasi::sockets::tcp_create_socket::{create_tcp_socket, IpAddressFamily}; +use wasi::sockets0_2_6::tcp::TcpSocket; +use wasi::sockets0_2_6::tcp_create_socket::{create_tcp_socket, IpAddressFamily}; pub mod resp; diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index 54e5a01..a622d80 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -1,8 +1,9 @@ package componentized:valkey-components; world cli { - include wasi:cli/imports@0.2.6; + include wasi:cli/imports@0.3.0; import componentized:valkey/store@0.3.0-dev; + export wasi:cli/run@0.3.0; } world ops { diff --git a/components/wkg.lock b/components/wkg.lock index a1fd740..a0e3f89 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -11,6 +11,11 @@ requirement = "=0.2.6" version = "0.2.6" digest = "sha256:fdbe84136b3dd46d94305ef37f24f3cf04a70cc2026dca2592ac2ec0c9de15c7" +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:3a2d58743b67a057f7210b9a73d3b21b34ec7895ffe34d1cb092927307156e40" + [[packages]] name = "wasi:config" registry = "wasi.dev" diff --git a/make/components.mk b/make/components.mk index 3fa3bf6..012e5b8 100644 --- a/make/components.mk +++ b/make/components.mk @@ -52,19 +52,6 @@ ${COMPONENTS_DIR}/$1/$1.debug.wasm: components/$1/$1.wkg ${COMPONENTS_DIR}/$1/RE # cargo is checked last, other strategies may have a Cargo.toml for tests of non-rust sources else ifneq ($(wildcard components/$1/Cargo.toml),) -# a binary is a wasi:cli/command, the wasm32-wasip2 target builds it into a component -ifneq ($(wildcard components/$1/src/main.rs),) - -${COMPONENTS_DIR}/$1/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md - cargo build -p $1 --target wasm32-wasip2 --release - cp target/wasm32-wasip2/release/$1.wasm ${COMPONENTS_DIR}/$1/$1.wasm - -${COMPONENTS_DIR}/$1/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md - cargo build -p $1 --target wasm32-wasip2 - cp target/wasm32-wasip2/debug/$1.wasm ${COMPONENTS_DIR}/$1/$1.debug.wasm - -else - ${COMPONENTS_DIR}/$1/$1.wasm: Cargo.toml Cargo.lock components/wit/deps $(shell find components/$1 -type f) $(shell find crates -type f 2> /dev/null) ${COMPONENTS_DIR}/$1/README.md | $(call tool,wasm-tools) cargo build -p $1 --target wasm32-unknown-unknown --release $(WASM_TOOLS) component new target/wasm32-unknown-unknown/release/$(subst -,_,$1).wasm -o ${COMPONENTS_DIR}/$1/$1.wasm @@ -75,8 +62,6 @@ ${COMPONENTS_DIR}/$1/$1.debug.wasm: Cargo.toml Cargo.lock components/wit/deps $( endif -endif - ${COMPONENTS_DIR}/$1/README.md: components/$1/README.md @mkdir -p ${COMPONENTS_DIR}/$1 @cp components/$1/README.md ${COMPONENTS_DIR}/$1/README.md diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 8d82866..5459225 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] channel = "1.99" -targets = [ "wasm32-unknown-unknown", "wasm32-wasip2" ] +targets = [ "wasm32-unknown-unknown" ] From 7a8ee04a677d57eb9392eb87a3108fa77cf280db Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 9 Oct 2026 02:17:25 -0400 Subject: [PATCH 3/4] wasi:sockets@0.3.0 Signed-off-by: Scott Andrews --- components/ops/src/lib.rs | 136 +++++++++++++++++++++----------------- components/wit/worlds.wit | 2 +- components/wkg.lock | 5 ++ 3 files changed, 80 insertions(+), 63 deletions(-) diff --git a/components/ops/src/lib.rs b/components/ops/src/lib.rs index 8aa2c56..5397f26 100644 --- a/components/ops/src/lib.rs +++ b/components/ops/src/lib.rs @@ -7,16 +7,15 @@ use exports::componentized::valkey::store::{ Connection, Error, Guest as StoreGuest, GuestConnection, HelloOpts, HrandfieldOpts, HscanOpts, }; use resp::{decode, encode}; +use std::cell::RefCell; use std::net::IpAddr; use std::vec; -use wasi::io::streams::{InputStream, OutputStream, StreamError}; -use wasi::sockets0_2_6::instance_network::instance_network; -use wasi::sockets0_2_6::ip_name_lookup::resolve_addresses; -use wasi::sockets0_2_6::network::{ - ErrorCode, IpAddress, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, +use wasi::sockets::ip_name_lookup::{resolve_addresses, ErrorCode as LookupErrorCode}; +use wasi::sockets::types::{ + ErrorCode, IpAddress, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress, + TcpSocket, }; -use wasi::sockets0_2_6::tcp::TcpSocket; -use wasi::sockets0_2_6::tcp_create_socket::{create_tcp_socket, IpAddressFamily}; +use wit_bindgen::{FutureReader, StreamReader, StreamResult, StreamWriter}; pub mod resp; @@ -26,17 +25,22 @@ struct ValkeyOps; impl ValkeyOps { async fn open(address: IpSocketAddress) -> Result { let socket = match address { - IpSocketAddress::Ipv4(_) => create_tcp_socket(IpAddressFamily::Ipv4)?, - IpSocketAddress::Ipv6(_) => create_tcp_socket(IpAddressFamily::Ipv6)?, + IpSocketAddress::Ipv4(_) => TcpSocket::create(IpAddressFamily::Ipv4)?, + IpSocketAddress::Ipv6(_) => TcpSocket::create(IpAddressFamily::Ipv6)?, }; - socket.start_connect(&instance_network(), address)?; - socket.subscribe().block(); - let (input, output) = socket.finish_connect()?; + socket.connect(address).await?; + + // send and receive may each be called once per socket, the streams are kept for the life + // of the connection + let (output, send_stream) = wit_stream::new(); + let sent = socket.send(send_stream); + let (input, received) = socket.receive(); Ok(ValkeyConnection { - input, - output, - socket, + streams: RefCell::new(Some((output, input))), + _sent: sent, + _received: received, + _socket: socket, }) } @@ -68,7 +72,7 @@ impl ValkeyOps { } Err(_) => { // resolve as a hostname - Self::resolve_ip_addresses(host).await? + resolve_addresses(host.to_string()).await? } }; @@ -93,20 +97,6 @@ impl ValkeyOps { .collect(); Ok(socket_addresses) } - - async fn resolve_ip_addresses(host: &str) -> Result, Error> { - let network = instance_network(); - let address_stream = resolve_addresses(&network, host)?; - let mut addresses = vec![]; - loop { - address_stream.subscribe().block(); - match address_stream.resolve_next_address()? { - None => break, - Some(address) => addresses.push(address), - } - } - Ok(addresses) - } } impl StoreGuest for ValkeyOps { @@ -131,9 +121,31 @@ impl StoreGuest for ValkeyOps { } struct ValkeyConnection { - input: InputStream, - output: OutputStream, - socket: TcpSocket, + /// The streams writing to and reading from the socket, taken while a command is in flight, + /// a connection handles one command at a time. + streams: RefCell, StreamReader)>>, + _sent: FutureReader>, + _received: FutureReader>, + _socket: TcpSocket, +} + +impl ValkeyConnection { + async fn exchange( + output: &mut StreamWriter, + input: &mut StreamReader, + request: Vec, + ) -> Result, Error> { + if !output.write_all(request).await.is_empty() { + Err(Error::Client("Stream Closed".to_string()))? + } + + // TODO handle responses spanning multiple windows + match input.read(Vec::with_capacity(1024)).await { + (StreamResult::Complete(_), response) => Ok(response), + (StreamResult::Dropped, _) => Err(Error::Client("Stream Closed".to_string())), + (StreamResult::Cancelled, _) => Err(Error::Client("Stream Cancelled".to_string())), + } + } } impl GuestConnection for ValkeyConnection { @@ -141,15 +153,16 @@ impl GuestConnection for ValkeyConnection { let request = encode(Value::Array( command.into_iter().map(|c| c.into()).collect(), )); - self.socket.subscribe().block(); - self.output.blocking_write_and_flush(&request)?; - self.socket.subscribe().block(); - // TODO handle responses spanning multiple windows - let response = self.input.blocking_read(1024)?; - self.socket.subscribe().block(); + let (mut output, mut input) = self + .streams + .borrow_mut() + .take() + .ok_or_else(|| Error::Client("Connection busy with another command".to_string()))?; + let response = Self::exchange(&mut output, &mut input, request).await; + *self.streams.borrow_mut() = Some((output, input)); - decode(response).map(|r| r.into()) + decode(response?).map(|r| r.into()) } async fn acl_deluser(&self, username: String) -> Result<(), Error> { @@ -1096,33 +1109,44 @@ impl GuestConnection for ValkeyConnection { impl From for Error { fn from(e: ErrorCode) -> Self { match e { - ErrorCode::Unknown => Self::Client("Network Unknown".to_string()), ErrorCode::AccessDenied => Self::Client("Network AccessDenied".to_string()), ErrorCode::NotSupported => Self::Client("Network NotSupported".to_string()), ErrorCode::InvalidArgument => Self::Client("Network InvalidArgument".to_string()), ErrorCode::OutOfMemory => Self::Client("Network OutOfMemory".to_string()), ErrorCode::Timeout => Self::Client("Network Timeout".to_string()), - ErrorCode::ConcurrencyConflict => { - Self::Client("Network ConcurrencyConflict".to_string()) - } - ErrorCode::NotInProgress => Self::Client("Network NotInProgress".to_string()), - ErrorCode::WouldBlock => Self::Client("Network WouldBlock".to_string()), ErrorCode::InvalidState => Self::Client("Network InvalidState".to_string()), - ErrorCode::NewSocketLimit => Self::Client("Network NewSocketLimit".to_string()), ErrorCode::AddressNotBindable => Self::Client("Network AddressNotBindable".to_string()), ErrorCode::AddressInUse => Self::Client("Network AddressInUse".to_string()), ErrorCode::RemoteUnreachable => Self::Client("Network RemoteUnreachable".to_string()), ErrorCode::ConnectionRefused => Self::Client("Network ConnectionRefused".to_string()), + ErrorCode::ConnectionBroken => Self::Client("Network ConnectionBroken".to_string()), ErrorCode::ConnectionReset => Self::Client("Network ConnectionReset".to_string()), ErrorCode::ConnectionAborted => Self::Client("Network ConnectionAborted".to_string()), ErrorCode::DatagramTooLarge => Self::Client("Network DatagramTooLarge".to_string()), - ErrorCode::NameUnresolvable => Self::Client("Network NameUnresolvable".to_string()), - ErrorCode::TemporaryResolverFailure => { + ErrorCode::Other(None) => Self::Client("Network Other".to_string()), + ErrorCode::Other(Some(message)) => Self::Client(format!("Network Other: {message}")), + } + } +} + +impl From for Error { + fn from(e: LookupErrorCode) -> Self { + match e { + LookupErrorCode::AccessDenied => Self::Client("Network AccessDenied".to_string()), + LookupErrorCode::InvalidArgument => Self::Client("Network InvalidArgument".to_string()), + LookupErrorCode::NameUnresolvable => { + Self::Client("Network NameUnresolvable".to_string()) + } + LookupErrorCode::TemporaryResolverFailure => { Self::Client("Network TemporaryResolverFailure".to_string()) } - ErrorCode::PermanentResolverFailure => { + LookupErrorCode::PermanentResolverFailure => { Self::Client("Network PermanentResolverFailure".to_string()) } + LookupErrorCode::Other(None) => Self::Client("Network Other".to_string()), + LookupErrorCode::Other(Some(message)) => { + Self::Client(format!("Network Other: {message}")) + } } } } @@ -1137,18 +1161,6 @@ impl RespGuest for ValkeyOps { } } -impl From for Error { - fn from(e: StreamError) -> Self { - match e { - StreamError::LastOperationFailed(error) => Error::Client(format!( - "Stream LastOperationFailed: {}", - error.to_debug_string() - )), - StreamError::Closed => Error::Client("Stream Closed".to_string()), - } - } -} - impl From for NestedValue { fn from(value: Value) -> Self { ValkeyOps::encode(value).expect("values must encode cleanly") diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index a622d80..bbc38b9 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -7,7 +7,7 @@ world cli { } world ops { - include wasi:sockets/imports@0.2.6; + include wasi:sockets/imports@0.3.0; export componentized:valkey/resp@0.3.0-dev; export componentized:valkey/store@0.3.0-dev; } diff --git a/components/wkg.lock b/components/wkg.lock index a0e3f89..046274f 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -42,3 +42,8 @@ registry = "wasi.dev" requirement = "=0.2.6" version = "0.2.6" digest = "sha256:f3e112b5a7926758b82cfc884b5febfce8b924cda766dbdc1f4f60893f59b301" + +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:47d4cb10ad41dbd7f543693211876bf413793d4d8dc0aad08ead34bf44348fa1" From 4244e781c19f8f7583000a5b10ff6b9f9822af85 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 9 Oct 2026 10:59:20 -0400 Subject: [PATCH 4/4] Bannish linguring p2 APIs - wasi:http@0.2 -> 0.3 - wasi:keyvalue -> wasmcloud:keyvalue - add expire, expireat, hexpire, hexpireat operations Dropping wasi:keyvalue was required as the component model currently does not fully support calling a async method from a sync method. This restriction will be reverted once the component model restriction is resolved, or wasi:keyvalue's api becomes async. Until then, wasmcloud has an interface that is substantially similar and already async. Signed-off-by: Scott Andrews --- .config/wasm-pkg/config.toml | 1 + Cargo.lock | 74 +++++++ components/as-keyvalue/README.md | 5 +- components/as-keyvalue/src/lib.rs | 164 ++++++++------ components/client/README.md | 5 +- components/dep-cli/src/lib.rs | 4 +- components/ops/src/lib.rs | 200 +++++++++++++++++- components/sample-http-incrementor/Cargo.toml | 2 +- components/sample-http-incrementor/src/lib.rs | 62 +++--- .../wit/overrides/wasi-keyvalue/atomic.wit | 46 ---- .../wit/overrides/wasi-keyvalue/batch.wit | 63 ------ .../wit/overrides/wasi-keyvalue/store.wit | 122 ----------- .../wit/overrides/wasi-keyvalue/watch.wit | 16 -- .../wit/overrides/wasi-keyvalue/world.wit | 26 --- components/wit/worlds.wit | 13 +- components/wkg.lock | 32 +-- components/wkg.toml | 1 - make/wit.mk | 2 +- wit/store.wit | 27 +++ wkg.lock | 10 +- 20 files changed, 474 insertions(+), 401 deletions(-) delete mode 100644 components/wit/overrides/wasi-keyvalue/atomic.wit delete mode 100644 components/wit/overrides/wasi-keyvalue/batch.wit delete mode 100644 components/wit/overrides/wasi-keyvalue/store.wit delete mode 100644 components/wit/overrides/wasi-keyvalue/watch.wit delete mode 100644 components/wit/overrides/wasi-keyvalue/world.wit diff --git a/.config/wasm-pkg/config.toml b/.config/wasm-pkg/config.toml index ec58b31..358a73b 100644 --- a/.config/wasm-pkg/config.toml +++ b/.config/wasm-pkg/config.toml @@ -1,3 +1,4 @@ [namespace_registries] wasi = "wasi.dev" componentized = "componentized.dev" +wasmcloud = "wasmcloud.com" diff --git a/Cargo.lock b/Cargo.lock index 448b476..1a626fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,6 +137,67 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -224,6 +285,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "prettyplease" version = "0.2.29" @@ -312,6 +379,12 @@ dependencies = [ "serde", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "strsim" version = "0.11.1" @@ -466,6 +539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e473fd0095479f9689ac7d2a52c427cc96bb2b973ace50238dfcc1ab1cd52d93" dependencies = [ "bitflags", + "futures", "wit-bindgen-rust-macro", ] diff --git a/components/as-keyvalue/README.md b/components/as-keyvalue/README.md index 6f79f56..7c5671f 100644 --- a/components/as-keyvalue/README.md +++ b/components/as-keyvalue/README.md @@ -1,3 +1,6 @@ # as-keyvalue -Adapter to back wasi:keyvalue with the componentized:valkey client. +Adapter to back wasmcloud:keyvalue with the componentized:valkey client. + +> [!NOTE] +> `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. diff --git a/components/as-keyvalue/src/lib.rs b/components/as-keyvalue/src/lib.rs index ec18cb9..31966c0 100644 --- a/components/as-keyvalue/src/lib.rs +++ b/components/as-keyvalue/src/lib.rs @@ -1,10 +1,11 @@ #![no_main] -use componentized::valkey::store::{self as valkey, Connection, HelloOpts}; -use exports::wasi::keyvalue::atomics::{Cas, CasError, Guest as AtomicsGuest, GuestCas}; -use exports::wasi::keyvalue::batch::Guest as BatchGuest; -use exports::wasi::keyvalue::store::{ - Bucket, BucketBorrow, Error, Guest as StoreGuest, GuestBucket, KeyResponse, +use componentized::valkey::store::{self as valkey, Connection, Duration, HelloOpts, HscanOpts}; +use exports::wasmcloud::keyvalue::atomics::Guest as AtomicsGuest; +use exports::wasmcloud::keyvalue::batch::Guest as BatchGuest; +use exports::wasmcloud::keyvalue::store::Guest as StoreGuest; +use exports::wasmcloud::keyvalue::types::{ + Bucket, BucketBorrow, Error, Guest as TypesGuest, GuestBucket, KeyResponse, SetOptions, }; use wasi::config::store::{self as config}; @@ -19,12 +20,14 @@ const KEY_PREFIX_KEY: &str = "key-prefix"; const KEY_PREFIX_DEFAULT: &str = ""; #[derive(Debug, Clone)] -struct KeyvalueToValkey; +struct AsKeyvalue; -impl StoreGuest for KeyvalueToValkey { +impl TypesGuest for AsKeyvalue { type Bucket = KeyvalueToValkeyBucket; +} - fn open(identifier: String) -> Result { +impl StoreGuest for AsKeyvalue { + async fn open(identifier: String) -> Result { let hostname: String = config::get(HOSTNAME_KEY)?.unwrap_or(HOSTNAME_DEFAULT.to_string()); let port = config::get(PORT_KEY)?.unwrap_or(PORT_DEFAULT.to_string()); let port: u16 = port @@ -43,7 +46,9 @@ impl StoreGuest for KeyvalueToValkey { }, client_name: None, }; - let connection = wit_bindgen::block_on(valkey::connect(hostname, port, Some(opts)))?; + let connection = valkey::connect(hostname, port, Some(opts)) + .await + .map_err(|_| Error::StoreUnavailable)?; let key_prefix = config::get(KEY_PREFIX_KEY)?.unwrap_or(KEY_PREFIX_DEFAULT.to_string()); let hash_key = format!("{key_prefix}{identifier}"); @@ -61,78 +66,91 @@ struct KeyvalueToValkeyBucket { } impl GuestBucket for KeyvalueToValkeyBucket { - fn get(&self, key: String) -> Result>, Error> { - match wit_bindgen::block_on(self.connection.hget(self.hash_key.clone(), key))? { - Some(value) => Ok(Some(value.as_bytes().to_vec())), + async fn get(&self, key: String) -> Result>, Error> { + match self.connection.hget(self.hash_key.clone(), key).await? { + Some(value) => Ok(Some(value.into_bytes())), None => Ok(None), } } - fn set(&self, key: String, value: Vec) -> Result<(), Error> { + async fn set( + &self, + key: String, + value: Vec, + options: Option, + ) -> Result<(), Error> { let value = String::from_utf8(value).map_err(|e| Error::Other(e.to_string()))?; - Ok(wit_bindgen::block_on(self.connection.hset( - self.hash_key.clone(), - key, - value, - ))?) + let options = options.unwrap_or(SetOptions { + ttl_ms: None, + if_not_exists: false, + }); + + if options.if_not_exists { + match self + .connection + .hsetnx(self.hash_key.clone(), key.clone(), value) + .await? + { + true => (), + false => Err(Error::PreconditionFailed)?, + } + } else { + self.connection + .hset(self.hash_key.clone(), key.clone(), value) + .await? + } + if let Some(ttl_ms) = options.ttl_ms { + let ttl: Duration = ttl_ms / 1000; + self.connection + .hexpire(self.hash_key.clone(), ttl, None, vec![key]) + .await?; + } + Ok(()) } - fn delete(&self, key: String) -> Result<(), Error> { - Ok(wit_bindgen::block_on( - self.connection.hdel(self.hash_key.clone(), key), - )?) + async fn delete(&self, key: String) -> Result<(), Error> { + Ok(self.connection.hdel(self.hash_key.clone(), key).await?) } - fn exists(&self, key: String) -> Result { - Ok(wit_bindgen::block_on( - self.connection.hexists(self.hash_key.clone(), key), - )?) + async fn exists(&self, key: String) -> Result { + Ok(self.connection.hexists(self.hash_key.clone(), key).await?) } - fn list_keys(&self, cursor: Option) -> Result { - if cursor.is_some() { - Err(Error::Other("cursor is not supported".to_string()))?; - } + async fn list_keys( + &self, + prefix: Option, + cursor: Option, + ) -> Result { + let opts = HscanOpts { + match_: prefix.map(|prefix| format!("{}*", escape_glob(&prefix))), + count: None, + no_values: Some(true), + }; + let (cursor, fields) = self + .connection + .hscan(self.hash_key.clone(), cursor, Some(opts)) + .await?; Ok(KeyResponse { - cursor: None, - keys: wit_bindgen::block_on(self.connection.hkeys(self.hash_key.clone()))?, + keys: fields.into_iter().map(|(key, _)| key).collect(), + cursor, }) } } -impl AtomicsGuest for KeyvalueToValkey { - type Cas = KeyvalueToValkeyCas; - - fn increment(bucket: BucketBorrow<'_>, key: String, delta: i64) -> Result { +impl AtomicsGuest for AsKeyvalue { + async fn increment(bucket: BucketBorrow<'_>, key: String, delta: i64) -> Result { let bucket: &KeyvalueToValkeyBucket = bucket.get(); - Ok(wit_bindgen::block_on(bucket.connection.hincrby( - bucket.hash_key.clone(), - key, - delta, - ))?) - } - - fn swap(_cas: Cas, _value: Vec) -> Result<(), CasError> { - todo!() - } -} - -struct KeyvalueToValkeyCas; - -impl GuestCas for KeyvalueToValkeyCas { - fn new(_bucket: BucketBorrow<'_>, _key: String) -> Result { - todo!() - } - - fn current(&self) -> Result>, Error> { - todo!() + Ok(bucket + .connection + .hincrby(bucket.hash_key.clone(), key, delta) + .await?) } } -impl BatchGuest for KeyvalueToValkey { - fn get_many( +impl BatchGuest for AsKeyvalue { + async fn get_many( bucket: BucketBorrow<'_>, keys: Vec, ) -> Result)>>, Error> { @@ -140,7 +158,7 @@ impl BatchGuest for KeyvalueToValkey { let mut values: Vec)>> = vec![]; for key in keys { - let value = match bucket.get(key.clone())? { + let value = match bucket.get(key.clone()).await? { Some(value) => Some((key, value)), None => None, }; @@ -150,27 +168,43 @@ impl BatchGuest for KeyvalueToValkey { Ok(values) } - fn set_many(bucket: BucketBorrow<'_>, key_values: Vec<(String, Vec)>) -> Result<(), Error> { + async fn set_many( + bucket: BucketBorrow<'_>, + key_values: Vec<(String, Vec)>, + ) -> Result<(), Error> { let bucket: &KeyvalueToValkeyBucket = bucket.get(); for (key, value) in key_values { - bucket.set(key, value)?; + bucket.set(key, value, None).await?; } Ok(()) } - fn delete_many(bucket: BucketBorrow<'_>, keys: Vec) -> Result<(), Error> { + async fn delete_many(bucket: BucketBorrow<'_>, keys: Vec) -> Result<(), Error> { let bucket: &KeyvalueToValkeyBucket = bucket.get(); for key in keys { - bucket.delete(key)?; + bucket.delete(key).await?; } Ok(()) } } +/// Escapes the glob-style pattern characters Valkey's MATCH interprets, so a prefix matches +/// literally. +fn escape_glob(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + for c in value.chars() { + if matches!(c, '*' | '?' | '[' | ']' | '\\') { + escaped.push('\\'); + } + escaped.push(c); + } + escaped +} + impl From for Error { fn from(e: config::Error) -> Self { match e { @@ -193,4 +227,4 @@ wit_bindgen::generate!({ generate_all }); -export!(KeyvalueToValkey); +export!(AsKeyvalue); diff --git a/components/client/README.md b/components/client/README.md index 3ec0938..bf0b3f6 100644 --- a/components/client/README.md +++ b/components/client/README.md @@ -1,5 +1,8 @@ # client -Client for interacting with a Valkey server, as componentized:valkey and wasi:keyvalue. +Client for interacting with a Valkey server, as componentized:valkey and wasmcloud:keyvalue. + +> [!NOTE] +> `wasi:keyvalue` is currently unsupportable with an async based client. It will be restored when either it updates it's own API to be async, or the component model is fully able to make async calls from sync functions. Composes [`ops`](../ops/) with [`as-keyvalue`](../as-keyvalue/), exporting both. diff --git a/components/dep-cli/src/lib.rs b/components/dep-cli/src/lib.rs index 638d917..84cd6b4 100644 --- a/components/dep-cli/src/lib.rs +++ b/components/dep-cli/src/lib.rs @@ -3,9 +3,9 @@ use componentized::valkey::{ resp::{self, Value}, store::{connect, Error, HelloOpts, HrandfieldOpts, HscanOpts}, }; -use exports::wasi::cli0_3_0::run::Guest; +use exports::wasi::cli::run::Guest; use std::fmt; -use wasi::cli0_3_0::{environment::get_arguments, stderr, stdout, types::ErrorCode}; +use wasi::cli::{environment::get_arguments, stderr, stdout, types::ErrorCode}; use wit_bindgen::{FutureReader, StreamReader}; /// Prints a line to stdout, like `std::println!`, which has no stdout on wasm32-unknown-unknown. diff --git a/components/ops/src/lib.rs b/components/ops/src/lib.rs index 5397f26..43612f0 100644 --- a/components/ops/src/lib.rs +++ b/components/ops/src/lib.rs @@ -4,7 +4,8 @@ use exports::componentized::valkey::resp::{ Error as RespError, Guest as RespGuest, NestedValue, Value, }; use exports::componentized::valkey::store::{ - Connection, Error, Guest as StoreGuest, GuestConnection, HelloOpts, HrandfieldOpts, HscanOpts, + Connection, Duration, Error, ExpireMode, ExpireResult, Guest as StoreGuest, GuestConnection, + HelloOpts, HrandfieldOpts, HscanOpts, Instant, }; use resp::{decode, encode}; use std::cell::RefCell; @@ -299,6 +300,76 @@ impl GuestConnection for ValkeyConnection { } } + #[allow(async_fn_in_trait)] + async fn expire( + &self, + key: String, + after: Duration, + mode: Option, + ) -> Result { + // https://valkey.io/commands/exire/ + // EXPIRE key seconds [ NX | XX | GT | LT ] + + let mut cmd = vec![ + Value::BulkString("EXPIRE".to_string()), + Value::BulkString(key), + Value::Integer((after / 1000) as i64), + ]; + if let Some(mode) = mode { + cmd.push(mode.into()); + } + + let response = self.send(cmd).await?; + match response { + Value::Integer(code) => match code { + 0 => Ok(ExpireResult::ConditionNotMet), + 1 => Ok(ExpireResult::Applied), + _ => Err(Error::Client(format!("Unknown reply: {code}")))?, + }, + Value::Null => Err(Error::Client("Operation aborted".to_string()))?, + Value::Error(err) => Err(Error::Valkey(err))?, + response => Err(Error::Client(format!( + "Unexpected response type: {:?}", + response + )))?, + } + } + + #[allow(async_fn_in_trait)] + async fn expireat( + &self, + key: String, + after: Instant, + mode: Option, + ) -> Result { + // https://valkey.io/commands/exireat/ + // EXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] + + let mut cmd = vec![ + Value::BulkString("EXPIRE".to_string()), + Value::BulkString(key), + Value::Integer(after.seconds), + ]; + if let Some(mode) = mode { + cmd.push(mode.into()); + } + + let response = self.send(cmd).await?; + match response { + Value::Integer(code) => match code { + 0 => Ok(ExpireResult::ConditionNotMet), + 1 => Ok(ExpireResult::Applied), + _ => Err(Error::Client(format!("Unknown reply: {code}")))?, + }, + Value::Null => Err(Error::Client("Operation aborted".to_string()))?, + Value::Error(err) => Err(Error::Valkey(err))?, + response => Err(Error::Client(format!( + "Unexpected response type: {:?}", + response + )))?, + } + } + async fn get(&self, key: String) -> Result, Error> { // https://valkey.io/commands/get/ // GET key @@ -429,6 +500,122 @@ impl GuestConnection for ValkeyConnection { } } + #[allow(async_fn_in_trait)] + async fn hexpire( + &self, + key: String, + after: Duration, + mode: Option, + fields: Vec, + ) -> Result, Error> { + // https://valkey.io/commands/hexire/ + // HEXPIRE key seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + + let mut cmd = vec![ + Value::BulkString("HEXPIRE".to_string()), + Value::BulkString(key), + Value::Integer((after / 1000) as i64), + ]; + if let Some(mode) = mode { + cmd.push(mode.into()); + } + cmd.push(Value::BulkString("FIELDS".to_string())); + cmd.push(Value::Integer(fields.len() as i64)); + for field in fields { + cmd.push(Value::BulkString(field)); + } + + let response = self.send(cmd).await?; + match response { + Value::Array(codes) => { + let mut replies = vec![]; + for code in codes { + let code = code.into(); + match code { + Value::Integer(code) => { + replies.push(match code { + -2 => ExpireResult::DoesNotExist, + 0 => ExpireResult::ConditionNotMet, + 1 => ExpireResult::Applied, + 2 => ExpireResult::ZeroSeconds, + _ => Err(Error::Client(format!("Unknown reply: {code}")))?, + }); + } + value => Err(Error::Client(format!( + "Unexpected array item type: {:?}", + value + )))?, + } + } + Ok(replies) + } + Value::Null => Err(Error::Client("Operation aborted".to_string()))?, + Value::Error(err) => Err(Error::Valkey(err))?, + response => Err(Error::Client(format!( + "Unexpected response type: {:?}", + response + )))?, + } + } + + #[allow(async_fn_in_trait)] + async fn hexpireat( + &self, + key: String, + after: Instant, + mode: Option, + fields: Vec, + ) -> Result, Error> { + // https://valkey.io/commands/hexireat/ + // HEXPIREAT key unix-time-seconds [ NX | XX | GT | LT ] FIELDS numfields field [ field ... ] + + let mut cmd = vec![ + Value::BulkString("HEXPIREAT".to_string()), + Value::BulkString(key), + Value::Integer(after.seconds), + ]; + if let Some(mode) = mode { + cmd.push(mode.into()); + } + cmd.push(Value::BulkString("FIELDS".to_string())); + cmd.push(Value::Integer(fields.len() as i64)); + for field in fields { + cmd.push(Value::BulkString(field)); + } + + let response = self.send(cmd).await?; + match response { + Value::Array(codes) => { + let mut replies = vec![]; + for code in codes { + let code = code.into(); + match code { + Value::Integer(code) => { + replies.push(match code { + -2 => ExpireResult::DoesNotExist, + 0 => ExpireResult::ConditionNotMet, + 1 => ExpireResult::Applied, + 2 => ExpireResult::ZeroSeconds, + _ => Err(Error::Client(format!("Unknown reply: {code}")))?, + }); + } + value => Err(Error::Client(format!( + "Unexpected array item type: {:?}", + value + )))?, + } + } + Ok(replies) + } + Value::Null => Err(Error::Client("Operation aborted".to_string()))?, + Value::Error(err) => Err(Error::Valkey(err))?, + response => Err(Error::Client(format!( + "Unexpected response type: {:?}", + response + )))?, + } + } + async fn hget(&self, key: String, field: String) -> Result, Error> { // https://valkey.io/commands/hget/ // HGET key field @@ -1173,6 +1360,17 @@ impl From for Value { } } +impl From for Value { + fn from(mode: ExpireMode) -> Self { + match mode { + ExpireMode::Nx => Value::BulkString("NX".to_string()), + ExpireMode::Xx => Value::BulkString("XX".to_string()), + ExpireMode::Gt => Value::BulkString("GT".to_string()), + ExpireMode::Lt => Value::BulkString("LT".to_string()), + } + } +} + wit_bindgen::generate!({ world: "ops", path: "../wit", diff --git a/components/sample-http-incrementor/Cargo.toml b/components/sample-http-incrementor/Cargo.toml index 5d5e04f..4bda6c8 100644 --- a/components/sample-http-incrementor/Cargo.toml +++ b/components/sample-http-incrementor/Cargo.toml @@ -8,4 +8,4 @@ license = "Apache-2.0" crate-type = ["cdylib"] [dependencies] -wit-bindgen = { workspace = true } +wit-bindgen = { workspace = true, features = ["async-spawn"] } diff --git a/components/sample-http-incrementor/src/lib.rs b/components/sample-http-incrementor/src/lib.rs index c1a4125..e2efb03 100644 --- a/components/sample-http-incrementor/src/lib.rs +++ b/components/sample-http-incrementor/src/lib.rs @@ -1,50 +1,40 @@ #![no_main] -use exports::wasi::http::incoming_handler::Guest; -use wasi::http::types::{ - ErrorCode, Fields, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, -}; -use wasi::keyvalue::{ - atomics, - store::{self, Error}, -}; +use exports::wasi::http::handler::Guest; +use wasi::http::types::{ErrorCode, Fields, Request, Response}; +use wasmcloud::keyvalue::{atomics, store, types::Error}; #[derive(Debug, Clone)] struct SampleHttpIncrementor; impl SampleHttpIncrementor { - fn increment(path: &str) -> Result { - let bucket = store::open("http-incrementor")?; - atomics::increment(&bucket, path, 1) + async fn increment(path: &str) -> Result { + let bucket = store::open("http-incrementor".to_string()).await?; + atomics::increment(&bucket, path.to_string(), 1).await } } impl Guest for SampleHttpIncrementor { - fn handle(request: IncomingRequest, response_out: ResponseOutparam) { - let headers = Fields::new(); - let response = OutgoingResponse::new(headers); - let body = response.body().unwrap(); - - let path_with_query = request.path_with_query().unwrap(); - let parts: Vec<&str> = path_with_query.splitn(2, "?").collect(); - let path = *parts.get(0).unwrap(); - - match Self::increment(path) { - Ok(count) => { - ResponseOutparam::set(response_out, Ok(response)); - let out = body.write().expect("outgoing stream"); - out.blocking_write_and_flush(format!("{}\n", count).as_bytes()) - .expect("writing response"); - } - Err(err) => { - ResponseOutparam::set( - response_out, - Err(ErrorCode::InternalError(Some(err.to_string()))), - ); - } - } - - OutgoingBody::finish(body, None).unwrap(); + async fn handle(request: Request) -> Result { + let path_with_query = request.get_path_with_query().unwrap_or_default(); + let path = path_with_query.split('?').next().unwrap_or_default(); + + let count = Self::increment(path) + .await + .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?; + + let (mut contents, contents_reader) = wit_stream::new(); + // the response has no trailers, the dropped writer resolves the future to the default + let (_, trailers) = wit_future::new(|| Ok(None)); + let (response, _transmitted) = + Response::new(Fields::new(), Some(contents_reader), trailers); + + // the host reads the body once the response is returned, the body is written after + wit_bindgen::spawn_local(async move { + contents.write_all(format!("{count}\n").into_bytes()).await; + }); + + Ok(response) } } diff --git a/components/wit/overrides/wasi-keyvalue/atomic.wit b/components/wit/overrides/wasi-keyvalue/atomic.wit deleted file mode 100644 index 87d562a..0000000 --- a/components/wit/overrides/wasi-keyvalue/atomic.wit +++ /dev/null @@ -1,46 +0,0 @@ -/// A keyvalue interface that provides atomic operations. -/// -/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to -/// fail, it will appear to the invoker of the atomic operation that the action either completed -/// successfully or did nothing at all. -/// -/// Please note that this interface is bare functions that take a reference to a bucket. This is to -/// get around the current lack of a way to "extend" a resource with additional methods inside of -/// wit. Future version of the interface will instead extend these methods on the base `bucket` -/// resource. -interface atomics { - use store.{bucket, error}; - - /// The error returned by a CAS operation - variant cas-error { - /// A store error occurred when performing the operation - store-error(error), - /// The CAS operation failed because the value was too old. This returns a new CAS handle - /// for easy retries. Implementors MUST return a CAS handle that has been updated to the - /// latest version or transaction. - cas-failed(cas), - } - - /// A handle to a CAS (compare-and-swap) operation. - resource cas { - /// Construct a new CAS operation. Implementors can map the underlying functionality - /// (transactions, versions, etc) as desired. - new: static func(bucket: borrow, key: string) -> result; - /// Get the current value of the key (if it exists). This allows for avoiding reads if all - /// that is needed to ensure the atomicity of the operation - current: func() -> result>, error>; - } - - /// Atomically increment the value associated with the key in the store by the given delta. It - /// returns the new value. - /// - /// If the key does not exist in the store, it creates a new key-value pair with the value set - /// to the given delta. - /// - /// If any other error occurs, it returns an `Err(error)`. - increment: func(bucket: borrow, key: string, delta: s64) -> result; - - /// Perform the swap on a CAS operation. This consumes the CAS handle and returns an error if - /// the CAS operation failed. - swap: func(cas: cas, value: list) -> result<_, cas-error>; -} \ No newline at end of file diff --git a/components/wit/overrides/wasi-keyvalue/batch.wit b/components/wit/overrides/wasi-keyvalue/batch.wit deleted file mode 100644 index 70c05fe..0000000 --- a/components/wit/overrides/wasi-keyvalue/batch.wit +++ /dev/null @@ -1,63 +0,0 @@ -/// A keyvalue interface that provides batch operations. -/// -/// A batch operation is an operation that operates on multiple keys at once. -/// -/// Batch operations are useful for reducing network round-trip time. For example, if you want to -/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1 -/// batch get operation. The batch operation is faster because it only needs to make 1 network call -/// instead of 100. -/// -/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some -/// of the keys may have been modified and some may not. -/// -/// This interface does has the same consistency guarantees as the `store` interface, meaning that -/// you should be able to "read your writes." -/// -/// Please note that this interface is bare functions that take a reference to a bucket. This is to -/// get around the current lack of a way to "extend" a resource with additional methods inside of -/// wit. Future version of the interface will instead extend these methods on the base `bucket` -/// resource. -interface batch { - use store.{bucket, error}; - - /// Get the key-value pairs associated with the keys in the store. It returns a list of - /// key-value pairs. - /// - /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the - /// list. - /// - /// MAY show an out-of-date value if there are concurrent writes to the store. - /// - /// If any other error occurs, it returns an `Err(error)`. - get-many: func(bucket: borrow, keys: list) -> result>>>, error>; - - /// Set the values associated with the keys in the store. If the key already exists in the - /// store, it overwrites the value. - /// - /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. - /// - /// If any of the keys do not exist in the store, it creates a new key-value pair. - /// - /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not - /// rollback the key-value pairs that were already set. Thus, this batch operation does not - /// guarantee atomicity, implying that some key-value pairs could be set while others might - /// fail. - /// - /// Other concurrent operations may also be able to see the partial results. - set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; - - /// Delete the key-value pairs associated with the keys in the store. - /// - /// Note that the key-value pairs are not guaranteed to be deleted in the order they are - /// provided. - /// - /// If any of the keys do not exist in the store, it skips the key. - /// - /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not - /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not - /// guarantee atomicity, implying that some key-value pairs could be deleted while others might - /// fail. - /// - /// Other concurrent operations may also be able to see the partial results. - delete-many: func(bucket: borrow, keys: list) -> result<_, error>; -} diff --git a/components/wit/overrides/wasi-keyvalue/store.wit b/components/wit/overrides/wasi-keyvalue/store.wit deleted file mode 100644 index a35c2d4..0000000 --- a/components/wit/overrides/wasi-keyvalue/store.wit +++ /dev/null @@ -1,122 +0,0 @@ -/// A keyvalue interface that provides eventually consistent key-value operations. -/// -/// Each of these operations acts on a single key-value pair. -/// -/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is -/// the common denominator for all data types defined by different key-value stores to handle data, -/// ensuring compatibility between different key-value stores. Note: the clients will be expecting -/// serialization/deserialization overhead to be handled by the key-value store. The value could be -/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects. -/// -/// Data consistency in a key value store refers to the guarantee that once a write operation -/// completes, all subsequent read operations will return the value that was written. -/// -/// Any implementation of this interface must have enough consistency to guarantee "reading your -/// writes." In particular, this means that the client should never get a value that is older than -/// the one it wrote, but it MAY get a newer value if one was written around the same time. These -/// guarantees only apply to the same client (which will likely be provided by the host or an -/// external capability of some kind). In this context a "client" is referring to the caller or -/// guest that is consuming this interface. Once a write request is committed by a specific client, -/// all subsequent read requests by the same client will reflect that write or any subsequent -/// writes. Another client running in a different context may or may not immediately see the result -/// due to the replication lag. As an example of all of this, if a value at a given key is A, and -/// the client writes B, then immediately reads, it should get B. If something else writes C in -/// quick succession, then the client may get C. However, a client running in a separate context may -/// still see A or B -interface store { - /// The set of errors which may be raised by functions in this package - variant error { - /// The host does not recognize the store identifier requested. - no-such-store, - - /// The requesting component does not have access to the specified store - /// (which may or may not exist). - access-denied, - - /// Some implementation-specific error has occurred (e.g. I/O) - other(string) - } - - /// A response to a `list-keys` operation. - record key-response { - /// The list of keys returned by the query. - keys: list, - /// The continuation token to use to fetch the next page of keys. If this is `null`, then - /// there are no more keys to fetch. - cursor: option - } - - /// Get the bucket with the specified identifier. - /// - /// `identifier` must refer to a bucket provided by the host. - /// - /// `error::no-such-store` will be raised if the `identifier` is not recognized. - open: func(identifier: string) -> result; - - /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the - /// bucket, and the bucket itself acts as a collection of all these entries. - /// - /// It is worth noting that the exact terminology for bucket in key-value stores can very - /// depending on the specific implementation. For example: - /// - /// 1. Amazon DynamoDB calls a collection of key-value pairs a table - /// 2. Redis has hashes, sets, and sorted sets as different types of collections - /// 3. Cassandra calls a collection of key-value pairs a column family - /// 4. MongoDB calls a collection of key-value pairs a collection - /// 5. Riak calls a collection of key-value pairs a bucket - /// 6. Memcached calls a collection of key-value pairs a slab - /// 7. Azure Cosmos DB calls a collection of key-value pairs a container - /// - /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs - resource bucket { - /// Get the value associated with the specified `key` - /// - /// The value is returned as an option. If the key-value pair exists in the - /// store, it returns `Ok(value)`. If the key does not exist in the - /// store, it returns `Ok(none)`. - /// - /// If any other error occurs, it returns an `Err(error)`. - get: func(key: string) -> result>, error>; - - /// Set the value associated with the key in the store. If the key already - /// exists in the store, it overwrites the value. - /// - /// If the key does not exist in the store, it creates a new key-value pair. - /// - /// If any other error occurs, it returns an `Err(error)`. - set: func(key: string, value: list) -> result<_, error>; - - /// Delete the key-value pair associated with the key in the store. - /// - /// If the key does not exist in the store, it does nothing. - /// - /// If any other error occurs, it returns an `Err(error)`. - delete: func(key: string) -> result<_, error>; - - /// Check if the key exists in the store. - /// - /// If the key exists in the store, it returns `Ok(true)`. If the key does - /// not exist in the store, it returns `Ok(false)`. - /// - /// If any other error occurs, it returns an `Err(error)`. - exists: func(key: string) -> result; - - /// Get all the keys in the store with an optional cursor (for use in pagination). It - /// returns a list of keys. Please note that for most KeyValue implementations, this is a - /// can be a very expensive operation and so it should be used judiciously. Implementations - /// can return any number of keys in a single response, but they should never attempt to - /// send more data than is reasonable (i.e. on a small edge device, this may only be a few - /// KB, while on a large machine this could be several MB). Any response should also return - /// a cursor that can be used to fetch the next page of keys. See the `key-response` record - /// for more information. - /// - /// Note that the keys are not guaranteed to be returned in any particular order. - /// - /// If the store is empty, it returns an empty list. - /// - /// MAY show an out-of-date list of keys if there are concurrent writes to the store. - /// - /// If any error occurs, it returns an `Err(error)`. - list-keys: func(cursor: option) -> result; - } -} diff --git a/components/wit/overrides/wasi-keyvalue/watch.wit b/components/wit/overrides/wasi-keyvalue/watch.wit deleted file mode 100644 index ff13f75..0000000 --- a/components/wit/overrides/wasi-keyvalue/watch.wit +++ /dev/null @@ -1,16 +0,0 @@ -/// A keyvalue interface that provides watch operations. -/// -/// This interface is used to provide event-driven mechanisms to handle -/// keyvalue changes. -interface watcher { - /// A keyvalue interface that provides handle-watch operations. - use store.{bucket}; - - /// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket` - /// that can be used to interact with the store. - on-set: func(bucket: bucket, key: string, value: list); - - /// Handle the `delete` event for the given bucket and key. It includes a reference to the - /// `bucket` that can be used to interact with the store. - on-delete: func(bucket: bucket, key: string); -} \ No newline at end of file diff --git a/components/wit/overrides/wasi-keyvalue/world.wit b/components/wit/overrides/wasi-keyvalue/world.wit deleted file mode 100644 index fa26226..0000000 --- a/components/wit/overrides/wasi-keyvalue/world.wit +++ /dev/null @@ -1,26 +0,0 @@ -package wasi:keyvalue@0.2.0-draft2; - -/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores. -/// Components targeting this world will be able to do: -/// -/// 1. CRUD (create, read, update, delete) operations on key-value stores. -/// 2. Atomic `increment` and CAS (compare-and-swap) operations. -/// 3. Batch operations that can reduce the number of round trips to the network. -world imports { - /// The `store` capability allows the component to perform eventually consistent operations on - /// the key-value store. - import store; - - /// The `atomic` capability allows the component to perform atomic / `increment` and CAS - /// (compare-and-swap) operations. - import atomics; - - /// The `batch` capability allows the component to perform eventually consistent batch - /// operations that can reduce the number of round trips to the network. - import batch; -} - -world watch-service { - include imports; - export watcher; -} \ No newline at end of file diff --git a/components/wit/worlds.wit b/components/wit/worlds.wit index bbc38b9..895a998 100644 --- a/components/wit/worlds.wit +++ b/components/wit/worlds.wit @@ -15,13 +15,14 @@ world ops { world as-keyvalue { import componentized:valkey/store@0.3.0-dev; import wasi:config/store@0.2.0-rc.1; - export wasi:keyvalue/store@0.2.0-draft2; - export wasi:keyvalue/atomics@0.2.0-draft2; - export wasi:keyvalue/batch@0.2.0-draft2; + export wasmcloud:keyvalue/types@0.2.0; + export wasmcloud:keyvalue/store@0.2.0; + export wasmcloud:keyvalue/atomics@0.2.0; + export wasmcloud:keyvalue/batch@0.2.0; } world sample-http-incrementor { - import wasi:keyvalue/store@0.2.0-draft2; - import wasi:keyvalue/atomics@0.2.0-draft2; - export wasi:http/incoming-handler@0.2.6; + import wasmcloud:keyvalue/store@0.2.0; + import wasmcloud:keyvalue/atomics@0.2.0; + export wasi:http/handler@0.3.0; } diff --git a/components/wkg.lock b/components/wkg.lock index 046274f..64adf2e 100644 --- a/components/wkg.lock +++ b/components/wkg.lock @@ -7,14 +7,18 @@ name = "wasi:cli" registry = "wasi.dev" [[packages.versions]] -requirement = "=0.2.6" -version = "0.2.6" -digest = "sha256:fdbe84136b3dd46d94305ef37f24f3cf04a70cc2026dca2592ac2ec0c9de15c7" +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:3a2d58743b67a057f7210b9a73d3b21b34ec7895ffe34d1cb092927307156e40" + +[[packages]] +name = "wasi:clocks" +registry = "wasi.dev" [[packages.versions]] requirement = "=0.3.0" version = "0.3.0" -digest = "sha256:3a2d58743b67a057f7210b9a73d3b21b34ec7895ffe34d1cb092927307156e40" +digest = "sha256:59e1f4079e64ada450e19ffc9d08854c904e356ed8cc1fda36ff4fe150264db2" [[packages]] name = "wasi:config" @@ -30,20 +34,24 @@ name = "wasi:http" registry = "wasi.dev" [[packages.versions]] -requirement = "=0.2.6" -version = "0.2.6" -digest = "sha256:26a636620af0485dfcfc9a0563a0606564eedcf3fcabda031ab4d4ec0e505ff1" +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf7" [[packages]] name = "wasi:sockets" registry = "wasi.dev" -[[packages.versions]] -requirement = "=0.2.6" -version = "0.2.6" -digest = "sha256:f3e112b5a7926758b82cfc884b5febfce8b924cda766dbdc1f4f60893f59b301" - [[packages.versions]] requirement = "=0.3.0" version = "0.3.0" digest = "sha256:47d4cb10ad41dbd7f543693211876bf413793d4d8dc0aad08ead34bf44348fa1" + +[[packages]] +name = "wasmcloud:keyvalue" +registry = "wasmcloud.com" + +[[packages.versions]] +requirement = "=0.2.0" +version = "0.2.0" +digest = "sha256:a61448d7ffc36e0dc8433fad53691a6a1ca7da4d816355a32e4064f00e5eed83" diff --git a/components/wkg.toml b/components/wkg.toml index d204963..2e37d48 100644 --- a/components/wkg.toml +++ b/components/wkg.toml @@ -1,3 +1,2 @@ [overrides] "componentized:valkey" = { path = "../wit" } -"wasi:keyvalue" = { path = "./wit/overrides/wasi-keyvalue" } diff --git a/make/wit.mk b/make/wit.mk index f1e07d7..b2a341f 100644 --- a/make/wit.mk +++ b/make/wit.mk @@ -20,7 +20,7 @@ wit: $(WIT_DEPS) define FETCH_WIT # a package overridden with a local path, e.g. `{ path = "../wit" }`, has its dependencies fetched first, -# an override without dependencies of its own is skipped, e.g. `{ path = "./wit/overrides/wasi-keyvalue" }` +# an override without dependencies of its own is skipped, e.g. `{ path = "./wit/overrides/wasi-muppets" }` $(call wit_deps,$1): $1/wkg.toml $1/wkg.lock $(shell find $1/wit -type f -name "*.wit" -not -path "*/deps/*") $(filter $(WIT_DEPS),$(foreach path,$(shell sed -n 's/.*path *= *"\(.*\)".*/\1/p' $1/wkg.toml),$(call relpath,$1/$(path))/deps)) | $(call tool,wkg) $(if $(filter .,$1),,cd $1 && )$(WKG) fetch diff --git a/wit/store.wit b/wit/store.wit index 6c8c21f..b19cf73 100644 --- a/wit/store.wit +++ b/wit/store.wit @@ -1,4 +1,5 @@ interface store { + use wasi:clocks/system-clock@0.3.0.{ duration, instant }; use resp.{error as resp-error, value}; variant error { @@ -10,6 +11,28 @@ interface store { valkey(string), } + enum expire-mode { + /// Set expiration only when the field has no expiration. + nx, + /// Set expiration only when the field has an existing expiration. + xx, + /// Set expiration only when the new expiration is greater than current one. + gt, + /// Set expiration only when the new expiration is less than current one. + lt, + } + + enum expire-result { + /// -2: key does not exist, or field does not exist in the HASH. + does-not-exist, + /// 0: the specified NX | XX | GT | LT condition has not been met. + condition-not-met, + /// 1: the expiration time was applied. + applied, + /// 2: when called with 0 seconds. + zero-seconds, + } + record hello-opts { proto-ver: option, auth: option>, @@ -36,10 +59,14 @@ interface store { auth: async func(username: string, password: string) -> result<_, error>; del: async func(key: string) -> result<_, error>; exists: async func(key: string) -> result; + expire: async func(key: string, seconds: duration, mode: option) -> result; + expireat: async func(key: string, seconds: instant, mode: option) -> result; get: async func(key: string) -> result, error>; hdel: async func(key: string, field: string) -> result<_, error>; hello: async func(opts: option) -> result>, error>; hexists: async func(key: string, field: string) -> result; + hexpire: async func(key: string, seconds: duration, mode: option, fields: list) -> result, error>; + hexpireat: async func(key: string, seconds: instant, mode: option, fields: list) -> result, error>; hget: async func(key: string, field: string) -> result, error>; hgetall: async func(key: string) -> result>, error>; hincrby: async func(key: string, field: string, increment: s64) -> result; diff --git a/wkg.lock b/wkg.lock index 2ad4aa8..5b9a143 100644 --- a/wkg.lock +++ b/wkg.lock @@ -1,4 +1,12 @@ # This file is automatically generated. # It is not intended for manual editing. version = 1 -packages = [] + +[[packages]] +name = "wasi:clocks" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:59e1f4079e64ada450e19ffc9d08854c904e356ed8cc1fda36ff4fe150264db2"