use super::owned_save::{source_hash, writable};
use super::Documents;
use serde_json::Value;
use std::collections::HashMap;
const ECAD_BLOCKS: [&str; 2] = ["diagram", "pcb"];
#[derive(Default)]
pub(super) struct LiveLinks {
current: HashMap<String, (u64, String)>,
seen: HashMap<(u64, String), u64>,
published: HashMap<u64, PublishKey>,
walks: u64,
}
#[derive(Clone, PartialEq)]
struct PublishKey {
edit: (u64, u64),
metadata: u64,
baseline: u64,
name: Option<String>,
}
fn publish_key(doc: &super::Document) -> PublishKey {
PublishKey {
edit: doc.edit_key(),
metadata: doc.engine.metadata.stamp(),
baseline: doc.baseline_generation,
name: doc.name().map(str::to_string),
}
}
struct Copy {
key: String,
raw: String,
document: Value,
baseline: Option<u64>,
}
fn embedded_copies(canon: &dyn Fn(&str) -> String, baseline: Option<&Value>, current: &Value, out: &mut Vec<Copy>) {
let Some(entries) = current["partsLibrary"].as_object() else {
return;
};
for (name, entry) in entries {
let base = baseline.map(|b| &b["partsLibrary"][name]).filter(|b| b.is_object());
let raw = entry["sourceKey"].as_str().unwrap_or("");
let key = canon(raw);
if writable(raw) && entry["document"].is_object() && !out.iter().any(|c| c.key == key) {
out.push(Copy {
raw: raw.to_string(),
key,
document: entry["document"].clone(),
baseline: base.map(|b| source_hash(&b["document"])),
});
}
embedded_copies(canon, base.map(|b| &b["document"]), &entry["document"], out);
}
}
fn retarget(document: &mut Value, canon: &dyn Fn(&str) -> String, old: &str, new_key: &str, body: &Value, signature: &str) -> usize {
let Some(entries) = document.get_mut("partsLibrary").and_then(Value::as_object_mut) else {
return 0;
};
let mut changed = 0;
for entry in entries.values_mut() {
if let Some(child) = entry.get_mut("document") {
changed += retarget(child, canon, old, new_key, body, signature);
}
let key = entry["sourceKey"].as_str().unwrap_or("");
if writable(key) && canon(key) == canon(old) {
entry["sourceKey"] = Value::String(new_key.to_string());
entry["sourceSignature"] = Value::String(signature.to_string());
entry["document"] = body.clone();
changed += 1;
}
}
changed
}
impl Documents {
pub fn tabs_holding(&self, key: &str, except: u64) -> Vec<(u64, String)> {
let canon = self.canon.clone();
self.open
.iter()
.filter(|doc| doc.id() != except)
.filter(|doc| {
let mut document: Value = serde_json::from_str(&doc.engine.history_request_json()).unwrap_or(Value::Null);
retarget(&mut document, &*canon, key, "", &Value::Null, "") > 0
})
.map(|doc| (doc.id(), doc.title()))
.collect()
}
pub fn retarget_copies(&mut self, id: u64, old: &str, new_key: &str, text: &str) -> Result<usize, String> {
let canon = self.canon.clone();
let doc = self.open.iter_mut().find(|doc| doc.id() == id).ok_or("that tab is closed")?;
let mut document: Value = serde_json::from_str(&doc.engine.history_request_json()).map_err(|e| e.to_string())?;
let body: Value = serde_json::from_str(text).map_err(|e| e.to_string())?;
let signature = crate::panels::parts_library::document_signature(text);
let moved = retarget(&mut document, &*canon, old, new_key, &body, &signature);
if moved > 0 {
doc.engine.edit_document_json(&document.to_string())?;
}
Ok(moved)
}
}
impl Documents {
fn sources_of(&self, index: usize) -> (Vec<Copy>, Value) {
let doc = &self.open[index];
let current: Value = serde_json::from_str(&doc.engine.history_request_json()).unwrap_or(Value::Null);
let mut baseline: Option<Value> = doc.saved_signature.as_deref().and_then(|s| serde_json::from_str(s).ok());
if let Some(object) = baseline.as_mut().and_then(Value::as_object_mut) {
if current.get("metadata").is_none() {
object.remove("metadata");
}
}
let mut copies = Vec::new();
if let Some(name) = doc.name().filter(|name| writable(name)) {
copies.push(Copy {
key: (self.canon)(name),
raw: name.to_string(),
document: current.clone(),
baseline: baseline.as_ref().map(source_hash),
});
}
embedded_copies(self.canon.as_ref(), baseline.as_ref(), ¤t, &mut copies);
(copies, current)
}
pub(super) fn publish_live(&mut self, index: usize) {
if index >= self.open.len() {
return;
}
let id = self.open[index].id();
let key = publish_key(&self.open[index]);
if self.live.published.get(&id) == Some(&key) {
return;
}
if self.open[index].is_its_baseline() && !self.live.seen.keys().any(|(tab, _)| *tab == id) {
self.live.published.insert(id, key);
return;
}
self.live.walks += 1;
let (copies, _) = self.sources_of(index);
self.live.published.insert(id, key);
for copy in copies {
let hash = source_hash(©.document);
let reference = self.live.seen.get(&(id, copy.key.clone())).copied().or(copy.baseline);
if reference != Some(hash) {
self.live.current.insert(copy.key.clone(), (hash, copy.document.to_string()));
self.live.seen.insert((id, copy.key), hash);
}
}
}
pub(super) fn pull_live(&mut self, index: usize) {
if index >= self.open.len() || self.live.current.is_empty() {
return;
}
let id = self.open[index].id();
let own = self.open[index].name().map(|name| (self.canon)(name));
let (copies, mut document) = self.sources_of(index);
let ecad = ECAD_BLOCKS.iter().any(|key| document.get(*key).is_some_and(|block| !block.is_null()));
let mut pulled_parts = Vec::new();
let mut own_state = None;
let mut embedded_changed = false;
let mut notes = Vec::new();
for copy in copies {
let Some((hash, text)) = self.live.current.get(©.key).cloned() else {
continue;
};
let is_own = own.as_deref() == Some(copy.key.as_str());
if self.live.seen.insert((id, copy.key.clone()), hash) != Some(hash) {
self.live.published.remove(&id);
}
if source_hash(©.document) == hash {
continue;
}
if is_own {
own_state = Some(text);
continue;
}
pulled_parts.push(crate::store::model_display_name(©.raw));
let Ok(state) = serde_json::from_str::<Value>(&text) else {
continue;
};
match super::work_part::refresh_parts(&mut document, &[], Some(©.raw), &state, &mut notes) {
Ok(changed) => embedded_changed |= changed,
Err(error) => notes.push(format!("'{}' not updated: {error}", copy.key)),
}
}
let engine = &mut self.open[index].engine;
let result = match (own_state, embedded_changed) {
(Some(text), _) => engine.edit_document_json(&text).map(drop),
(None, true) => engine.edit_document_json(&document.to_string()).map(drop),
(None, false) => Ok(()),
};
if let Err(error) = result {
engine.push_notice(format!("Linked documents: could not take the newer state: {error}"));
}
if ecad && !pulled_parts.is_empty() {
engine.push_notice_as(
brep_render::engine_state::NoticeSeverity::Warning,
format!(
"Linked documents: this board took the newer {} from its open tab{} — Undo takes it back",
pulled_parts.join(", "),
if notes.is_empty() { String::new() } else { format!(": {}", notes.join("; ")) }
),
);
notes.clear();
}
for note in notes {
engine.push_notice(format!("Linked documents: {note}"));
}
}
pub(super) fn forget_live(&mut self, id: u64) {
self.live.seen.retain(|(tab, _), _| *tab != id);
self.live.published.remove(&id);
}
pub(super) fn forget_published(&mut self) {
self.live.published.clear();
}
}