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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion uzumibi-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uzumibi-cli"
version = "0.8.1"
version = "0.8.2"
edition = "2024"
authors = ["Uchio Kondo <udzura@udzura.jp>"]
description = "Uzumibi CLI tool to generate serverless mruby/edge apps"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
24 changes: 21 additions & 3 deletions uzumibi-cli/templates/cloudflare/__features__/queue/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion uzumibi-cli/templates/cloudflare/wasm-app/Cargo.toml_
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
3 changes: 2 additions & 1 deletion uzumibi-cloudflare-ext/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "uzumibi-cloudflare-ext"
version = "0.3.1"
version = "0.4.0"
edition = "2024"
authors = ["Uchio Kondo <udzura@udzura.jp>"]
description = "Cloudflare Workers extension for Uzumibi (mruby/edge serverless framework)"
Expand All @@ -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 = []
Expand Down
71 changes: 66 additions & 5 deletions uzumibi-cloudflare-ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -154,16 +159,55 @@ fn cf_kv_get(key: &str) -> Result<Option<String>, 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<i64>,
expire_at: Option<i64>,
) -> Result<String, String> {
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<Option<String>, String> {
const BUFFER_SIZE: usize = 65536;
Expand Down Expand Up @@ -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,
Expand All @@ -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))
})?;

Expand Down Expand Up @@ -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));
Expand Down
24 changes: 21 additions & 3 deletions uzumibi-on-cloudflare-spike/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
},

Expand Down
24 changes: 21 additions & 3 deletions uzumibi-on-cloudflare-spike/src/index.queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
},

Expand Down
Loading