huskies: merge 743

This commit is contained in:
dave
2026-04-27 23:44:36 +00:00
parent bf1393fa60
commit 9b24c2e281
3 changed files with 174 additions and 1 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ mod write;
pub use ops::{all_ops_json, apply_remote_op, ops_since, our_vector_clock, subscribe_ops}; pub use ops::{all_ops_json, apply_remote_op, ops_since, our_vector_clock, subscribe_ops};
pub use presence::{ pub use presence::{
is_claimed_by_us, our_node_id, read_all_node_presence, release_claim, sign_challenge, is_claimed_by_us, our_node_id, read_all_node_presence, release_claim, sign_challenge,
sign_versioned_challenge, write_claim, write_node_presence, sign_versioned_challenge, write_claim, write_node_metadata, write_node_presence,
}; };
pub use read::{ pub use read::{
CrdtItemDump, CrdtStateDump, check_archived_deps_crdt, check_unmet_deps_crdt, CrdtItemDump, CrdtStateDump, check_archived_deps_crdt, check_unmet_deps_crdt,
+51
View File
@@ -162,10 +162,46 @@ pub fn write_node_presence(node_id: &str, address: &str, last_seen: f64, alive:
node.address.advance_seq(floor); node.address.advance_seq(floor);
node.last_seen.advance_seq(floor); node.last_seen.advance_seq(floor);
node.alive.advance_seq(floor); node.alive.advance_seq(floor);
node.label.advance_seq(floor);
node.assigned_project.advance_seq(floor);
node.last_seen_ms.advance_seq(floor);
} }
} }
} }
/// Write agent metadata fields for an existing node presence entry.
///
/// Updates `label`, `assigned_project`, and `last_seen_ms` for the node
/// identified by `node_id`. Does nothing if the node does not exist or the
/// CRDT is not initialised.
pub fn write_node_metadata(
node_id: &str,
label: &str,
assigned_project: Option<&str>,
last_seen_ms: f64,
) {
let Some(state_mutex) = get_crdt() else {
return;
};
let Ok(mut state) = state_mutex.lock() else {
return;
};
let Some(&idx) = state.node_index.get(node_id) else {
return;
};
apply_and_persist(&mut state, |s| {
s.crdt.doc.nodes[idx].label.set(label.to_string())
});
apply_and_persist(&mut state, |s| {
s.crdt.doc.nodes[idx]
.assigned_project
.set(assigned_project.unwrap_or("").to_string())
});
apply_and_persist(&mut state, |s| {
s.crdt.doc.nodes[idx].last_seen_ms.set(last_seen_ms)
});
}
/// Read all node presence entries from the CRDT document. /// Read all node presence entries from the CRDT document.
/// ///
/// Returns `None` before `init()`. /// Returns `None` before `init()`.
@@ -200,10 +236,25 @@ fn extract_node_view(node: &NodePresenceCrdt) -> Option<NodePresenceView> {
JsonValue::Bool(b) => b, JsonValue::Bool(b) => b,
_ => true, _ => true,
}; };
let label = match node.label.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
};
let assigned_project = match node.assigned_project.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
};
let last_seen_ms = match node.last_seen_ms.view() {
JsonValue::Number(n) if n > 0.0 => Some(n),
_ => None,
};
Some(NodePresenceView { Some(NodePresenceView {
node_id, node_id,
address, address,
last_seen, last_seen,
alive, alive,
label,
assigned_project,
last_seen_ms,
}) })
} }
+122
View File
@@ -74,6 +74,12 @@ pub struct NodePresenceCrdt {
pub last_seen: LwwRegisterCrdt<f64>, pub last_seen: LwwRegisterCrdt<f64>,
/// `false` once a stale-detection pass has tombstoned this node. /// `false` once a stale-detection pass has tombstoned this node.
pub alive: LwwRegisterCrdt<bool>, pub alive: LwwRegisterCrdt<bool>,
/// Human-readable display name for this node, e.g. `"builder-1"`.
pub label: LwwRegisterCrdt<String>,
/// Project slug this node is assigned to, or empty string if unassigned.
pub assigned_project: LwwRegisterCrdt<String>,
/// Unix timestamp in **milliseconds** of the last heartbeat (higher precision than `last_seen`).
pub last_seen_ms: LwwRegisterCrdt<f64>,
} }
// ── Read-side view types ───────────────────────────────────────────── // ── Read-side view types ─────────────────────────────────────────────
@@ -105,6 +111,12 @@ pub struct NodePresenceView {
/// Unix timestamp (seconds). /// Unix timestamp (seconds).
pub last_seen: f64, pub last_seen: f64,
pub alive: bool, pub alive: bool,
/// Human-readable display name; `None` if not yet set.
pub label: Option<String>,
/// Project slug this node is assigned to; `None` if unassigned.
pub assigned_project: Option<String>,
/// Unix timestamp in milliseconds; `None` if not yet set.
pub last_seen_ms: Option<f64>,
} }
#[cfg(test)] #[cfg(test)]
@@ -229,6 +241,116 @@ mod tests {
}); });
} }
#[test]
fn node_metadata_fields_replicate_to_peer() {
// Create two independent CRDT instances simulating two peers.
let kp_a = make_keypair();
let kp_b = make_keypair();
let mut crdt_a = BaseCrdt::<PipelineDoc>::new(&kp_a);
let mut crdt_b = BaseCrdt::<PipelineDoc>::new(&kp_b);
// Peer A inserts a node entry with the new metadata fields.
let node_json: JsonValue = serde_json::json!({
"node_id": "aabbccdd",
"address": "ws://192.168.1.10:3001/crdt-sync",
"last_seen": 1_000_000.0_f64,
"alive": true,
"label": "builder-1",
"assigned_project": "my-project",
"last_seen_ms": 1_000_000_123.0_f64,
})
.into();
let insert_op = crdt_a.doc.nodes.insert(ROOT_ID, node_json).sign(&kp_a);
assert_eq!(crdt_a.apply(insert_op.clone()), OpState::Ok);
// Replicate the insert op to peer B.
assert_eq!(crdt_b.apply(insert_op), OpState::Ok);
// Assert the node appeared on peer B.
assert_eq!(crdt_b.doc.nodes.view().len(), 1);
let node = &crdt_b.doc.nodes[0];
assert_eq!(
node.node_id.view(),
JsonValue::String("aabbccdd".to_string())
);
assert_eq!(
node.label.view(),
JsonValue::String("builder-1".to_string())
);
assert_eq!(
node.assigned_project.view(),
JsonValue::String("my-project".to_string())
);
assert_eq!(node.last_seen_ms.view(), JsonValue::Number(1_000_000_123.0));
// Peer A updates label and last_seen_ms via separate ops.
let label_op = crdt_a.doc.nodes[0]
.label
.set("builder-1-renamed".to_string())
.sign(&kp_a);
let ms_op = crdt_a.doc.nodes[0]
.last_seen_ms
.set(2_000_000_000.0_f64)
.sign(&kp_a);
assert_eq!(crdt_a.apply(label_op.clone()), OpState::Ok);
assert_eq!(crdt_a.apply(ms_op.clone()), OpState::Ok);
// Replicate updates to peer B.
assert_eq!(crdt_b.apply(label_op), OpState::Ok);
assert_eq!(crdt_b.apply(ms_op), OpState::Ok);
// Both peers converge to the same state.
assert_eq!(
crdt_b.doc.nodes[0].label.view(),
JsonValue::String("builder-1-renamed".to_string())
);
assert_eq!(
crdt_b.doc.nodes[0].last_seen_ms.view(),
JsonValue::Number(2_000_000_000.0)
);
assert_eq!(
crdt_a.doc.nodes[0].label.view(),
crdt_b.doc.nodes[0].label.view()
);
assert_eq!(
crdt_a.doc.nodes[0].last_seen_ms.view(),
crdt_b.doc.nodes[0].last_seen_ms.view()
);
}
#[test]
fn node_metadata_deserializes_with_defaults_for_pre_existing_entries() {
// Pre-existing entries without new fields should deserialize with sensible defaults.
let kp = make_keypair();
let mut crdt = BaseCrdt::<PipelineDoc>::new(&kp);
// Insert a legacy node without the new fields.
let legacy_json: JsonValue = serde_json::json!({
"node_id": "legacy-node",
"address": "ws://10.0.0.1:3001/crdt-sync",
"last_seen": 500_000.0_f64,
"alive": true,
})
.into();
let op = crdt.doc.nodes.insert(ROOT_ID, legacy_json).sign(&kp);
assert_eq!(crdt.apply(op), OpState::Ok);
let node = &crdt.doc.nodes[0];
// New fields default to Null (not set).
assert_eq!(node.label.view(), JsonValue::Null);
assert_eq!(node.assigned_project.view(), JsonValue::Null);
assert_eq!(node.last_seen_ms.view(), JsonValue::Null);
// Existing fields still work.
assert_eq!(
node.node_id.view(),
JsonValue::String("legacy-node".to_string())
);
assert_eq!(node.alive.view(), JsonValue::Bool(true));
}
#[test] #[test]
fn crdt_event_broadcast_channel_round_trip() { fn crdt_event_broadcast_channel_round_trip() {
let (tx, mut rx) = broadcast::channel::<CrdtEvent>(16); let (tx, mut rx) = broadcast::channel::<CrdtEvent>(16);