forge/arma/server/extension/src/transport.rs
Jacob Schmidt ffbfc70be8 Add checkout mutation and defer hot state saves
- Replace bank payment flow with a checkout mutation using explicit source context
- Return backend errors to players instead of silently falling back to local state
- Queue hot state persistence for actors, garages, lockers, orgs, and owned assets
2026-04-02 09:54:32 -05:00

953 lines
33 KiB
Rust

//! Shared transport helpers for oversized extension requests and responses.
//!
//! This module provides a routed invoke path that accepts JSON-encoded string
//! arguments, supports request staging for large payloads, and stores oversized
//! responses in memory for chunked retrieval by SQF.
use arma_rs::{CallContext, Group};
use serde::Serialize;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex as StdMutex};
use crate::{actor, bank, cad, garage, locker, org, v_garage, v_locker};
const CHUNK_PREFIX: &str = "FORGE_TRANSPORT_CHUNK:";
const RESPONSE_CHUNK_SIZE: usize = 12_000;
const UNSUPPORTED_ROUTE_PREFIX: &str = "Unsupported transport route";
static REQUEST_STORE: LazyLock<StdMutex<HashMap<String, String>>> =
LazyLock::new(|| StdMutex::new(HashMap::new()));
static RESPONSE_STORE: LazyLock<StdMutex<HashMap<String, Vec<String>>>> =
LazyLock::new(|| StdMutex::new(HashMap::new()));
static TRANSFER_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ChunkEnvelope {
transfer_id: String,
chunk_count: usize,
total_size: usize,
}
pub fn group() -> Group {
Group::new()
.command("invoke", invoke)
.command("invoke_stored", invoke_stored)
.group(
"request",
Group::new()
.command("append", append_request_chunk)
.command("clear", clear_request_chunks),
)
.group(
"response",
Group::new()
.command("get", get_response_chunk)
.command("clear", clear_response_chunks),
)
}
fn append_request_chunk(transfer_id: String, chunk: String) -> String {
let mut store = REQUEST_STORE.lock().unwrap();
store.entry(transfer_id).or_default().push_str(&chunk);
"OK".to_string()
}
fn clear_request_chunks(transfer_id: String) -> String {
REQUEST_STORE.lock().unwrap().remove(&transfer_id);
"OK".to_string()
}
fn get_response_chunk(transfer_id: String, index: String) -> String {
let chunk_index = match index.parse::<usize>() {
Ok(value) => value,
Err(error) => return format!("Error: Invalid response chunk index: {error}"),
};
let store = RESPONSE_STORE.lock().unwrap();
let Some(chunks) = store.get(&transfer_id) else {
return format!("Error: Response transfer '{transfer_id}' was not found");
};
chunks.get(chunk_index).cloned().unwrap_or_else(|| {
format!(
"Error: Response chunk {} was not found for '{}'",
chunk_index, transfer_id
)
})
}
fn clear_response_chunks(transfer_id: String) -> String {
RESPONSE_STORE.lock().unwrap().remove(&transfer_id);
"OK".to_string()
}
fn invoke(call_context: CallContext, function_name: String, arguments_json: String) -> String {
invoke_internal(call_context, function_name, arguments_json)
}
fn invoke_stored(call_context: CallContext, function_name: String, transfer_id: String) -> String {
let Some(arguments_json) = REQUEST_STORE.lock().unwrap().remove(&transfer_id) else {
return format!("Error: Request transfer '{transfer_id}' was not found");
};
invoke_internal(call_context, function_name, arguments_json)
}
fn invoke_internal(
call_context: CallContext,
function_name: String,
arguments_json: String,
) -> String {
let arguments: Vec<String> = match parse_transport_arguments(&arguments_json) {
Ok(value) => value,
Err(error) => return format!("Error: Invalid transport arguments JSON: {error}"),
};
let result = match route_command(call_context, &function_name, arguments) {
Ok(value) => value,
Err(error) => format!("Error: {error}"),
};
chunk_response_if_needed(result)
}
fn parse_transport_arguments(arguments_json: &str) -> Result<Vec<String>, String> {
let value: serde_json::Value =
serde_json::from_str(arguments_json).map_err(|error| error.to_string())?;
parse_transport_argument_value(value)
}
fn parse_transport_argument_value(value: serde_json::Value) -> Result<Vec<String>, String> {
match value {
serde_json::Value::Array(values) => Ok(values
.into_iter()
.map(|entry| match entry {
serde_json::Value::String(string_value) => string_value,
other => other.to_string(),
})
.collect()),
serde_json::Value::String(value) => {
let trimmed = value.trim();
if trimmed.starts_with('[') || trimmed.starts_with('{') || trimmed.eq("null") {
if let Ok(nested_value) = serde_json::from_str::<serde_json::Value>(trimmed) {
return parse_transport_argument_value(nested_value);
}
}
Ok(vec![value])
}
serde_json::Value::Null => Ok(Vec::new()),
other => Err(format!("expected string or array but received {}", other)),
}
}
fn route_command(
call_context: CallContext,
function_name: &str,
arguments: Vec<String>,
) -> Result<String, String> {
match function_name {
"actor:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::get_actor(call_context, arguments[0].clone()))
}
"actor:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::create_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::update_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::actor_exists(call_context, arguments[0].clone()))
}
"actor:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::delete_actor(call_context, arguments[0].clone()))
}
"actor:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::init_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::get_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::override_hot_actor(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"actor:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::save_hot_actor(call_context, arguments[0].clone()))
}
"actor:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::remove_hot_actor(call_context, arguments[0].clone()))
}
"bank:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::get_bank(call_context, arguments[0].clone()))
}
"bank:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::create_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::update_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::bank_exists(call_context, arguments[0].clone()))
}
"bank:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::delete_bank(call_context, arguments[0].clone()))
}
"bank:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::init_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::get_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::override_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:hot:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(bank::patch_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"bank:hot:charge_checkout" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::charge_checkout_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:deposit" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::deposit_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:withdraw" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::withdraw_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:deposit_earnings" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::deposit_earnings_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:transfer" => {
expect_arg_count(function_name, &arguments, 4)?;
Ok(bank::transfer_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
arguments[3].clone(),
))
}
"bank:hot:validate_pin" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(bank::validate_pin_hot_bank(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"bank:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::save_hot_bank(call_context, arguments[0].clone()))
}
"bank:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(bank::remove_hot_bank(call_context, arguments[0].clone()))
}
"org:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_org(arguments[0].clone()))
}
"org:create" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::create_org(arguments[0].clone(), arguments[1].clone()))
}
"org:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_org(arguments[0].clone(), arguments[1].clone()))
}
"org:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::org_exists(arguments[0].clone()))
}
"org:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::delete_org(arguments[0].clone()))
}
"org:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::init_hot_org(arguments[0].clone()))
}
"org:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_hot_org(arguments[0].clone()))
}
"org:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::override_hot_org(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::save_hot_org(arguments[0].clone()))
}
"org:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::remove_hot_org(arguments[0].clone()))
}
"org:assets:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_assets(arguments[0].clone()))
}
"org:assets:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_assets(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:fleet:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_fleet(arguments[0].clone()))
}
"org:fleet:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::update_fleet(
arguments[0].clone(),
arguments[1].clone(),
))
}
"org:members:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::get_members(arguments[0].clone()))
}
"org:members:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::add_member(arguments[0].clone(), arguments[1].clone()))
}
"org:members:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(org::remove_member(
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::create_garage(call_context, arguments[0].clone()))
}
"garage:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::get_garage(call_context, arguments[0].clone()))
}
"garage:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::add_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::update_garage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::patch_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::remove_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::delete_garage(call_context, arguments[0].clone()))
}
"garage:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::garage_exists(call_context, arguments[0].clone()))
}
"garage:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::init_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::get_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::override_hot_garage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::save_hot_garage(call_context, arguments[0].clone()))
}
"garage:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(garage::remove_hot_garage(
call_context,
arguments[0].clone(),
))
}
"garage:hot:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::add_hot_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"garage:hot:remove_vehicle" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(garage::remove_hot_vehicle(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::create_locker(call_context, arguments[0].clone()))
}
"locker:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::get_locker(call_context, arguments[0].clone()))
}
"locker:add" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::add_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:update" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::update_locker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:patch" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::patch_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:remove" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::remove_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::delete_locker(call_context, arguments[0].clone()))
}
"locker:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::locker_exists(call_context, arguments[0].clone()))
}
"locker:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::init_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:get" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::get_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(locker::override_hot_locker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"locker:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::save_hot_locker(call_context, arguments[0].clone()))
}
"locker:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(locker::remove_hot_locker(
call_context,
arguments[0].clone(),
))
}
"owned:garage:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::create_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::fetch_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::get_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::add_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:remove" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::remove_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::delete_vgarage(call_context, arguments[0].clone()))
}
"owned:garage:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::vgarage_exists(call_context, arguments[0].clone()))
}
"owned:garage:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::init_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::fetch_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::get_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_garage::override_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:garage:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::save_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_garage::remove_hot_vgarage(
call_context,
arguments[0].clone(),
))
}
"owned:garage:hot:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::add_hot_vgarage(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:garage:hot:remove_item" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_garage::remove_hot_vgarage_item(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::create_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::fetch_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::get_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:add" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_locker::add_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:remove" => {
expect_arg_count(function_name, &arguments, 3)?;
Ok(v_locker::remove_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
arguments[2].clone(),
))
}
"owned:locker:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::delete_vlocker(call_context, arguments[0].clone()))
}
"owned:locker:exists" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::vlocker_exists(call_context, arguments[0].clone()))
}
"owned:locker:hot:init" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::init_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:fetch" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::fetch_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:get" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::get_hot_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(v_locker::override_hot_vlocker(
call_context,
arguments[0].clone(),
arguments[1].clone(),
))
}
"owned:locker:hot:save" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::save_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"owned:locker:hot:remove" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(v_locker::remove_hot_vlocker(
call_context,
arguments[0].clone(),
))
}
"cad:activity:append" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::append_activity(arguments[0].clone()))
}
"cad:activity:recent" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::recent_activity(arguments[0].clone()))
}
"cad:assignments:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_assignments())
}
"cad:assignments:assign" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::assign_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:acknowledge" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::acknowledge_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:decline" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::decline_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_assignment(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:assignments:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_assignment(arguments[0].clone()))
}
"cad:orders:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_orders())
}
"cad:orders:create" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::create_order(arguments[0].clone()))
}
"cad:orders:create_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::create_order_from_context(arguments[0].clone()))
}
"cad:orders:close" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::close_order(arguments[0].clone()))
}
"cad:orders:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_order(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:orders:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_order(arguments[0].clone()))
}
"cad:requests:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_requests())
}
"cad:requests:submit" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::submit_request(arguments[0].clone()))
}
"cad:requests:submit_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::submit_request_from_context(arguments[0].clone()))
}
"cad:requests:close" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::close_request(arguments[0].clone()))
}
"cad:requests:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_request(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:requests:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_request(arguments[0].clone()))
}
"cad:profiles:list" => {
expect_arg_count(function_name, &arguments, 0)?;
Ok(cad::list_profiles())
}
"cad:profiles:update_from_context" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::update_profile_from_context(arguments[0].clone()))
}
"cad:profiles:upsert" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(cad::upsert_profile(
arguments[0].clone(),
arguments[1].clone(),
))
}
"cad:profiles:delete" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::delete_profile(arguments[0].clone()))
}
"cad:groups:build" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::build_groups(arguments[0].clone()))
}
"cad:view:hydrate" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(cad::hydrate_view(arguments[0].clone()))
}
_ => Err(format!(
"{UNSUPPORTED_ROUTE_PREFIX} for function '{function_name}'"
)),
}
}
fn expect_arg_count(
function_name: &str,
arguments: &[String],
expected_count: usize,
) -> Result<(), String> {
if arguments.len() == expected_count {
return Ok(());
}
Err(format!(
"Transport route '{}' expected {} arguments but received {}",
function_name,
expected_count,
arguments.len()
))
}
fn chunk_response_if_needed(result: String) -> String {
if result.len() <= RESPONSE_CHUNK_SIZE {
return result;
}
let transfer_id = next_transfer_id("rsp");
let chunks = split_string_chunks(&result, RESPONSE_CHUNK_SIZE);
let envelope = ChunkEnvelope {
transfer_id: transfer_id.clone(),
chunk_count: chunks.len(),
total_size: result.len(),
};
RESPONSE_STORE.lock().unwrap().insert(transfer_id, chunks);
format!(
"{CHUNK_PREFIX}{}",
serde_json::to_string(&envelope)
.unwrap_or_else(|error| format!("{{\"error\":\"{error}\"}}"))
)
}
fn next_transfer_id(prefix: &str) -> String {
let sequence = TRANSFER_SEQUENCE.fetch_add(1, Ordering::Relaxed);
format!("{prefix}_{sequence}")
}
fn split_string_chunks(input: &str, max_bytes: usize) -> Vec<String> {
if input.is_empty() {
return vec![String::new()];
}
let mut chunks = Vec::new();
let mut chunk_start = 0usize;
let mut chunk_len = 0usize;
for (index, character) in input.char_indices() {
let char_len = character.len_utf8();
if chunk_len > 0 && chunk_len + char_len > max_bytes {
chunks.push(input[chunk_start..index].to_string());
chunk_start = index;
chunk_len = 0;
}
chunk_len += char_len;
}
chunks.push(input[chunk_start..].to_string());
chunks
}