diff --git a/Cargo.lock b/Cargo.lock index e53cbf9..607571c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2896,7 +2896,7 @@ dependencies = [ [[package]] name = "uzumibi-cli" -version = "0.7.1" +version = "0.8.0" dependencies = [ "clap", "dialoguer", @@ -2905,18 +2905,7 @@ dependencies = [ [[package]] name = "uzumibi-cloudflare-ext" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee0e8e2a653d9586d7c0e66379017f544e6db895a2deee0ec2a88e6ce6ca6ae" -dependencies = [ - "mrubyedge", - "mrubyedge-serde-json", - "uzumibi-gem 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "uzumibi-cloudflare-ext" -version = "0.2.2" +version = "0.3.1" dependencies = [ "mrubyedge", "mrubyedge-serde-json", @@ -2983,7 +2972,7 @@ dependencies = [ "mrubyedge", "mrubyedge-serde-json", "uzumibi-art-router 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", - "uzumibi-cloudflare-ext 0.2.1", + "uzumibi-cloudflare-ext", "uzumibi-gem 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/uzumibi-cli/Cargo.toml b/uzumibi-cli/Cargo.toml index 5260e26..07a4f8c 100644 --- a/uzumibi-cli/Cargo.toml +++ b/uzumibi-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uzumibi-cli" -version = "0.7.1" +version = "0.8.0" edition = "2024" authors = ["Uchio Kondo "] description = "Uzumibi CLI tool to generate serverless mruby/edge apps" diff --git a/uzumibi-cli/src/main.rs b/uzumibi-cli/src/main.rs index fd90967..ba68b09 100644 --- a/uzumibi-cli/src/main.rs +++ b/uzumibi-cli/src/main.rs @@ -374,10 +374,25 @@ fn print_project_next_steps(template: &str, _project_name: &str, features: &[Str println!(); println!(" 1. Install dependencies:"); println!(" \x1b[36mpnpm install\x1b[0m"); - println!(" 2. Build and start development server:"); - println!(" \x1b[36mpnpm run dev\x1b[0m"); - println!(" 3. Deploy to Cloudflare:"); - println!(" \x1b[36mpnpm run deploy\x1b[0m"); + if has_enable_external || has_queue { + println!(" 2. Set up a KV namespace for Uzumibi::KV:"); + println!(" • Create a new namespace:"); + println!(" \x1b[36mnpx wrangler kv namespace create UZUMIBI_KV\x1b[0m"); + println!(" • Or list existing namespaces to reuse one:"); + println!(" \x1b[36mnpx wrangler kv namespace list\x1b[0m"); + println!( + " Then replace \x1b[36m\x1b[0m in \x1b[36mwrangler.jsonc\x1b[0m with the namespace ID." + ); + println!(" 3. Build and start development server:"); + println!(" \x1b[36mpnpm run dev\x1b[0m"); + println!(" 4. Deploy to Cloudflare:"); + println!(" \x1b[36mpnpm run deploy\x1b[0m"); + } else { + println!(" 2. Build and start development server:"); + println!(" \x1b[36mpnpm run dev\x1b[0m"); + println!(" 3. Deploy to Cloudflare:"); + println!(" \x1b[36mpnpm run deploy\x1b[0m"); + } if has_queue { println!(); println!( @@ -394,7 +409,10 @@ fn print_project_next_steps(template: &str, _project_name: &str, features: &[Str " • \x1b[36mUzumibi::Fetch.fetch(url, method, body)\x1b[0m → Uzumibi::Response" ); println!( - " • \x1b[36mUzumibi::KV.get(key)\x1b[0m / \x1b[36mUzumibi::KV.set(key, value)\x1b[0m → Durable Object storage" + " • \x1b[36mUzumibi::KV.get(key)\x1b[0m / \x1b[36mUzumibi::KV.set(key, value)\x1b[0m → Cloudflare KV" + ); + println!( + " • \x1b[36mUzumibi::LegacyKV.get(key)\x1b[0m / \x1b[36mUzumibi::LegacyKV.set(key, value)\x1b[0m → Durable Object storage" ); println!( " • \x1b[36mUzumibi::Queue.send(queue_name, message)\x1b[0m → Cloudflare Queue" @@ -407,7 +425,10 @@ fn print_project_next_steps(template: &str, _project_name: &str, features: &[Str " • \x1b[36mUzumibi::Fetch.fetch(url, method, body)\x1b[0m → Uzumibi::Response" ); println!( - " • \x1b[36mUzumibi::KV.get(key)\x1b[0m / \x1b[36mUzumibi::KV.set(key, value)\x1b[0m → Durable Object storage" + " • \x1b[36mUzumibi::KV.get(key)\x1b[0m / \x1b[36mUzumibi::KV.set(key, value)\x1b[0m → Cloudflare KV" + ); + println!( + " • \x1b[36mUzumibi::LegacyKV.get(key)\x1b[0m / \x1b[36mUzumibi::LegacyKV.set(key, value)\x1b[0m → Durable Object storage" ); println!( " • \x1b[36mUzumibi::Queue.send(queue_name, message)\x1b[0m → Cloudflare Queue" 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 6c91ff7..537c981 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js @@ -5,7 +5,7 @@ import mod from "./$$PROJECT_NAME_UNDERSCORE$$.wasm"; const wasmModule = mod; /** - * Durable Object for Uzumibi::KV storage + * Durable Object storage retained for Uzumibi::LegacyKV. */ export class UzumibiKVObject extends DurableObject { async get(key) { @@ -27,7 +27,9 @@ export default { const query = new URL(request.url).searchParams; - // Durable Object stub (if binding exists) + const kv = env.UZUMIBI_KV ?? null; + + // Durable Object stub retained for Uzumibi::LegacyKV. const doStub = env.UZUMIBI_KV_DATA ? env.UZUMIBI_KV_DATA.getByName("default") : null; @@ -137,7 +139,35 @@ export default { return pos; }, - // KV.get(key) -> value string (via Durable Object) + // KV.get(key) -> value string + uzumibi_cf_kv_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!kv) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await kv.get(key); + if (value === null) { + return -1; + } + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) + uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + 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)); + + await kv.put(key, value); + return 0; + }, + + // LegacyKV.get(key) -> value string (via Durable Object) uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { if (!doStub) return -1; const memory = exports.memory; @@ -154,7 +184,7 @@ export default { return length; }, - // KV.set(key, value) (via Durable Object) + // LegacyKV.set(key, value) (via Durable Object) uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { if (!doStub) return -1; const memory = exports.memory; 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 9509fb9..cc0a682 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.2.2" +uzumibi-cloudflare-ext = ">= 0.3.1" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc index 08e3213..aec6c66 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc @@ -19,6 +19,17 @@ "directory": "./public", "binding": "ASSETS" }, + /** + * KV Namespace + * Used for Uzumibi::KV.get/set. + * Replace the placeholder with a namespace ID from `wrangler kv namespace create`. + */ + "kv_namespaces": [ + { + "binding": "UZUMIBI_KV", + "id": "" + } + ], /** * Queues * Used for Uzumibi::Queue.send @@ -36,7 +47,7 @@ // }, /** * Durable Objects - * Used for Uzumibi::KV.get/set + * Used for Uzumibi::LegacyKV.get/set * https://developers.cloudflare.com/durable-objects/ */ "durable_objects": { diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js index eb41615..c5c7f4d 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js @@ -5,7 +5,7 @@ import mod from "./$$PROJECT_NAME_UNDERSCORE$$_queue.wasm"; const wasmModule = mod; /** - * Durable Object for Uzumibi::KV storage + * Durable Object storage retained for Uzumibi::LegacyKV. */ export class UzumibiKVObject extends DurableObject { async get(key) { @@ -27,7 +27,9 @@ export default { const decoder = new TextDecoder(); const encoder = new TextEncoder(); - // Durable Object stub (if binding exists) + const kv = env.UZUMIBI_KV ?? null; + + // Durable Object stub retained for Uzumibi::LegacyKV. const doStub = env.UZUMIBI_KV_DATA ? env.UZUMIBI_KV_DATA.getByName("default") : null; @@ -133,7 +135,34 @@ export default { return pos; }, - // KV.get(key) -> value string (via Durable Object) + // KV.get(key) -> value string + uzumibi_cf_kv_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!kv) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await kv.get(key); + if (value === null) return -1; + + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) + uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + 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)); + + await kv.put(key, value); + return 0; + }, + + // LegacyKV.get(key) -> value string (via Durable Object) uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { if (!doStub) return -1; const memory = exports.memory; @@ -149,7 +178,7 @@ export default { return length; }, - // KV.set(key, value) (via Durable Object) + // LegacyKV.set(key, value) (via Durable Object) uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { if (!doStub) return -1; const memory = exports.memory; 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 c5fea81..e8e617d 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.2.0" +uzumibi-cloudflare-ext = ">= 0.3.1" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cli/templates/cloudflare/__features__/queue/wrangler.jsonc b/uzumibi-cli/templates/cloudflare/__features__/queue/wrangler.jsonc index 7f1510f..381de9c 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/queue/wrangler.jsonc +++ b/uzumibi-cli/templates/cloudflare/__features__/queue/wrangler.jsonc @@ -11,9 +11,20 @@ "observability": { "enabled": true }, + /** + * KV Namespace + * Used for Uzumibi::KV.get/set. + * Replace the placeholder with a namespace ID from `wrangler kv namespace create`. + */ + "kv_namespaces": [ + { + "binding": "UZUMIBI_KV", + "id": "" + } + ], /** * Durable Objects - * Used for Uzumibi::KV.get/set + * Used for Uzumibi::LegacyKV.get/set * https://developers.cloudflare.com/durable-objects/ */ "durable_objects": { diff --git a/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ b/uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_ index a3e9df6..d6b2e7c 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.2.0" +uzumibi-cloudflare-ext = ">= 0.3.1" mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-cli/tests/runn/new_cloudflare.yml b/uzumibi-cli/tests/runn/new_cloudflare.yml index bc3f64a..faa354b 100644 --- a/uzumibi-cli/tests/runn/new_cloudflare.yml +++ b/uzumibi-cli/tests/runn/new_cloudflare.yml @@ -70,6 +70,7 @@ steps: desc: Build the WASM app exec: command: cd {{ vars.tmpdir }}/{{ vars.project_name }}/wasm-app && cargo build --target wasm32-unknown-unknown --release + dump: steps test: current.exit_code == 0 start_server: diff --git a/uzumibi-cloudflare-ext/Cargo.toml b/uzumibi-cloudflare-ext/Cargo.toml index b96bda9..2b06540 100644 --- a/uzumibi-cloudflare-ext/Cargo.toml +++ b/uzumibi-cloudflare-ext/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uzumibi-cloudflare-ext" -version = "0.2.2" +version = "0.3.1" edition = "2024" authors = ["Uchio Kondo "] description = "Cloudflare Workers extension for Uzumibi (mruby/edge serverless framework)" diff --git a/uzumibi-cloudflare-ext/src/lib.rs b/uzumibi-cloudflare-ext/src/lib.rs index 13624dd..7ea8bb1 100644 --- a/uzumibi-cloudflare-ext/src/lib.rs +++ b/uzumibi-cloudflare-ext/src/lib.rs @@ -19,11 +19,14 @@ pub const PASS_ASSETS: u64 = 0xFEFFFFFF; // ---- Cloudflare-specific extern C declarations ---- +// Host functions are supplied by the Worker's JavaScript importObject under the "env" module. +#[link(wasm_import_module = "env")] unsafe extern "C" { unsafe fn debug_console_log(ptr: *const u8, len: usize); } #[cfg(feature = "queue")] +#[link(wasm_import_module = "env")] unsafe extern "C" { unsafe fn uzumibi_cf_message_ack(message_id_ptr: *const u8, message_id_size: usize) -> i32; unsafe fn uzumibi_cf_message_retry( @@ -34,6 +37,7 @@ unsafe extern "C" { } #[cfg(feature = "enable-external")] +#[link(wasm_import_module = "env")] unsafe extern "C" { unsafe fn uzumibi_cf_fetch( url_ptr: *const u8, @@ -47,6 +51,18 @@ unsafe extern "C" { result_ptr: *mut u8, result_max_size: usize, ) -> i32; + unsafe fn uzumibi_cf_kv_get( + key_ptr: *const u8, + key_size: usize, + result_ptr: *mut u8, + result_max_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_kv_set( + key_ptr: *const u8, + key_size: usize, + value_ptr: *const u8, + value_size: usize, + ) -> i32; unsafe fn uzumibi_cf_durable_object_get( key_ptr: *const u8, key_size: usize, @@ -117,6 +133,37 @@ fn cf_fetch(url: &str, method: &str, body: &str, headers: &[u8]) -> Result Result, String> { + const BUFFER_SIZE: usize = 65536; + let mut buffer = vec![0u8; BUFFER_SIZE]; + + unsafe { + let result = uzumibi_cf_kv_get(key.as_ptr(), key.len(), buffer.as_mut_ptr(), BUFFER_SIZE); + match result { + -1 => Ok(None), + len if len >= 0 => { + let len = len as usize; + let value = String::from_utf8(buffer[..len].to_vec()) + .map_err(|e| format!("Failed to decode UTF-8: {}", e))?; + Ok(Some(value)) + } + _ => Err(format!("Unexpected return value from kv_get: {}", result)), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_kv_set(key: &str, value: &str) -> Result<(), String> { + unsafe { + let result = uzumibi_cf_kv_set(key.as_ptr(), key.len(), value.as_ptr(), value.len()); + match result { + 0 => Ok(()), + _ => Err(format!("Failed to set value: return code {}", result)), + } + } +} + #[cfg(feature = "enable-external")] fn cf_durable_object_get(key: &str) -> Result, String> { const BUFFER_SIZE: usize = 65536; @@ -374,7 +421,7 @@ fn uzumibi_kv_class_get( let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; let key: String = key.as_ref().try_into()?; - match cf_durable_object_get(&key) { + match cf_kv_get(&key) { Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()), Ok(None) => Ok(RObject::nil().to_refcount_assigned()), Err(e) => Err(mrubyedge::Error::RuntimeError(format!( @@ -398,13 +445,54 @@ 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_durable_object_set(&key, &value).map_err(|e| { + cf_kv_set(&key, &value).map_err(|e| { mrubyedge::Error::RuntimeError(format!("Failed to set storage value: {}", e)) })?; Ok(RObject::boolean(true).to_refcount_assigned()) } +/// LegacyKV.get(key) +#[cfg(feature = "enable-external")] +fn uzumibi_legacy_kv_class_get( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + match cf_durable_object_get(&key) { + Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()), + Ok(None) => Ok(RObject::nil().to_refcount_assigned()), + Err(e) => Err(mrubyedge::Error::RuntimeError(format!( + "Failed to access legacy storage value: {}", + e + ))), + } +} + +/// LegacyKV.set(key, value) +#[cfg(feature = "enable-external")] +fn uzumibi_legacy_kv_class_set( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + let value_obj = &args[1]; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + + cf_durable_object_set(&key, &value).map_err(|e| { + mrubyedge::Error::RuntimeError(format!("Failed to set legacy storage value: {}", e)) + })?; + + Ok(RObject::boolean(true).to_refcount_assigned()) +} + /// Secret.get(key) -> String | nil #[cfg(feature = "enable-external")] fn uzumibi_secret_class_get( @@ -728,6 +816,21 @@ pub fn init_cloudflare_ext(vm: &mut VM) { 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)); + // Uzumibi::LegacyKV.get(key) / Uzumibi::LegacyKV.set(key, value) + let legacy_kv_class = vm.define_class("LegacyKV", None, Some(uzumibi_module.clone())); + mrb_define_class_cmethod( + vm, + legacy_kv_class.clone(), + "get", + Box::new(uzumibi_legacy_kv_class_get), + ); + mrb_define_class_cmethod( + vm, + legacy_kv_class, + "set", + Box::new(uzumibi_legacy_kv_class_set), + ); + // Uzumibi::Secret.get(key) let secret_class = vm.define_class("Secret", None, Some(uzumibi_module.clone())); mrb_define_class_cmethod(vm, secret_class, "get", Box::new(uzumibi_secret_class_get)); diff --git a/uzumibi-on-cloudflare-spike/.gitignore b/uzumibi-on-cloudflare-spike/.gitignore index 9ee5478..652ddaf 100644 --- a/uzumibi-on-cloudflare-spike/.gitignore +++ b/uzumibi-on-cloudflare-spike/.gitignore @@ -1,4 +1,5 @@ +.wrangler node_modules/* wasm-app/target src/*.wasm -*.mrb \ No newline at end of file +*.mrb diff --git a/uzumibi-on-cloudflare-spike/lib/app.rb b/uzumibi-on-cloudflare-spike/lib/app.rb index 4c9eaf5..6392cef 100644 --- a/uzumibi-on-cloudflare-spike/lib/app.rb +++ b/uzumibi-on-cloudflare-spike/lib/app.rb @@ -66,6 +66,20 @@ class App < Uzumibi::Router res end + get "/kv/:key" do |req, res| + value = Uzumibi::KV.get(req.params[:key]) + if value + res.return(200, { "Content-Type" => "text/plain" }, value) + else + res.return(404, { "Content-Type" => "text/plain" }, "Key not found\n") + end + end + + post "/kv/:key" do |req, res| + Uzumibi::KV.set(req.params[:key], req.body) + res.return(201, { "Content-Type" => "text/plain" }, "Stored\n") + end + get "/assets/*" do |req, res| fetch_assets end diff --git a/uzumibi-on-cloudflare-spike/src/index.js b/uzumibi-on-cloudflare-spike/src/index.js index 35de2e7..388a946 100644 --- a/uzumibi-on-cloudflare-spike/src/index.js +++ b/uzumibi-on-cloudflare-spike/src/index.js @@ -5,7 +5,7 @@ import mod from "./uzumibi_on_cloudflare_spike.wasm"; const wasmModule = mod; /** - * Durable Object for Uzumibi::KV storage + * Durable Object storage retained for Uzumibi::LegacyKV. */ export class UzumibiKVObject extends DurableObject { async get(key) { @@ -27,7 +27,9 @@ export default { const query = new URL(request.url).searchParams; - // Durable Object stub (if binding exists) + const kv = env.UZUMIBI_KV ?? null; + + // Durable Object stub retained for Uzumibi::LegacyKV. const doStub = env.UZUMIBI_KV_DATA ? env.UZUMIBI_KV_DATA.getByName("default") : null; @@ -137,7 +139,35 @@ export default { return pos; }, - // KV.get(key) -> value string (via Durable Object) + // KV.get(key) -> value string + uzumibi_cf_kv_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!kv) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await kv.get(key); + if (value === null) { + return -1; + } + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) + uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + 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)); + + await kv.put(key, value); + return 0; + }, + + // LegacyKV.get(key) -> value string (via Durable Object) uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { if (!doStub) return -1; const memory = exports.memory; @@ -154,7 +184,7 @@ export default { return length; }, - // KV.set(key, value) (via Durable Object) + // LegacyKV.set(key, value) (via Durable Object) uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { if (!doStub) return -1; const memory = exports.memory; diff --git a/uzumibi-on-cloudflare-spike/src/index.queue.js b/uzumibi-on-cloudflare-spike/src/index.queue.js index e18fa1f..55b0bfa 100644 --- a/uzumibi-on-cloudflare-spike/src/index.queue.js +++ b/uzumibi-on-cloudflare-spike/src/index.queue.js @@ -5,7 +5,7 @@ import mod from "./uzumibi_on_cloudflare_spike_queue.wasm"; const wasmModule = mod; /** - * Durable Object for Uzumibi::KV storage + * Durable Object storage retained for Uzumibi::LegacyKV. */ export class UzumibiKVObject extends DurableObject { async get(key) { @@ -27,7 +27,9 @@ export default { const decoder = new TextDecoder(); const encoder = new TextEncoder(); - // Durable Object stub (if binding exists) + const kv = env.UZUMIBI_KV ?? null; + + // Durable Object stub retained for Uzumibi::LegacyKV. const doStub = env.UZUMIBI_KV_DATA ? env.UZUMIBI_KV_DATA.getByName("default") : null; @@ -133,7 +135,34 @@ export default { return pos; }, - // KV.get(key) -> value string (via Durable Object) + // KV.get(key) -> value string + uzumibi_cf_kv_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!kv) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await kv.get(key); + if (value === null) return -1; + + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) + uzumibi_cf_kv_set: async (keyPtr, keySize, valuePtr, valueSize) => { + 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)); + + await kv.put(key, value); + return 0; + }, + + // LegacyKV.get(key) -> value string (via Durable Object) uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { if (!doStub) return -1; const memory = exports.memory; @@ -149,7 +178,7 @@ export default { return length; }, - // KV.set(key, value) (via Durable Object) + // LegacyKV.set(key, value) (via Durable Object) uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { if (!doStub) return -1; const memory = exports.memory; diff --git a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml index 8a94341..edb90b1 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml +++ b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml @@ -13,7 +13,7 @@ mrubyedge = { version = ">= 1.1.10", features = [ ], default-features = false } uzumibi-gem = ">= 0.6.0" uzumibi-art-router = ">= 0.3.1" -uzumibi-cloudflare-ext = ">= 0.2.0" +uzumibi-cloudflare-ext = { path = "../../uzumibi-cloudflare-ext" } mrubyedge-serde-json = ">= 0.1.2" [build-dependencies] diff --git a/uzumibi-on-cloudflare-spike/wrangler.jsonc b/uzumibi-on-cloudflare-spike/wrangler.jsonc index 1efd8e9..1f72b5b 100644 --- a/uzumibi-on-cloudflare-spike/wrangler.jsonc +++ b/uzumibi-on-cloudflare-spike/wrangler.jsonc @@ -30,9 +30,20 @@ "directory": "./public", "binding": "ASSETS" }, + /** + * KV Namespace + * Used for Uzumibi::KV.get/set. + * Replace the placeholder with a namespace ID from `wrangler kv namespace create`. + */ + "kv_namespaces": [ + { + "binding": "UZUMIBI_KV", + "id": "e0bc3ab9504b4fc78a5b33b847ddbd19" + } + ], /** * Durable Objects - * Used for Uzumibi::KV.get/set + * Used for Uzumibi::LegacyKV.get/set * https://developers.cloudflare.com/durable-objects/ */ "durable_objects": { diff --git a/uzumibi-on-cloudflare-spike/wrangler.queue.jsonc b/uzumibi-on-cloudflare-spike/wrangler.queue.jsonc index d988a97..aca22f2 100644 --- a/uzumibi-on-cloudflare-spike/wrangler.queue.jsonc +++ b/uzumibi-on-cloudflare-spike/wrangler.queue.jsonc @@ -11,9 +11,20 @@ "observability": { "enabled": true }, + /** + * KV Namespace + * Used for Uzumibi::KV.get/set. + * Replace the placeholder with a namespace ID from `wrangler kv namespace create`. + */ + "kv_namespaces": [ + { + "binding": "UZUMIBI_KV", + "id": "e0bc3ab9504b4fc78a5b33b847ddbd19" + } + ], /** * Durable Objects - * Used for Uzumibi::KV.get/set + * Used for Uzumibi::LegacyKV.get/set * https://developers.cloudflare.com/durable-objects/ */ "durable_objects": {