use did_hosting_common::did_ops::{self, DidRecord};
use did_hosting_common::didcomm_types::*;
use serde_json::json;
use tracing::{info, warn};
use crate::registry::{self, ServiceType};
use crate::server::AppState;
pub fn sync_all_dids_to_server(
state: &AppState,
server_did: String,
reported: std::collections::HashMap<String, u64>,
) {
let dids_ks = state.dids_ks.clone();
let registry_ks = state.registry_ks.clone();
let store = state.store.clone();
let notify = state.outbox_notify.clone();
tokio::spawn(async move {
let instance_id = server_did.replace(':', "_");
let batch = crate::registry::get_instance(®istry_ks, &instance_id)
.await
.ok()
.flatten()
.is_some_and(|inst| inst.sync_batch_capable);
let raw = match dids_ks.prefix_iter_raw("did:").await {
Ok(raw) => raw,
Err(e) => {
warn!(error = %e, "sync_all_dids: failed to iterate DIDs");
return;
}
};
let mut count = 0u64; let mut frames = 0u64; let mut pending: Vec<serde_json::Value> = Vec::new();
let mut pending_bytes = 0usize;
for (_key, value) in raw {
let record: DidRecord = match serde_json::from_slice(&value) {
Ok(r) => r,
Err(_) => continue,
};
if record.version_count == 0 {
continue;
}
if reported
.get(&record.mnemonic)
.is_some_and(|&have| have >= record.version_count)
{
continue;
}
let log_content = match dids_ks
.get_raw(did_ops::content_log_key(&record.mnemonic))
.await
{
Ok(Some(bytes)) => match String::from_utf8(bytes) {
Ok(s) => s,
Err(_) => continue,
},
_ => continue,
};
let witness_content = match dids_ks
.get_raw(did_ops::content_witness_key(&record.mnemonic))
.await
{
Ok(Some(bytes)) => String::from_utf8(bytes).ok(),
_ => None,
};
let body = json!({
"mnemonic": record.mnemonic,
"did_id": record.did_id.unwrap_or_default(),
"log_content": log_content,
"witness_content": witness_content,
"version_count": record.version_count,
});
if !batch {
if let Err(e) =
crate::outbox::enqueue(&store, &server_did, MSG_SYNC_UPDATE, body).await
{
warn!(server_did = %server_did, mnemonic = %record.mnemonic, error = %e, "sync_all_dids: outbox enqueue failed");
} else {
count += 1;
frames += 1;
}
continue;
}
let sz = body.to_string().len();
if !pending.is_empty()
&& (pending.len() >= SYNC_BATCH_MAX_COUNT
|| pending_bytes + sz > SYNC_BATCH_MAX_BYTES)
{
let payload = json!({ "updates": std::mem::take(&mut pending) });
pending_bytes = 0;
if let Err(e) =
crate::outbox::enqueue(&store, &server_did, MSG_SYNC_BATCH, payload).await
{
warn!(server_did = %server_did, error = %e, "sync_all_dids: outbox batch enqueue failed");
} else {
frames += 1;
}
}
pending_bytes += sz;
pending.push(body);
count += 1;
}
if !pending.is_empty() {
let payload = json!({ "updates": pending });
if let Err(e) =
crate::outbox::enqueue(&store, &server_did, MSG_SYNC_BATCH, payload).await
{
warn!(server_did = %server_did, error = %e, "sync_all_dids: outbox batch enqueue failed");
} else {
frames += 1;
}
}
if count > 0 {
notify.notify_one();
info!(
server_did = %server_did,
count,
frames,
batched = batch,
"initial DID sync queued for newly registered server"
);
}
});
}
const SYNC_BATCH_MAX_COUNT: usize = 50;
const SYNC_BATCH_MAX_BYTES: usize = 512 * 1024;
pub fn notify_servers_did(state: &AppState, mnemonic: String) {
let registry_ks = state.registry_ks.clone();
let dids_ks = state.dids_ks.clone();
let store = state.store.clone();
let notify = state.outbox_notify.clone();
tokio::spawn(async move {
info!(mnemonic = %mnemonic, "DID changed — queueing sync to servers");
let record = match dids_ks.get::<DidRecord>(did_ops::did_key(&mnemonic)).await {
Ok(Some(r)) => r,
Ok(None) => {
warn!(mnemonic = %mnemonic, "DID sync: record not found in store");
return;
}
Err(e) => {
warn!(mnemonic = %mnemonic, error = %e, "DID sync: failed to read record");
return;
}
};
let log_content = match dids_ks.get_raw(did_ops::content_log_key(&mnemonic)).await {
Ok(Some(bytes)) => match String::from_utf8(bytes) {
Ok(s) => s,
Err(_) => {
warn!(mnemonic = %mnemonic, "DID sync: invalid UTF-8 in log content");
return;
}
},
Ok(None) => {
warn!(mnemonic = %mnemonic, "DID sync: no log content found");
return;
}
Err(e) => {
warn!(mnemonic = %mnemonic, error = %e, "DID sync: failed to read log");
return;
}
};
let witness_content = match dids_ks
.get_raw(did_ops::content_witness_key(&mnemonic))
.await
{
Ok(Some(bytes)) => String::from_utf8(bytes).ok(),
_ => None,
};
let body = json!({
"mnemonic": mnemonic,
"did_id": record.did_id.unwrap_or_default(),
"log_content": log_content,
"witness_content": witness_content,
"version_count": record.version_count,
});
let servers = match get_active_servers(®istry_ks).await {
Some(s) => s,
None => {
warn!(mnemonic = %mnemonic, "DID sync: no active servers in registry");
return;
}
};
for (server_did, instance_id) in &servers {
if let Err(e) =
crate::outbox::enqueue(&store, server_did, MSG_SYNC_UPDATE, body.clone()).await
{
warn!(
server_did,
instance_id,
mnemonic = %mnemonic,
error = %e,
"DID sync: outbox enqueue failed"
);
} else {
info!(
server_did,
instance_id,
mnemonic = %mnemonic,
"DID sync: queued for server"
);
}
}
notify.notify_one();
});
}
pub fn notify_servers_delete(state: &AppState, mnemonic: String) {
let registry_ks = state.registry_ks.clone();
let store = state.store.clone();
let notify = state.outbox_notify.clone();
tokio::spawn(async move {
info!(mnemonic = %mnemonic, "DID deleted — queueing sync to servers");
let servers = match get_active_servers(®istry_ks).await {
Some(s) => s,
None => {
warn!(mnemonic = %mnemonic, "DID delete sync: no active servers in registry");
return;
}
};
let body = json!({ "mnemonic": mnemonic });
for (server_did, instance_id) in &servers {
if let Err(e) =
crate::outbox::enqueue(&store, server_did, MSG_SYNC_DELETE, body.clone()).await
{
warn!(
server_did,
instance_id,
mnemonic = %mnemonic,
error = %e,
"DID delete sync: outbox enqueue failed"
);
} else {
info!(
server_did,
instance_id,
mnemonic = %mnemonic,
"DID delete sync: queued for server"
);
}
}
notify.notify_one();
});
}
async fn get_active_servers(
registry_ks: &crate::store::KeyspaceHandle,
) -> Option<Vec<(String, String)>> {
let instances = match registry::list_instances(registry_ks).await {
Ok(i) => i,
Err(e) => {
warn!(error = %e, "server push: failed to list instances");
return None;
}
};
let servers: Vec<_> = instances
.into_iter()
.filter(|i| {
i.service_type == ServiceType::Server && i.status == registry::ServiceStatus::Active
})
.filter_map(|i| {
let did = i.metadata.get("did")?.as_str()?.to_string();
Some((did, i.instance_id))
})
.collect();
if servers.is_empty() {
None
} else {
Some(servers)
}
}
pub async fn send_domain_assign(
state: &AppState,
target_did: &str,
domain: &str,
) -> Result<(), did_hosting_common::server::error::AppError> {
crate::outbox::enqueue_and_notify(
state,
target_did,
MSG_DOMAIN_ASSIGN,
json!({ "domain": domain }),
)
.await?;
Ok(())
}
pub async fn send_domain_purge(
state: &AppState,
target_did: &str,
domain: &str,
) -> Result<(), did_hosting_common::server::error::AppError> {
crate::outbox::enqueue_and_notify(
state,
target_did,
MSG_DOMAIN_PURGE,
json!({ "domain": domain }),
)
.await?;
Ok(())
}
pub async fn send_domain_unassign(
state: &AppState,
target_did: &str,
domain: &str,
) -> Result<(), did_hosting_common::server::error::AppError> {
crate::outbox::enqueue_and_notify(
state,
target_did,
MSG_DOMAIN_UNASSIGN,
json!({ "domain": domain }),
)
.await?;
Ok(())
}
pub async fn send_domain_upsert(
state: &AppState,
target_did: &str,
entry: &did_hosting_common::server::domain::DomainEntry,
) -> Result<(), did_hosting_common::server::error::AppError> {
let body = serde_json::to_value(entry).map_err(|e| {
did_hosting_common::server::error::AppError::Internal(format!("serialise DomainEntry: {e}"))
})?;
crate::outbox::enqueue_and_notify(state, target_did, MSG_DOMAIN_UPSERT, body).await?;
Ok(())
}
pub async fn fanout_domain_upsert(
state: &AppState,
entry: &did_hosting_common::server::domain::DomainEntry,
) -> (usize, usize) {
let instances = match registry::list_instances(&state.registry_ks).await {
Ok(v) => v,
Err(e) => {
warn!(error = %e, "fanout_domain_upsert: failed to list registry — no servers notified");
return (0, 0);
}
};
let mut enqueued = 0;
let mut skipped = 0;
for instance in instances {
let did = match instance.metadata.get("did").and_then(|v| v.as_str()) {
Some(d) => d.to_string(),
None => {
skipped += 1;
continue;
}
};
match send_domain_upsert(state, &did, entry).await {
Ok(()) => enqueued += 1,
Err(e) => {
warn!(
target_did = %did,
domain = %entry.name,
error = %e,
"fanout_domain_upsert: enqueue failed"
);
skipped += 1;
}
}
}
(enqueued, skipped)
}