Merge branch 'v0.10.0' into dr/sqlite-overhaul

This commit is contained in:
doria 2024-12-22 16:28:48 -05:00 committed by GitHub
commit 619c6c465c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 921 additions and 459 deletions

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1890,7 +1890,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1813,7 +1813,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1774,7 +1774,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1763,7 +1763,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -1313,7 +1313,7 @@ dependencies = [
name = "get_block"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.10.0 (git+https://github.com/kinode-dao/process_lib?rev=ea8490a)",
"kinode_process_lib",
"serde",
"serde_json",
"wit-bindgen",
@ -1761,29 +1761,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=d97e012#d97e012842dd4cc0e036d5de5048064e770302ab"
dependencies = [
"alloy",
"alloy-primitives",
"alloy-sol-macro",
"alloy-sol-types",
"anyhow",
"bincode",
"http",
"mime_guess",
"rand",
"rmp-serde",
"serde",
"serde_json",
"thiserror 1.0.69",
"url",
"wit-bindgen",
]
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",
@ -1810,7 +1788,7 @@ dependencies = [
"alloy-sol-types",
"anyhow",
"hex",
"kinode_process_lib 0.10.0 (git+https://github.com/kinode-dao/process_lib?rev=ea8490a)",
"kinode_process_lib",
"process_macros",
"rmp-serde",
"serde",
@ -1943,7 +1921,7 @@ dependencies = [
name = "node_info"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.10.0 (git+https://github.com/kinode-dao/process_lib?rev=ea8490a)",
"kinode_process_lib",
"process_macros",
"serde",
"serde_json",
@ -2420,7 +2398,7 @@ dependencies = [
name = "reset"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.10.0 (git+https://github.com/kinode-dao/process_lib?rev=ea8490a)",
"kinode_process_lib",
"process_macros",
"serde",
"serde_json",
@ -2824,7 +2802,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
name = "state"
version = "0.1.0"
dependencies = [
"kinode_process_lib 0.10.0 (git+https://github.com/kinode-dao/process_lib?rev=d97e012)",
"kinode_process_lib",
"process_macros",
"serde",
"serde_json",

View File

@ -37,8 +37,6 @@ const KIMAP_FIRST_BLOCK: u64 = kimap::KIMAP_FIRST_BLOCK; // optimism
#[cfg(feature = "simulation-mode")]
const KIMAP_FIRST_BLOCK: u64 = 1; // local
const CURRENT_VERSION: u32 = 1;
const MAX_PENDING_ATTEMPTS: u8 = 3;
const SUBSCRIPTION_TIMEOUT: u64 = 60;
const DELAY_MS: u64 = 1_000; // 1s
@ -65,7 +63,7 @@ impl State {
contract_address: eth::Address::from_str(KIMAP_ADDRESS).unwrap(),
names: HashMap::new(),
nodes: HashMap::new(),
last_checkpoint_block: 0,
last_checkpoint_block: KIMAP_FIRST_BLOCK,
}
}
@ -165,15 +163,20 @@ enum KnsError {
call_init!(init);
fn init(our: Address) {
// state is checkpointed regularly (default every 5 minutes if new events are found)
let state = State::load();
println!("started");
if let Err(e) = main(our, state) {
println!("fatal error: {e}");
// state is checkpointed regularly (default every 5 minutes if new events are found)
let mut state = State::load();
loop {
if let Err(e) = main(&our, &mut state) {
println!("fatal error: {e}");
break;
}
}
}
fn main(our: Address, mut state: State) -> anyhow::Result<()> {
fn main(our: &Address, state: &mut State) -> anyhow::Result<()> {
#[cfg(feature = "simulation-mode")]
add_temp_hardcoded_tlzs(&mut state);
@ -214,7 +217,7 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
// if they do time out, we try them again
let eth_provider: eth::Provider = eth::Provider::new(state.chain_id, SUBSCRIPTION_TIMEOUT);
// subscribe to logs first, so no logs are m issed
// subscribe to logs first, so no logs are missed
eth_provider.subscribe_loop(1, mints_filter.clone(), 2, 0);
eth_provider.subscribe_loop(2, notes_filter.clone(), 2, 0);
@ -230,14 +233,14 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
print_to_terminal(2, &format!("syncing old logs from block: {}", last_block));
fetch_and_process_logs(
&eth_provider,
&mut state,
state,
mints_filter.clone(),
&mut pending_notes,
&mut last_block,
);
fetch_and_process_logs(
&eth_provider,
&mut state,
state,
notes_filter.clone(),
&mut pending_notes,
&mut last_block,
@ -266,12 +269,13 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
source,
body,
capabilities,
expects_response,
..
} = message
else {
if tick {
handle_eth_message(
&mut state,
state,
&eth_provider,
tick,
checkpoint,
@ -287,7 +291,7 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
if source.node() == our.node() && source.process == "eth:distro:sys" {
handle_eth_message(
&mut state,
state,
&eth_provider,
tick,
checkpoint,
@ -298,48 +302,41 @@ fn main(our: Address, mut state: State) -> anyhow::Result<()> {
&mut last_block,
)?;
} else {
match serde_json::from_slice(&body)? {
let response_body = match serde_json::from_slice(&body)? {
IndexerRequest::NamehashToName(NamehashToNameRequest { ref hash, .. }) => {
// TODO: make sure we've seen the whole block, while actually
// sending a response to the proper place.
Response::new()
.body(IndexerResponse::Name(state.names.get(hash).cloned()))
.send()?;
IndexerResponse::Name(state.names.get(hash).cloned())
}
IndexerRequest::NodeInfo(NodeInfoRequest { ref name, .. }) => {
Response::new()
.body(IndexerResponse::NodeInfo(
state.nodes.get(name).map(|n| n.clone().into()),
))
.send()?;
IndexerResponse::NodeInfo(state.nodes.get(name).map(|n| n.clone().into()))
}
IndexerRequest::Reset => {
// check for root capability
let root_cap = Capability {
issuer: our.clone(),
params: "{\"root\":true}".to_string(),
};
if source.package_id() != our.package_id() {
if !capabilities.contains(&root_cap) {
Response::new()
.body(IndexerResponse::Reset(ResetResult::Err(
ResetError::NoRootCap,
)))
.send()?;
continue;
}
let root_cap = Capability::new(our.clone(), "{\"root\":true}");
if source.package_id() != our.package_id() && !capabilities.contains(&root_cap)
{
IndexerResponse::Reset(ResetResult::Err(ResetError::NoRootCap))
} else {
// reload state fresh - this will create new db
state.reset();
IndexerResponse::Reset(ResetResult::Success)
}
// reload state fresh - this will create new db
state.reset();
}
IndexerRequest::GetState(_) => IndexerResponse::GetState(state.clone().into()),
};
if let IndexerResponse::Reset(ResetResult::Success) = response_body {
println!("resetting state");
if expects_response.is_some() {
Response::new()
.body(IndexerResponse::Reset(ResetResult::Success))
.send()?;
panic!("resetting state, restarting!");
}
IndexerRequest::GetState(_) => {
Response::new()
.body(IndexerResponse::GetState(state.clone().into()))
.send()?;
return Ok(());
} else {
if expects_response.is_some() {
Response::new().body(response_body).send()?;
}
}
}

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1751,7 +1751,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -0,0 +1,87 @@
interface settings {
variant request {
/// lazy-load-blob: none.
hi(hi-request),
/// lazy-load-blob: none.
peer-id(string),
/// lazy-load-blob: none.
eth-config(eth-config-request),
/// lazy-load-blob: none.
shutdown,
/// lazy-load-blob: none.
reset,
/// lazy-load-blob: none.
kill-process(string),
/// lazy-load-blob: none.
set-stylesheet(string),
}
type response = result<option<settings-data>, settings-error>;
record hi-request {
node: string,
content: string,
timeout: u64,
}
/// A subset of the actions that can be taken on the `eth`
/// runtime module. These are mostly used by the settings frontend.
variant eth-config-request {
add-provider(provider-config),
remove-provider(tuple<u64, string>),
set-public,
set-private,
allow-node(string),
unallow-node(string),
deny-node(string),
undeny-node(string),
}
/// This will be converted to the ProviderConfig type used in `eth`.
/// `trusted` in ProviderConfig will always be true.
/// Rather than provide full NodeOrRpcUrl, the settings
/// process will fetch KNS update for node-providers
/// and convert this to the type used in `eth`.
record provider-config {
chain-id: u64,
node-or-rpc-url: node-or-rpc-url,
}
variant node-or-rpc-url {
node(string),
rpc-url(string),
}
variant settings-data {
peer-id(identity),
}
record identity {
name: string,
networking-key: string,
routing: node-routing,
}
variant node-routing {
routers(list<string>),
direct(direct),
}
record direct {
ip: string,
ports: list<tuple<string, u16>>,
}
variant settings-error {
hi-timeout,
hi-offline,
kernel-nonresponsive,
malformed-request,
state-fetch-failed,
}
}
world settings-sys-v0 {
import settings;
include process-v1;
}

View File

@ -165,7 +165,7 @@
margin-left: 6px;
}
</style>
<script type="module" crossorigin src="/settings:settings:sys/assets/index-CwCaX2Ut.js"></script>
<script type="module" crossorigin src="/settings:settings:sys/assets/index-CepIUSQF.js"></script>
<link rel="stylesheet" crossorigin href="/settings:settings:sys/assets/index-iGirBDd0.css">
</head>

View File

@ -1,6 +1,11 @@
use crate::kinode::process::settings::{
Direct, EthConfigRequest as SettingsEthConfigAction, HiRequest, Identity as SettingsIdentity,
NodeOrRpcUrl as SettingsNodeOrRpcUrl, NodeRouting as SettingsNodeRouting,
Request as SettingsRequest, Response as SettingsResponse, SettingsData, SettingsError,
};
use kinode_process_lib::{
await_message, call_init, eth, get_blob, get_capability, homepage, http, kernel_types, kimap,
net, println, Address, Capability, LazyLoadBlob, Message, NodeId, ProcessId, Request, Response,
net, println, Address, Capability, LazyLoadBlob, Message, ProcessId, Request, Response,
SendError, SendErrorKind,
};
use serde::{Deserialize, Serialize};
@ -8,36 +13,12 @@ use std::{collections::HashMap, vec};
const ICON: &str = include_str!("icon");
#[derive(Debug, Serialize, Deserialize)]
enum SettingsRequest {
Hi {
node: NodeId,
content: String,
timeout: u64,
},
PeerId(NodeId),
EthConfig(eth::EthConfigAction),
Shutdown,
Reset,
KillProcess(ProcessId),
SetStylesheet(String),
}
type SettingsResponse = Result<Option<SettingsData>, SettingsError>;
#[derive(Debug, Serialize, Deserialize)]
enum SettingsData {
PeerId(net::Identity),
}
#[derive(Debug, Serialize, Deserialize)]
enum SettingsError {
HiTimeout,
HiOffline,
KernelNonresponsive,
MalformedRequest,
StateFetchFailed,
}
wit_bindgen::generate!({
path: "target/wit",
world: "settings-sys-v0",
generate_unused_types: true,
additional_derives: [serde::Deserialize, serde::Serialize],
});
/// never gets persisted
#[derive(Debug, Serialize, Deserialize)]
@ -214,11 +195,6 @@ impl SettingsState {
}
}
wit_bindgen::generate!({
path: "target/wit",
world: "process-v1",
});
call_init!(initialize);
fn initialize(our: Address) {
// Grab our state, then enter the main event loop.
@ -387,11 +363,11 @@ fn handle_settings_request(
request: SettingsRequest,
) -> SettingsResponse {
match request {
SettingsRequest::Hi {
SettingsRequest::Hi(HiRequest {
node,
content,
timeout,
} => {
}) => {
if let Err(SendError { kind, .. }) = Request::to((&node, "net", "distro", "sys"))
.body(content.into_bytes())
.send_and_await_response(timeout)
@ -421,7 +397,23 @@ fn handle_settings_request(
Ok(msg) => match rmp_serde::from_slice::<net::NetResponse>(msg.body()) {
Ok(net::NetResponse::Peer(Some(peer))) => {
println!("got peer info: {peer:?}");
return Ok(Some(SettingsData::PeerId(peer)));
// convert Identity to SettingsIdentity
let settings_identity = SettingsIdentity {
name: peer.name,
networking_key: peer.networking_key,
routing: match peer.routing {
net::NodeRouting::Direct { ip, ports } => {
SettingsNodeRouting::Direct(Direct {
ip,
ports: ports.into_iter().map(|(p, q)| (p, q)).collect(),
})
}
net::NodeRouting::Routers(routers) => {
SettingsNodeRouting::Routers(routers)
}
},
};
return Ok(Some(SettingsData::PeerId(settings_identity)));
}
Ok(net::NetResponse::Peer(None)) => {
println!("peer not found");
@ -436,7 +428,9 @@ fn handle_settings_request(
}
}
}
SettingsRequest::EthConfig(action) => {
SettingsRequest::EthConfig(settings_eth_config_request) => {
// convert SettingsEthConfigRequest to EthConfigRequest
let action = eth_config_convert(settings_eth_config_request)?;
match Request::to(("our", "eth", "distro", "sys"))
.body(serde_json::to_vec(&action).unwrap())
.send_and_await_response(30)
@ -478,8 +472,11 @@ fn handle_settings_request(
.unwrap();
}
}
SettingsRequest::KillProcess(pid) => {
SettingsRequest::KillProcess(pid_str) => {
// kill a process
let Ok(pid) = pid_str.parse::<ProcessId>() else {
return SettingsResponse::Err(SettingsError::MalformedRequest);
};
if let Err(_) = Request::to(("our", "kernel", "distro", "sys"))
.body(serde_json::to_vec(&kernel_types::KernelCommand::KillProcess(pid)).unwrap())
.send_and_await_response(30)
@ -517,6 +514,45 @@ fn handle_settings_request(
SettingsResponse::Ok(None)
}
fn eth_config_convert(
settings_eth_config_request: SettingsEthConfigAction,
) -> Result<eth::EthConfigAction, SettingsError> {
match settings_eth_config_request {
SettingsEthConfigAction::AddProvider(settings_provider_config) => {
Ok(eth::EthConfigAction::AddProvider(eth::ProviderConfig {
chain_id: settings_provider_config.chain_id,
provider: match settings_provider_config.node_or_rpc_url {
SettingsNodeOrRpcUrl::Node(node_str) => {
// the eth module does not actually need the full routing info
// so we can just use the name as the kns update
eth::NodeOrRpcUrl::Node {
kns_update: net::KnsUpdate {
name: node_str,
public_key: "".to_string(),
ips: vec![],
ports: std::collections::BTreeMap::new(),
routers: vec![],
},
use_as_provider: true,
}
}
SettingsNodeOrRpcUrl::RpcUrl(url) => eth::NodeOrRpcUrl::RpcUrl(url),
},
trusted: true,
}))
}
SettingsEthConfigAction::RemoveProvider((chain_id, provider_str)) => Ok(
eth::EthConfigAction::RemoveProvider((chain_id, provider_str)),
),
SettingsEthConfigAction::SetPublic => Ok(eth::EthConfigAction::SetPublic),
SettingsEthConfigAction::SetPrivate => Ok(eth::EthConfigAction::SetPrivate),
SettingsEthConfigAction::AllowNode(node) => Ok(eth::EthConfigAction::AllowNode(node)),
SettingsEthConfigAction::UnallowNode(node) => Ok(eth::EthConfigAction::UnallowNode(node)),
SettingsEthConfigAction::DenyNode(node) => Ok(eth::EthConfigAction::DenyNode(node)),
SettingsEthConfigAction::UndenyNode(node) => Ok(eth::EthConfigAction::UndenyNode(node)),
}
}
fn make_widget(state: &SettingsState) -> String {
let owner_string = state.our_owner.to_string();
let tba_string = state.our_tba.to_string();

View File

@ -68,7 +68,7 @@ function App() {
}, []);
const apiCall = async (body: any) => {
await fetch(APP_PATH, {
return await fetch(APP_PATH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
@ -106,6 +106,7 @@ function App() {
const handlePeerPing = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const form = e.currentTarget;
const response = await fetch(APP_PATH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@ -117,14 +118,58 @@ function App() {
}
}),
});
const data = await response.json();
if (data === null) {
e.currentTarget.reset();
form.reset();
try {
const data = await response.json();
if (data === null) {
setPeerPingResponse("ping successful!");
} else if (data === "HiTimeout") {
setPeerPingResponse("node timed out");
} else if (data === "HiOffline") {
setPeerPingResponse("node is offline");
}
} catch (err) {
setPeerPingResponse("ping successful!");
} else if (data === "HiTimeout") {
setPeerPingResponse("node timed out");
} else if (data === "HiOffline") {
setPeerPingResponse("node is offline");
}
};
const handleAddEthProvider = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const form = e.currentTarget;
const response = await apiCall({
"EthConfig": {
"AddProvider": {
chain_id: Number(formData.get('chain-id')),
node_or_rpc_url: { "RpcUrl": formData.get('rpc-url') as string }
}
}
});
try {
const data = await response.json();
console.log(data);
} catch (err) {
form.reset();
// this is actually a success
}
};
const handleRemoveEthProvider = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const form = e.currentTarget;
const response = await apiCall({
"EthConfig": {
"RemoveProvider": [Number(formData.get('chain-id')), formData.get('rpc-url') as string]
}
});
try {
const data = await response.json();
console.log(data);
} catch (err) {
form.reset();
// this is actually a success
}
};
@ -149,13 +194,14 @@ function App() {
<div className="mt-16 flex flex-col justify-start">
<button
onClick={handleShutdown}
className="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded w-full mb-8"
id="shutdown"
>
Shutdown Node
</button>
<br />
<br />
<button
onClick={handleReset}
className="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded w-full"
>
Reset KNS State
</button>
@ -182,12 +228,12 @@ function App() {
<article id="eth-rpc-providers">
<h2>ETH RPC providers</h2>
<article id="provider-edits">
<form id="add-eth-provider">
<form id="add-eth-provider" onSubmit={handleAddEthProvider}>
<input type="number" name="chain-id" placeholder="1" />
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com" />
<button type="submit">add provider</button>
</form>
<form id="remove-eth-provider">
<form id="remove-eth-provider" onSubmit={handleRemoveEthProvider}>
<input type="number" name="chain-id" placeholder="1" />
<input type="text" name="rpc-url" placeholder="wss://rpc-url.com" />
<button type="submit">remove provider</button>

View File

@ -1919,7 +1919,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -1751,7 +1751,7 @@ dependencies = [
[[package]]
name = "kinode_process_lib"
version = "0.10.0"
source = "git+https://github.com/kinode-dao/process_lib?rev=ea8490a#ea8490aba4837d04243d84b4f5b76fbefb498007"
source = "git+https://github.com/kinode-dao/process_lib?rev=ef78f0e#ef78f0eb18d00826874258131a1bf471e55796f0"
dependencies = [
"alloy",
"alloy-primitives",

View File

@ -2,9 +2,9 @@ use crate::vfs::UniqueQueue;
use dashmap::DashMap;
use lib::types::core::{
Address, CapMessage, CapMessageSender, Capability, FdManagerRequest, KernelMessage, KvAction,
KvError, KvRequest, KvResponse, LazyLoadBlob, Message, MessageReceiver, MessageSender,
PackageId, PrintSender, Printout, ProcessId, Request, Response, FD_MANAGER_PROCESS_ID,
KV_PROCESS_ID,
KvCapabilityKind, KvCapabilityParams, KvError, KvRequest, KvResponse, LazyLoadBlob, Message,
MessageReceiver, MessageSender, PackageId, PrintSender, Printout, ProcessId, Request, Response,
FD_MANAGER_PROCESS_ID, KV_PROCESS_ID,
};
use rocksdb::OptimisticTransactionDB;
use std::{
@ -46,48 +46,43 @@ impl KvState {
}
}
pub async fn open_db(&mut self, package_id: PackageId, db: String) -> Result<(), KvError> {
let key = (package_id.clone(), db.clone());
if self.open_kvs.contains_key(&key) {
pub async fn open_db(&mut self, key: &(PackageId, String)) -> Result<(), KvError> {
if self.open_kvs.contains_key(key) {
let mut access_order = self.access_order.lock().await;
access_order.remove(&key);
access_order.push_back(key);
access_order.remove(key);
access_order.push_back(key.clone());
return Ok(());
}
if self.open_kvs.len() as u64 >= self.fds_limit {
// close least recently used db
let key = self.access_order.lock().await.pop_front().unwrap();
self.remove_db(key.0, key.1).await;
let to_close = self.access_order.lock().await.pop_front().unwrap();
self.remove_db(&to_close).await;
}
#[cfg(unix)]
let db_path = self.kv_path.join(format!("{package_id}")).join(&db);
let db_path = self.kv_path.join(format!("{}", key.0)).join(&key.1);
#[cfg(target_os = "windows")]
let db_path = self
.kv_path
.join(format!(
"{}_{}",
package_id._package(),
package_id._publisher()
))
.join(&db);
.join(format!("{}_{}", key.0._package(), key.0._publisher()))
.join(&key.1);
fs::create_dir_all(&db_path).await?;
self.open_kvs.insert(
key,
key.clone(),
OptimisticTransactionDB::open_default(&db_path).map_err(rocks_to_kv_err)?,
);
let mut access_order = self.access_order.lock().await;
access_order.push_back((package_id, db));
access_order.push_back(key.clone());
Ok(())
}
pub async fn remove_db(&mut self, package_id: PackageId, db: String) {
self.open_kvs.remove(&(package_id.clone(), db.to_string()));
pub async fn remove_db(&mut self, key: &(PackageId, String)) {
self.open_kvs.remove(key);
let mut access_order = self.access_order.lock().await;
access_order.remove(&(package_id, db));
access_order.remove(key);
}
pub async fn remove_least_recently_used_dbs(&mut self, n: u64) {
@ -95,7 +90,7 @@ impl KvState {
let mut lock = self.access_order.lock().await;
let key = lock.pop_front().unwrap();
drop(lock);
self.remove_db(key.0, key.1).await;
self.remove_db(&key).await;
}
}
}
@ -215,29 +210,33 @@ async fn handle_request(
..
}) = message
else {
return Err(KvError::InputError {
error: "not a request".into(),
});
// we got a response -- safe to ignore
return Ok(());
};
let request: KvRequest = match serde_json::from_slice(&body) {
Ok(r) => r,
Err(e) => {
println!("kv: got invalid Request: {}", e);
return Err(KvError::InputError {
error: "didn't serialize to KvAction.".into(),
});
println!("kv: got invalid request: {e}");
return Err(KvError::MalformedRequest);
}
};
check_caps(&source, state, send_to_caps_oracle, &request).await?;
let db_key = (request.package_id, request.db);
check_caps(
&source,
state,
send_to_caps_oracle,
&request.action,
&db_key,
)
.await?;
// always open to ensure db exists
state
.open_db(request.package_id.clone(), request.db.clone())
.await?;
state.open_db(&db_key).await?;
let (body, bytes) = match &request.action {
let (body, bytes) = match request.action {
KvAction::Open => {
// handled in check_caps.
(serde_json::to_vec(&KvResponse::Ok).unwrap(), None)
@ -246,27 +245,24 @@ async fn handle_request(
// handled in check_caps.
(serde_json::to_vec(&KvResponse::Ok).unwrap(), None)
}
KvAction::Get { key } => {
let db = match state.open_kvs.get(&(request.package_id, request.db)) {
KvAction::Get(key) => {
let db = match state.open_kvs.get(&db_key) {
None => {
return Err(KvError::NoDb);
return Err(KvError::NoDb(db_key.0, db_key.1));
}
Some(db) => db,
};
match db.get(key) {
match db.get(&key) {
Ok(Some(value)) => (
serde_json::to_vec(&KvResponse::Get { key: key.to_vec() }).unwrap(),
serde_json::to_vec(&KvResponse::Get(key)).unwrap(),
Some(value),
),
Ok(None) => {
return Err(KvError::KeyNotFound);
}
Err(e) => {
return Err(KvError::RocksDBError {
action: request.action.to_string(),
error: e.to_string(),
})
return Err(rocks_to_kv_err(e));
}
}
}
@ -278,17 +274,15 @@ async fn handle_request(
None,
)
}
KvAction::Set { key, tx_id } => {
let db = match state.open_kvs.get(&(request.package_id, request.db)) {
KvAction::Set { ref key, tx_id } => {
let db = match state.open_kvs.get(&db_key) {
None => {
return Err(KvError::NoDb);
return Err(KvError::NoDb(db_key.0, db_key.1));
}
Some(db) => db,
};
let Some(blob) = blob else {
return Err(KvError::InputError {
error: "no blob".into(),
});
return Err(KvError::MalformedRequest);
};
match tx_id {
@ -296,22 +290,22 @@ async fn handle_request(
db.put(key, blob.bytes).map_err(rocks_to_kv_err)?;
}
Some(tx_id) => {
let mut tx = match state.txs.get_mut(tx_id) {
let mut tx = match state.txs.get_mut(&tx_id) {
None => {
return Err(KvError::NoTx);
return Err(KvError::NoTx(tx_id));
}
Some(tx) => tx,
};
tx.push((request.action.clone(), Some(blob.bytes)));
tx.push((request.action, Some(blob.bytes)));
}
}
(serde_json::to_vec(&KvResponse::Ok).unwrap(), None)
}
KvAction::Delete { key, tx_id } => {
let db = match state.open_kvs.get(&(request.package_id, request.db)) {
KvAction::Delete { ref key, tx_id } => {
let db = match state.open_kvs.get(&db_key) {
None => {
return Err(KvError::NoDb);
return Err(KvError::NoDb(db_key.0, db_key.1));
}
Some(db) => db,
};
@ -320,28 +314,28 @@ async fn handle_request(
db.delete(key).map_err(rocks_to_kv_err)?;
}
Some(tx_id) => {
let mut tx = match state.txs.get_mut(tx_id) {
let mut tx = match state.txs.get_mut(&tx_id) {
None => {
return Err(KvError::NoTx);
return Err(KvError::NoTx(tx_id));
}
Some(tx) => tx,
};
tx.push((request.action.clone(), None));
tx.push((request.action, None));
}
}
(serde_json::to_vec(&KvResponse::Ok).unwrap(), None)
}
KvAction::Commit { tx_id } => {
let db = match state.open_kvs.get(&(request.package_id, request.db)) {
let db = match state.open_kvs.get(&db_key) {
None => {
return Err(KvError::NoDb);
return Err(KvError::NoDb(db_key.0, db_key.1));
}
Some(db) => db,
};
let txs = match state.txs.remove(tx_id).map(|(_, tx)| tx) {
let txs = match state.txs.remove(&tx_id).map(|(_, tx)| tx) {
None => {
return Err(KvError::NoTx);
return Err(KvError::NoTx(tx_id));
}
Some(tx) => tx,
};
@ -364,21 +358,10 @@ async fn handle_request(
match tx.commit() {
Ok(_) => (serde_json::to_vec(&KvResponse::Ok).unwrap(), None),
Err(e) => {
return Err(KvError::RocksDBError {
action: request.action.to_string(),
error: e.to_string(),
})
return Err(rocks_to_kv_err(e));
}
}
}
KvAction::Backup => {
// looping through open dbs and flushing their memtables
for db_ref in state.open_kvs.iter() {
let db = db_ref.value();
db.flush().map_err(rocks_to_kv_err)?;
}
(serde_json::to_vec(&KvResponse::Ok).unwrap(), None)
}
};
if let Some(target) = km.rsvp.or_else(|| expects_response.map(|_| source)) {
@ -412,128 +395,110 @@ async fn check_caps(
source: &Address,
state: &mut KvState,
send_to_caps_oracle: &CapMessageSender,
request: &KvRequest,
action: &KvAction,
db_key: &(PackageId, String),
) -> Result<(), KvError> {
let (send_cap_bool, recv_cap_bool) = tokio::sync::oneshot::channel();
let src_package_id = PackageId::new(source.process.package(), source.process.publisher());
match &request.action {
match &action {
KvAction::Delete { .. }
| KvAction::Set { .. }
| KvAction::BeginTx
| KvAction::Commit { .. } => {
send_to_caps_oracle
let Ok(()) = send_to_caps_oracle
.send(CapMessage::Has {
on: source.process.clone(),
cap: Capability::new(
state.our.as_ref().clone(),
serde_json::json!({
"kind": "write",
"db": request.db.to_string(),
serde_json::to_string(&KvCapabilityParams {
kind: KvCapabilityKind::Write,
db_key: db_key.clone(),
})
.to_string(),
.unwrap(),
),
responder: send_cap_bool,
})
.await?;
let has_cap = recv_cap_bool.await?;
if !has_cap {
return Err(KvError::NoCap {
error: request.action.to_string(),
});
}
.await
else {
return Err(KvError::NoWriteCap);
};
let Ok(true) = recv_cap_bool.await else {
return Err(KvError::NoWriteCap);
};
Ok(())
}
KvAction::Get { .. } => {
send_to_caps_oracle
let Ok(()) = send_to_caps_oracle
.send(CapMessage::Has {
on: source.process.clone(),
cap: Capability::new(
state.our.as_ref().clone(),
serde_json::json!({
"kind": "read",
"db": request.db.to_string(),
serde_json::to_string(&KvCapabilityParams {
kind: KvCapabilityKind::Read,
db_key: db_key.clone(),
})
.to_string(),
.unwrap(),
),
responder: send_cap_bool,
})
.await?;
let has_cap = recv_cap_bool.await?;
if !has_cap {
return Err(KvError::NoCap {
error: request.action.to_string(),
});
}
.await
else {
return Err(KvError::NoReadCap);
};
let Ok(true) = recv_cap_bool.await else {
return Err(KvError::NoReadCap);
};
Ok(())
}
KvAction::Open { .. } => {
if src_package_id != request.package_id {
return Err(KvError::NoCap {
error: request.action.to_string(),
});
if src_package_id != db_key.0 {
return Err(KvError::MismatchingPackageId);
}
add_capability(
"read",
&request.db.to_string(),
KvCapabilityKind::Read,
&db_key,
&state.our,
&source,
send_to_caps_oracle,
)
.await?;
add_capability(
"write",
&request.db.to_string(),
KvCapabilityKind::Write,
&db_key,
&state.our,
&source,
send_to_caps_oracle,
)
.await?;
if state
.open_kvs
.contains_key(&(request.package_id.clone(), request.db.clone()))
{
if state.open_kvs.contains_key(&db_key) {
return Ok(());
}
state
.open_db(request.package_id.clone(), request.db.clone())
.await?;
state.open_db(&db_key).await?;
Ok(())
}
KvAction::RemoveDb { .. } => {
if src_package_id != request.package_id {
return Err(KvError::NoCap {
error: request.action.to_string(),
});
if src_package_id != db_key.0 {
return Err(KvError::MismatchingPackageId);
}
state
.remove_db(request.package_id.clone(), request.db.clone())
.await;
state.remove_db(&db_key).await;
#[cfg(unix)]
let db_path = state
.kv_path
.join(format!("{}", request.package_id))
.join(&request.db);
let db_path = state.kv_path.join(format!("{}", db_key.0)).join(&db_key.1);
#[cfg(target_os = "windows")]
let db_path = state
.kv_path
.join(format!(
"{}_{}",
request.package_id._package(),
request.package_id._publisher()
))
.join(&request.db);
.join(format!("{}_{}", db_key.0._package(), db_key.0._publisher()))
.join(&db_key.1);
fs::remove_dir_all(&db_path).await?;
Ok(())
}
KvAction::Backup { .. } => Ok(()),
}
}
@ -564,31 +529,37 @@ async fn handle_fd_request(km: KernelMessage, state: &mut KvState) -> anyhow::Re
}
async fn add_capability(
kind: &str,
db: &str,
kind: KvCapabilityKind,
db_key: &(PackageId, String),
our: &Address,
source: &Address,
send_to_caps_oracle: &CapMessageSender,
) -> Result<(), KvError> {
let cap = Capability {
issuer: our.clone(),
params: serde_json::json!({ "kind": kind, "db": db }).to_string(),
params: serde_json::to_string(&KvCapabilityParams {
kind,
db_key: db_key.clone(),
})
.unwrap(),
};
let (send_cap_bool, recv_cap_bool) = tokio::sync::oneshot::channel();
send_to_caps_oracle
let Ok(()) = send_to_caps_oracle
.send(CapMessage::Add {
on: source.process.clone(),
caps: vec![cap],
responder: Some(send_cap_bool),
})
.await?;
let _ = recv_cap_bool.await?;
.await
else {
return Err(KvError::AddCapFailed);
};
let Ok(_) = recv_cap_bool.await else {
return Err(KvError::AddCapFailed);
};
Ok(())
}
fn rocks_to_kv_err(error: rocksdb::Error) -> KvError {
KvError::RocksDBError {
action: "".into(),
error: error.to_string(),
}
KvError::RocksDBError(error.to_string())
}

View File

@ -412,7 +412,7 @@ async fn handle_remote_request(
}
_ => {
// if we can't parse this to a NetAction, treat it as a hello and print it,
// and respond with a simple "delivered" response
// and respond with a simple "ack" response
utils::parse_hello_message(
&ext.our,
&km,

View File

@ -415,7 +415,7 @@ pub async fn parse_hello_message(
.message(Message::Response((
Response {
inherit: false,
body: "delivered".as_bytes().to_vec(),
body: b"ack".to_vec(),
metadata: None,
capabilities: vec![],
},

View File

@ -11,6 +11,7 @@ import KinodeHome from "./pages/KinodeHome"
import ImportKeyfile from "./pages/ImportKeyfile";
import { UnencryptedIdentity } from "./lib/types";
import Header from "./components/Header";
import ProgressBar from "./components/ProgressBar";
function App() {
const params = useParams()
@ -104,13 +105,33 @@ function App() {
? <Navigate to="/login" replace />
: <KinodeHome {...props} />
} />
<Route path="/commit-os-name" element={<CommitDotOsName {...props} />} />
<Route path="/mint-os-name" element={<MintDotOsName {...props} />} />
<Route path="/set-password" element={<SetPassword {...props} />} />
<Route path="/commit-os-name" element={
<>
<ProgressBar knsName={knsName} />
<CommitDotOsName {...props} />
</>
} />
<Route path="/mint-os-name" element={
<>
<ProgressBar knsName={knsName} />
<MintDotOsName {...props} />
</>
} />
<Route path="/set-password" element={
<>
<ProgressBar knsName={knsName} />
<SetPassword {...props} />
</>
} />
<Route path="/reset" element={<ResetName {...props} />} />
<Route path="/import-keyfile" element={<ImportKeyfile {...props} />} />
<Route path="/login" element={<Login {...props} />} />
<Route path="/custom-register" element={<MintCustom {...props} />} />
<Route path="/custom-register" element={
<>
<ProgressBar knsName={knsName} />
<MintCustom {...props} />
</>
} />
</Routes>
</main>
</Router>

View File

@ -0,0 +1,68 @@
import { useNavigate, useLocation } from 'react-router-dom';
const steps = [
{ path: '/', label: 'Home' },
{ path: '/commit-os-name', label: 'Choose Name' },
{ path: '/mint-os-name', label: 'Mint Name' },
{ path: '/set-password', label: 'Set Password' },
];
interface ProgressBarProps {
knsName: string;
}
const ProgressBar = ({ knsName }: ProgressBarProps) => {
const navigate = useNavigate();
const location = useLocation();
const currentStepIndex = steps.findIndex(step => step.path === location.pathname);
const isStepAccessible = (index: number) => {
// Home is always accessible
if (index === 0) return true;
if (knsName && index <= 2) return true;
// Otherwise only allow going back
return index <= currentStepIndex;
};
const handleStepClick = (path: string, index: number) => {
if (isStepAccessible(index)) {
navigate(path);
}
};
return (
<div className="progress-container">
<div className="progress-bar">
{steps.map((step, index) => {
const accessible = isStepAccessible(index);
return (
<div key={step.path} className="step-wrapper">
<div
className={`step ${index <= currentStepIndex ? 'active' : ''} ${
index < currentStepIndex ? 'completed' : ''
} ${accessible ? 'clickable' : 'disabled'}`}
onClick={() => handleStepClick(step.path, index)}
>
<div className="step-number">{index}</div>
<div className="step-label">{step.label}</div>
</div>
{index < steps.length - 1 && (
<div className={`connector ${index < currentStepIndex ? 'active' : ''}`} />
)}
</div>
);
})}
</div>
{knsName && (
<div className="selected-name">
Selected name: <span>{knsName}</span>
</div>
)}
</div>
);
};
export default ProgressBar;

View File

@ -1,253 +1,437 @@
/* forms */
.form-group {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
}
.form-label {
margin-bottom: 0.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
font-weight: bold;
}
.form-input {
padding: 0.75rem;
border: 2px solid var(--orange);
border-radius: 4px;
transition: all 0.3s ease;
padding: 0.75rem;
border: 2px solid var(--orange);
border-radius: 4px;
transition: all 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--dark-orange);
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
outline: none;
border-color: var(--dark-orange);
box-shadow: 0 0 0 3px rgba(255, 79, 0, 0.2);
}
/* tooltips */
.tooltip {
position: relative;
display: inline-block;
position: relative;
display: inline-block;
}
.tooltip-text {
font-size: 0.8em;
visibility: hidden;
width: 200px;
background-color: #555;
color: var(--off-white);
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
font-size: 0.8em;
visibility: hidden;
width: 200px;
background-color: #555;
color: var(--off-white);
text-align: center;
border-radius: 6px;
padding: 5px;
position: absolute;
z-index: 1;
opacity: 0;
transition: opacity 0.3s;
}
.tooltip-top .tooltip-text {
bottom: 125%;
left: 50%;
margin-left: -100px;
bottom: 125%;
left: 50%;
margin-left: -100px;
}
.tooltip-bottom .tooltip-text {
top: 125%;
left: 50%;
margin-left: -100px;
top: 125%;
left: 50%;
margin-left: -100px;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
visibility: visible;
opacity: 1;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.section {
background-color: light-dark(var(--off-white), var(--tasteful-dark));
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 2rem;
background-color: light-dark(var(--off-white), var(--tasteful-dark));
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.content {
text-align: center;
text-align: center;
}
.button-group {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 1rem;
}
.text-center {
text-align: center;
text-align: center;
}
.mb-2 {
margin-bottom: 1rem;
margin-bottom: 1rem;
}
.mt-2 {
margin-top: 1rem;
margin-top: 1rem;
}
.header {
position: fixed;
top: 0;
right: 0;
padding: 1rem;
z-index: 1000;
position: fixed;
top: 0;
right: 0;
padding: 1rem;
z-index: 1000;
}
.connect-wallet {
display: flex;
justify-content: flex-end;
display: flex;
justify-content: flex-end;
}
.container {
padding-top: 4rem;
/* Add some top padding to account for the fixed header */
padding-top: 4rem;
/* Add some top padding to account for the fixed header */
}
.enter-kns-name {
width: 100%;
margin-bottom: 1rem;
width: 100%;
margin-bottom: 1rem;
}
.input-wrapper {
display: flex;
align-items: center;
width: 100%;
display: flex;
align-items: center;
width: 100%;
}
.kns-input {
flex-grow: 1;
padding: 0.5rem;
font-size: 1.2em;
border: 1px solid var(--gray);
border-radius: 4px 0 0 4px;
flex-grow: 1;
padding: 0.5rem;
font-size: 1.2em;
border: 1px solid var(--gray);
border-radius: 4px 0 0 4px;
}
.kns-suffix {
padding: 0.5rem;
background-color: var(--blue);
border: 1px solid var(--tasteful-dark);
border-left: none;
border-radius: 0 4px 4px 0;
padding: 0.5rem;
background-color: var(--blue);
border: 1px solid var(--tasteful-dark);
border-left: none;
border-radius: 0 4px 4px 0;
}
.button-group {
display: flex;
flex-direction: column;
gap: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.error-message {
color: var(--ansi-red);
margin-top: 0.5rem;
overflow-wrap: break-word;
color: var(--ansi-red);
margin-top: 0.5rem;
overflow-wrap: break-word;
}
.direct-checkbox {
display: flex;
align-items: center;
margin-bottom: 1rem;
display: flex;
align-items: center;
margin-bottom: 1rem;
}
.checkbox-container {
display: flex;
align-items: center;
position: relative;
padding-left: 35px;
cursor: pointer;
font-size: 16px;
user-select: none;
display: flex;
align-items: center;
position: relative;
padding-left: 35px;
cursor: pointer;
font-size: 16px;
user-select: none;
}
.checkbox-container input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
background-color: #eee;
border: 2px solid var(--orange);
border-radius: 4px;
position: absolute;
top: 0;
left: 0;
height: 25px;
width: 25px;
background-color: #eee;
border: 2px solid var(--orange);
border-radius: 4px;
}
.checkbox-container:hover input~.checkmark {
background-color: var(--gray);
background-color: var(--gray);
}
.checkbox-container input:checked~.checkmark {
background-color: var(--orange);
background-color: var(--orange);
}
.checkmark:after {
content: "";
position: absolute;
display: none;
content: "";
position: absolute;
display: none;
}
.checkbox-container input:checked~.checkmark:after {
display: block;
display: block;
}
.checkbox-container .checkmark:after {
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border: solid var(--off-white);
border-width: 0 3px 3px 0;
transform: rotate(45deg);
left: 9px;
top: 5px;
width: 5px;
height: 10px;
border: solid var(--off-white);
border-width: 0 3px 3px 0;
transform: rotate(45deg);
}
.checkbox-label {
margin-left: 10px;
font-size: 0.9em;
margin-left: 10px;
font-size: 0.9em;
}
.file-input-label {
display: inline-block;
display: inline-block;
}
.file-input {
display: none;
display: none;
}
.file-input-label .button {
display: inline-block;
margin: 0;
padding: 0.3rem 0.8rem;
font-size: 0.9em;
display: inline-block;
margin: 0;
padding: 0.3rem 0.8rem;
font-size: 0.9em;
}
.file-input-label:hover .button {
background-color: var(--dark-orange);
color: var(--off-white);
background-color: var(--dark-orange);
color: var(--off-white);
}
button.secondary {
width: 100%;
width: 100%;
}
button.back {
float: left;
width: auto !important;
padding: 5px !important;
border: none !important;
float: left;
width: auto !important;
padding: 5px !important;
border: none !important;
}
/* Progress Bar Styles */
.progress-container {
font-family: var(--font-family-main);
margin-top: 1.5rem;
padding: 1.5rem;
max-width: 600px;
background: linear-gradient(145deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02));
border-radius: 16px;
position: relative;
}
.progress-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg,
transparent,
rgba(255, 79, 0, 0.2),
rgba(255, 79, 0, 0.2),
transparent);
}
.progress-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 auto;
position: relative;
padding: 0 1rem;
}
.step-wrapper {
display: flex;
align-items: center;
flex: 1;
position: relative;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
position: relative;
transition: all 0.3s ease;
z-index: 2;
padding: 0.5rem;
}
.step.clickable {
cursor: pointer;
}
.step.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.step.disabled:hover .step-number {
transform: none;
box-shadow: none;
}
.step.clickable:not(.active):hover .step-number {
background: rgba(255, 79, 0, 0.1);
}
.step.clickable.completed:hover .step-number {
background: var(--dark-orange);
opacity: 0.9;
}
.step:hover .step-number {
transform: scale(1.1);
box-shadow: 0 0 12px rgba(255, 79, 0, 0.3);
}
.step-number {
width: 28px;
height: 28px;
font-size: 0.8rem;
border-radius: 50%;
background: var(--off-white);
border: 2px solid var(--orange);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: var(--orange);
transition: all 0.3s ease;
position: relative;
}
.step-number::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
border: 2px solid transparent;
transition: all 0.3s ease;
}
.step.active .step-number {
background: var(--orange);
color: var(--off-white);
transform: scale(1.1);
}
.step.active .step-number::after {
border-color: rgba(255, 79, 0, 0.3);
inset: -4px;
}
.step.completed .step-number {
background: var(--dark-orange);
border-color: var(--dark-orange);
color: var(--off-white);
}
.step-label {
margin-top: 0.6rem;
font-size: 0.75rem;
color: var(--orange);
font-weight: 500;
transition: all 0.3s ease;
text-align: center;
white-space: nowrap;
opacity: 0.85;
letter-spacing: 0.2px;
}
.step.active .step-label {
opacity: 1;
font-weight: 600;
transform: translateY(1px);
}
.connector {
flex: 1;
height: 2px;
background: rgba(255, 79, 0, 0.15);
position: relative;
top: -8px;
transform-origin: left center;
margin: 0 0.25rem;
min-width: 20px;
}
.connector.active {
background: linear-gradient(90deg, var(--orange), var(--dark-orange));
animation: fillLine 0.4s ease-out forwards;
}
.selected-name {
text-align: center;
margin-top: 1.5rem;
font-size: 0.9rem;
color: var(--text);
opacity: 0.9;
padding: 0.75rem;
background: rgba(255, 79, 0, 0.05);
border-radius: 8px;
letter-spacing: 0.3px;
}
.selected-name span {
font-weight: 600;
color: var(--orange);
margin-left: 0.3rem;
}
@keyframes fillLine {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}

View File

@ -1,79 +1,153 @@
use crate::types::core::{CapMessage, PackageId};
use crate::types::core::PackageId;
use serde::{Deserialize, Serialize};
use thiserror::Error;
/// IPC Request format for the kv:distro:sys runtime module.
#[derive(Debug, Serialize, Deserialize)]
/// Actions are sent to a specific key value database. `db` is the name,
/// `package_id` is the [`PackageId`] that created the database. Capabilities
/// are checked: you can access another process's database if it has given
/// you the read and/or write capability to do so.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvRequest {
pub package_id: PackageId,
pub db: String,
pub action: KvAction,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
/// IPC Action format representing operations that can be performed on the
/// key-value runtime module. These actions are included in a [`KvRequest`]
/// sent to the `kv:distro:sys` runtime module.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum KvAction {
/// Opens an existing key-value database or creates a new one if it doesn't exist.
/// Requires `package_id` in [`KvRequest`] to match the package ID of the sender.
/// The sender will own the database and can remove it with [`KvAction::RemoveDb`].
///
/// A successful open will respond with [`KvResponse::Ok`]. Any error will be
/// contained in the [`KvResponse::Err`] variant.
Open,
/// Permanently deletes the entire key-value database.
/// Requires `package_id` in [`KvRequest`] to match the package ID of the sender.
/// Only the owner can remove the database.
///
/// A successful remove will respond with [`KvResponse::Ok`]. Any error will be
/// contained in the [`KvResponse::Err`] variant.
RemoveDb,
/// Sets a value for the specified key in the database.
///
/// # Parameters
/// * `key` - The key as a byte vector
/// * `tx_id` - Optional transaction ID if this operation is part of a transaction
/// * blob: [`Vec<u8>`] - Byte vector to store for the key
///
/// Using this action requires the sender to have the write capability
/// for the database.
///
/// A successful set will respond with [`KvResponse::Ok`]. Any error will be
/// contained in the [`KvResponse::Err`] variant.
Set { key: Vec<u8>, tx_id: Option<u64> },
/// Deletes a key-value pair from the database.
///
/// # Parameters
/// * `key` - The key to delete as a byte vector
/// * `tx_id` - Optional transaction ID if this operation is part of a transaction
///
/// Using this action requires the sender to have the write capability
/// for the database.
///
/// A successful delete will respond with [`KvResponse::Ok`]. Any error will be
/// contained in the [`KvResponse::Err`] variant.
Delete { key: Vec<u8>, tx_id: Option<u64> },
Get { key: Vec<u8> },
/// Retrieves the value associated with the specified key.
///
/// # Parameters
/// * The key to look up as a byte vector
///
/// Using this action requires the sender to have the read capability
/// for the database.
///
/// A successful get will respond with [`KvResponse::Get`], where the response blob
/// contains the value associated with the key if any. Any error will be
/// contained in the [`KvResponse::Err`] variant.
Get(Vec<u8>),
/// Begins a new transaction for atomic operations.
///
/// Sending this will prompt a [`KvResponse::BeginTx`] response with the
/// transaction ID. Any error will be contained in the [`KvResponse::Err`] variant.
BeginTx,
/// Commits all operations in the specified transaction.
///
/// # Parameters
/// * `tx_id` - The ID of the transaction to commit
///
/// A successful commit will respond with [`KvResponse::Ok`]. Any error will be
/// contained in the [`KvResponse::Err`] variant.
Commit { tx_id: u64 },
Backup,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum KvResponse {
/// Indicates successful completion of an operation.
/// Sent in response to actions Open, RemoveDb, Set, Delete, and Commit.
Ok,
/// Returns the transaction ID for a newly created transaction.
///
/// # Fields
/// * `tx_id` - The ID of the newly created transaction
BeginTx { tx_id: u64 },
Get { key: Vec<u8> },
/// Returns the value for the key that was retrieved from the database.
///
/// # Parameters
/// * The retrieved key as a byte vector
/// * blob: [`Vec<u8>`] - Byte vector associated with the key
Get(Vec<u8>),
/// Indicates an error occurred during the operation.
Err(KvError),
}
#[derive(Debug, Serialize, Deserialize, Error)]
#[derive(Clone, Debug, Serialize, Deserialize, Error)]
pub enum KvError {
#[error("DbDoesNotExist")]
NoDb,
#[error("KeyNotFound")]
#[error("db [{0}, {1}] does not exist")]
NoDb(PackageId, String),
#[error("key not found")]
KeyNotFound,
#[error("no Tx found")]
NoTx,
#[error("No capability: {error}")]
NoCap { error: String },
#[error("rocksdb internal error: {error}")]
RocksDBError { action: String, error: String },
#[error("input bytes/json/key error: {error}")]
InputError { error: String },
#[error("IO error: {error}")]
IOError { error: String },
#[error("no transaction {0} found")]
NoTx(u64),
#[error("no write capability for requested DB")]
NoWriteCap,
#[error("no read capability for requested DB")]
NoReadCap,
#[error("request to open or remove DB with mismatching package ID")]
MismatchingPackageId,
#[error("failed to generate capability for new DB")]
AddCapFailed,
#[error("kv got a malformed request that either failed to deserialize or was missing a required blob")]
MalformedRequest,
#[error("RocksDB internal error: {0}")]
RocksDBError(String),
#[error("IO error: {0}")]
IOError(String),
}
impl std::fmt::Display for KvAction {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{:?}", self)
}
/// The JSON parameters contained in all capabilities issued by `kv:distro:sys`.
///
/// # Fields
/// * `kind` - The kind of capability, either [`KvCapabilityKind::Read`] or [`KvCapabilityKind::Write`]
/// * `db_key` - The database key, a tuple of the [`PackageId`] that created the database and the database name
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct KvCapabilityParams {
pub kind: KvCapabilityKind,
pub db_key: (PackageId, String),
}
impl From<tokio::sync::oneshot::error::RecvError> for KvError {
fn from(err: tokio::sync::oneshot::error::RecvError) -> Self {
KvError::NoCap {
error: err.to_string(),
}
}
}
impl From<tokio::sync::mpsc::error::SendError<CapMessage>> for KvError {
fn from(err: tokio::sync::mpsc::error::SendError<CapMessage>) -> Self {
KvError::NoCap {
error: err.to_string(),
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KvCapabilityKind {
Read,
Write,
}
impl From<std::io::Error> for KvError {
fn from(err: std::io::Error) -> Self {
KvError::IOError {
error: err.to_string(),
}
KvError::IOError(err.to_string())
}
}