From 89f24f421963ae17d80d380f78ac668e67d4f8b6 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Tue, 1 Sep 2026 20:55:41 +0900 Subject: [PATCH 1/4] Add expiration options to Cloudflare KV Support expiration_ttl and expire_at on Uzumibi::KV.set. Pass options as JSON across the Wasm host boundary. Handle parse errors in both Cloudflare spike hosts. Co-authored-by: Codex --- Cargo.lock | 1 + uzumibi-cloudflare-ext/Cargo.toml | 1 + uzumibi-cloudflare-ext/src/lib.rs | 71 +++++++++++++++++-- uzumibi-on-cloudflare-spike/src/index.js | 24 ++++++- .../src/index.queue.js | 24 ++++++- 5 files changed, 110 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ec1726..e8c76f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2909,6 +2909,7 @@ version = "0.3.1" dependencies = [ "mrubyedge", "mrubyedge-serde-json", + "serde_json", "uzumibi-gem 0.6.0", ] diff --git a/uzumibi-cloudflare-ext/Cargo.toml b/uzumibi-cloudflare-ext/Cargo.toml index 2b06540..92ab671 100644 --- a/uzumibi-cloudflare-ext/Cargo.toml +++ b/uzumibi-cloudflare-ext/Cargo.toml @@ -12,6 +12,7 @@ mrubyedge = { version = ">= 1.1.10", features = [ ], default-features = false } uzumibi-gem = ">= 0.6.0" mrubyedge-serde-json = ">= 0.1.2" +serde_json = "1" [features] default = [] diff --git a/uzumibi-cloudflare-ext/src/lib.rs b/uzumibi-cloudflare-ext/src/lib.rs index 7ea8bb1..cd5e57a 100644 --- a/uzumibi-cloudflare-ext/src/lib.rs +++ b/uzumibi-cloudflare-ext/src/lib.rs @@ -17,6 +17,9 @@ use mrubyedge::yamrb::{ /// Special return value indicating that the request should be passed through to static assets. pub const PASS_ASSETS: u64 = 0xFEFFFFFF; +#[cfg(feature = "enable-external")] +const KV_SET_ERROR_INVALID_OPTIONS_JSON: i32 = -2; + // ---- Cloudflare-specific extern C declarations ---- // Host functions are supplied by the Worker's JavaScript importObject under the "env" module. @@ -62,6 +65,8 @@ unsafe extern "C" { key_size: usize, value_ptr: *const u8, value_size: usize, + options_ptr: *const u8, + options_size: usize, ) -> i32; unsafe fn uzumibi_cf_durable_object_get( key_ptr: *const u8, @@ -154,16 +159,55 @@ fn cf_kv_get(key: &str) -> Result, String> { } #[cfg(feature = "enable-external")] -fn cf_kv_set(key: &str, value: &str) -> Result<(), String> { +fn cf_kv_set(key: &str, value: &str, options_json: &str) -> Result<(), String> { unsafe { - let result = uzumibi_cf_kv_set(key.as_ptr(), key.len(), value.as_ptr(), value.len()); + let result = uzumibi_cf_kv_set( + key.as_ptr(), + key.len(), + value.as_ptr(), + value.len(), + options_json.as_ptr(), + options_json.len(), + ); match result { 0 => Ok(()), + KV_SET_ERROR_INVALID_OPTIONS_JSON => Err("Failed to parse KV options JSON".to_string()), _ => Err(format!("Failed to set value: return code {}", result)), } } } +#[cfg(feature = "enable-external")] +fn kv_set_options_json( + expiration_ttl: Option, + expire_at: Option, +) -> Result { + const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; + + if expiration_ttl.is_some() && expire_at.is_some() { + return Err("expiration_ttl and expire_at cannot be used together".to_string()); + } + if let Some(value) = expiration_ttl + && !(60..=MAX_SAFE_INTEGER).contains(&value) + { + return Err("expiration_ttl must be between 60 and 9007199254740991".to_string()); + } + if let Some(value) = expire_at + && !(1..=MAX_SAFE_INTEGER).contains(&value) + { + return Err("expire_at must be between 1 and 9007199254740991".to_string()); + } + + let mut options = serde_json::Map::new(); + if let Some(value) = expiration_ttl { + options.insert("expirationTtl".to_string(), value.into()); + } + if let Some(value) = expire_at { + options.insert("expiration".to_string(), value.into()); + } + Ok(serde_json::Value::Object(options).to_string()) +} + #[cfg(feature = "enable-external")] fn cf_durable_object_get(key: &str) -> Result, String> { const BUFFER_SIZE: usize = 65536; @@ -431,7 +475,7 @@ fn uzumibi_kv_class_get( } } -/// KV.set(key, value) +/// KV.set(key, value, expiration_ttl: nil, expire_at: nil) #[cfg(feature = "enable-external")] fn uzumibi_kv_class_set( vm: &mut VM, @@ -445,7 +489,24 @@ fn uzumibi_kv_class_set( let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; let value: String = value.as_ref().try_into()?; - cf_kv_set(&key, &value).map_err(|e| { + let (expiration_ttl, expire_at) = match vm.get_kwargs() { + Some(kwargs) => { + let expiration_ttl = match kwargs.get("expiration_ttl") { + Some(value) => Some(value.as_ref().try_into()?), + None => None, + }; + let expire_at = match kwargs.get("expire_at") { + Some(value) => Some(value.as_ref().try_into()?), + None => None, + }; + (expiration_ttl, expire_at) + } + None => (None, None), + }; + let options_json = + kv_set_options_json(expiration_ttl, expire_at).map_err(mrubyedge::Error::RuntimeError)?; + + cf_kv_set(&key, &value, &options_json).map_err(|e| { mrubyedge::Error::RuntimeError(format!("Failed to set storage value: {}", e)) })?; @@ -811,7 +872,7 @@ pub fn init_cloudflare_ext(vm: &mut VM) { Box::new(uzumibi_fetch_class_fetch), ); - // Uzumibi::KV.get(key) / Uzumibi::KV.set(key, value) + // Uzumibi::KV.get(key) / Uzumibi::KV.set(key, value, expiration_ttl:, expire_at:) let kv_class = vm.define_class("KV", None, Some(uzumibi_module.clone())); mrb_define_class_cmethod(vm, kv_class.clone(), "get", Box::new(uzumibi_kv_class_get)); mrb_define_class_cmethod(vm, kv_class, "set", Box::new(uzumibi_kv_class_set)); diff --git a/uzumibi-on-cloudflare-spike/src/index.js b/uzumibi-on-cloudflare-spike/src/index.js index 53b56b2..754ab33 100644 --- a/uzumibi-on-cloudflare-spike/src/index.js +++ b/uzumibi-on-cloudflare-spike/src/index.js @@ -4,6 +4,7 @@ import mod from "./uzumibi_on_cloudflare_spike.wasm"; import { RequestTooLargeError, writeRequestToWasm } from "./request-buffer.js"; const wasmModule = mod; +const KV_SET_ERROR_INVALID_OPTIONS_JSON = -2; /** * Durable Object storage retained for Uzumibi::LegacyKV. @@ -155,14 +156,31 @@ export default { return length; }, - // KV.set(key, value) - uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + // KV.set(key, value, options) + uzumibi_cf_kv_set: async ( + keyPtr, + keySize, + valuePtr, + valueSize, + optionsPtr, + optionsSize, + ) => { if (!kv) return -1; const memory = exports.memory; const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + const optionsJson = decoder.decode( + new Uint8Array(memory.buffer, optionsPtr, optionsSize), + ); + let options; + try { + options = JSON.parse(optionsJson); + } catch (error) { + console.error("Failed to parse KV options JSON", error); + return KV_SET_ERROR_INVALID_OPTIONS_JSON; + } - await kv.put(key, value); + await kv.put(key, value, options); return 0; }, diff --git a/uzumibi-on-cloudflare-spike/src/index.queue.js b/uzumibi-on-cloudflare-spike/src/index.queue.js index 46b3966..81d74ae 100644 --- a/uzumibi-on-cloudflare-spike/src/index.queue.js +++ b/uzumibi-on-cloudflare-spike/src/index.queue.js @@ -3,6 +3,7 @@ import { instantiate } from "asyncify-wasm"; import mod from "./uzumibi_on_cloudflare_spike_queue.wasm"; const wasmModule = mod; +const KV_SET_ERROR_INVALID_OPTIONS_JSON = -2; /** * Durable Object storage retained for Uzumibi::LegacyKV. @@ -151,14 +152,31 @@ export default { return length; }, - // KV.set(key, value) - uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + // KV.set(key, value, options) + uzumibi_cf_kv_set: async ( + keyPtr, + keySize, + valuePtr, + valueSize, + optionsPtr, + optionsSize, + ) => { if (!kv) return -1; const memory = exports.memory; const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + const optionsJson = decoder.decode( + new Uint8Array(memory.buffer, optionsPtr, optionsSize), + ); + let options; + try { + options = JSON.parse(optionsJson); + } catch (error) { + console.error("Failed to parse KV options JSON", error); + return KV_SET_ERROR_INVALID_OPTIONS_JSON; + } - await kv.put(key, value); + await kv.put(key, value, options); return 0; }, From cd6af46929a8b7e30111608bba0f3f7e0e267160 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Wed, 2 Sep 2026 22:41:11 +0900 Subject: [PATCH 2/4] Support KV expiration options in Cloudflare templates Pass KV options JSON in the HTTP and Queue hosts. Match the spike's JSON parse error handling. Co-authored-by: Codex --- .../__features__/enable-external/src/index.js | 24 ++++++++++++++++--- .../__features__/queue/src/index.js | 24 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js index 87096a4..82c49c5 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js @@ -4,6 +4,7 @@ import mod from "./$$PROJECT_NAME_UNDERSCORE$$.wasm"; import { RequestTooLargeError, writeRequestToWasm } from "./request-buffer.js"; const wasmModule = mod; +const KV_SET_ERROR_INVALID_OPTIONS_JSON = -2; /** * Durable Object storage retained for Uzumibi::LegacyKV. @@ -155,14 +156,31 @@ export default { return length; }, - // KV.set(key, value) - uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + // KV.set(key, value, options) + uzumibi_cf_kv_set: async ( + keyPtr, + keySize, + valuePtr, + valueSize, + optionsPtr, + optionsSize, + ) => { if (!kv) return -1; const memory = exports.memory; const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + const optionsJson = decoder.decode( + new Uint8Array(memory.buffer, optionsPtr, optionsSize), + ); + let options; + try { + options = JSON.parse(optionsJson); + } catch (error) { + console.error("Failed to parse KV options JSON", error); + return KV_SET_ERROR_INVALID_OPTIONS_JSON; + } - await kv.put(key, value); + await kv.put(key, value, options); return 0; }, diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js index b14babb..4a7cdeb 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js @@ -3,6 +3,7 @@ import { instantiate } from "asyncify-wasm"; import mod from "./$$PROJECT_NAME_UNDERSCORE$$_queue.wasm"; const wasmModule = mod; +const KV_SET_ERROR_INVALID_OPTIONS_JSON = -2; /** * Durable Object storage retained for Uzumibi::LegacyKV. @@ -151,14 +152,31 @@ export default { return length; }, - // KV.set(key, value) - uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + // KV.set(key, value, options) + uzumibi_cf_kv_set: async ( + keyPtr, + keySize, + valuePtr, + valueSize, + optionsPtr, + optionsSize, + ) => { if (!kv) return -1; const memory = exports.memory; const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + const optionsJson = decoder.decode( + new Uint8Array(memory.buffer, optionsPtr, optionsSize), + ); + let options; + try { + options = JSON.parse(optionsJson); + } catch (error) { + console.error("Failed to parse KV options JSON", error); + return KV_SET_ERROR_INVALID_OPTIONS_JSON; + } - await kv.put(key, value); + await kv.put(key, value, options); return 0; }, From a0f4bd84e7a3486478096ca850c5d9c2b749d12b Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 3 Sep 2026 23:52:59 +0900 Subject: [PATCH 3/4] Bumped uzumibi-cloudflare-ext version --- Cargo.lock | 2 +- .../__features__/enable-external/wasm-app/Cargo.toml_ | 2 +- .../cloudflare/__features__/queue/wasm-app/Cargo.toml_ | 2 +- uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ | 2 +- uzumibi-cloudflare-ext/Cargo.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8c76f2..a59565a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2905,7 +2905,7 @@ dependencies = [ [[package]] name = "uzumibi-cloudflare-ext" -version = "0.3.1" +version = "0.4.0" dependencies = [ "mrubyedge", "mrubyedge-serde-json", diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ index cc0a682..538ba33 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ @@ -12,7 +12,7 @@ mrubyedge = { version = ">= 1.1.12", features = [ ], default-features = false } uzumibi-gem = ">= 0.6.0" uzumibi-art-router = ">= 0.3.1" -uzumibi-cloudflare-ext = ">= 0.3.1" +uzumibi-cloudflare-ext = ">= 0.4.0" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/Cargo.toml_ b/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/Cargo.toml_ index e8e617d..7fc8a92 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/Cargo.toml_ +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/wasm-app/Cargo.toml_ @@ -12,7 +12,7 @@ mrubyedge = { version = ">= 1.1.12", features = [ ], default-features = false } uzumibi-gem = ">= 0.6.0" uzumibi-art-router = ">= 0.3.1" -uzumibi-cloudflare-ext = ">= 0.3.1" +uzumibi-cloudflare-ext = ">= 0.4.0" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ b/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ index d6b2e7c..a64da70 100644 --- a/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ +++ b/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ @@ -12,7 +12,7 @@ mrubyedge = { version = ">= 1.1.12", features = [ ], default-features = false } uzumibi-gem = ">= 0.6.0" uzumibi-art-router = ">= 0.3.1" -uzumibi-cloudflare-ext = ">= 0.3.1" +uzumibi-cloudflare-ext = ">= 0.4.0" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cloudflare-ext/Cargo.toml b/uzumibi-cloudflare-ext/Cargo.toml index 92ab671..692fc19 100644 --- a/uzumibi-cloudflare-ext/Cargo.toml +++ b/uzumibi-cloudflare-ext/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uzumibi-cloudflare-ext" -version = "0.3.1" +version = "0.4.0" edition = "2024" authors = ["Uchio Kondo "] description = "Cloudflare Workers extension for Uzumibi (mruby/edge serverless framework)" From 222cd51308a3f910c57c87bd91cdb9f3b782f0ed Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 3 Sep 2026 23:54:59 +0900 Subject: [PATCH 4/4] Bump cli --- Cargo.lock | 2 +- uzumibi-cli/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a59565a..d0f153d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,7 +2896,7 @@ dependencies = [ [[package]] name = "uzumibi-cli" -version = "0.8.1" +version = "0.8.2" dependencies = [ "clap", "dialoguer", diff --git a/uzumibi-cli/Cargo.toml b/uzumibi-cli/Cargo.toml index 09ddf0e..1558ae6 100644 --- a/uzumibi-cli/Cargo.toml +++ b/uzumibi-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uzumibi-cli" -version = "0.8.1" +version = "0.8.2" edition = "2024" authors = ["Uchio Kondo "] description = "Uzumibi CLI tool to generate serverless mruby/edge apps"