Production-grade Holochain AppWebsocket client wrapper used by the unyt
server-side services (bridge orchestrator, unyt_cli daemon, pricing oracle,
watchtower).
Ham— a connect-once wrapper aroundholochain_client::AppWebsocketthat handles admin-interface discovery, app-interface attach, zome-call signing, and typed msgpack zome calls with an explicit per-request timeout. Signs either via lair as the cell's own agent key — no capability grant committed to the chain (HamConfig::try_lair_signing_from_node/with_lair_signing) — or, by default, by authorizing a throwaway signing key on chain (one cap grant per connect).errors::is_connection_error(&anyhow::Error) -> bool— string-based classifier that decides whether an error warrants rebuilding the socket (covered by unit tests).reconnect::connect_with_backoff— shutdown-aware exponential-backoff reconnect loop with jitter and log-level escalation.compute_delay_msis exposed as a pure function for testing.shutdown::install_shutdown_handler()— returns aShutdownRx(tokio::sync::watch::Receiver<bool>) that flips totrueon SIGINT or SIGTERM.
use ham::{Ham, HamConfig, BackoffConfig, install_shutdown_handler, connect_with_backoff};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let mut shutdown = install_shutdown_handler();
let backoff = BackoffConfig::default();
let cfg = HamConfig::new(30000, 30001, "bridging-app")
.with_request_timeout_secs(120);
let mut ham = match connect_with_backoff(
|| Ham::connect(cfg.clone()),
&backoff,
&mut shutdown,
).await {
Some(h) => h,
None => return Ok(()),
};
loop {
if *shutdown.borrow() { break }
if let Err(e) = ham.ping().await {
if ham::is_connection_error(&e) {
if let Some(h) = connect_with_backoff(
|| Ham::connect(cfg.clone()),
&backoff,
&mut shutdown,
).await {
ham = h;
} else {
break;
}
}
}
tokio::select! {
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {}
_ = shutdown.changed() => break,
}
}
Ok(())
}This crate pins holochain_client = "=0.9.0" exactly (the Holochain 0.7 line). All consumers must align to the same holochain_client version because its types flow across the ham crate boundary. Lair signing additionally uses lair_keystore_api = "0.7.1" (the version holochain_client 0.9.0 resolves) to open the keystore connection for the built-in holochain_client::LairAgentSigner.
The crate emits structured events with stable event field names that
deployment dashboards can alert on:
| Event | Level | When |
|---|---|---|
ham.connecting |
info |
Ham::connect is invoked. |
ham.connected |
info |
App websocket connected and signing set up; the signing field is lair (no cap grant) or client (cap grant committed). |
ham.lair_discovery_failed |
warn |
Lair signing requested but the URL/passphrase couldn't be resolved; fell back to client signing. |
ham.call_zome |
debug |
Per zome call. |
ham.reconnect.attempt |
warn / error |
Each failed reconnect attempt (error after escalate_after). |
ham.reconnected |
info |
Reconnect succeeded after one or more failed attempts. |
Daemons using connect_with_backoff typically also emit their own
ham.disconnected / ham.probe.failed events at the call sites.
Semver from 0.1.0. Consumers pin rev = "<sha>" (not a tag) so rollouts are
reproducible; tags are cut once a compatible set of consumer updates has
landed.