1use super::owned_save::{source_hash, writable};
25use super::Documents;
26use serde_json::Value;
27use std::collections::HashMap;
28
29const ECAD_BLOCKS: [&str; 2] = ["diagram", "pcb"];
34
35#[derive(Default)]
37pub(super) struct LiveLinks {
38 current: HashMap<String, (u64, String)>,
40 seen: HashMap<(u64, String), u64>,
42 published: HashMap<u64, PublishKey>,
45 walks: u64,
47}
48
49#[derive(Clone, PartialEq)]
55struct PublishKey {
56 edit: (u64, u64),
57 metadata: u64,
58 baseline: u64,
59 name: Option<String>,
60}
61
62fn publish_key(doc: &super::Document) -> PublishKey {
63 PublishKey {
64 edit: doc.edit_key(),
65 metadata: doc.engine.metadata.stamp(),
66 baseline: doc.baseline_generation,
67 name: doc.name().map(str::to_string),
68 }
69}
70
71struct Copy {
73 key: String,
75 raw: String,
77 document: Value,
78 baseline: Option<u64>,
79}
80
81fn embedded_copies(canon: &dyn Fn(&str) -> String, baseline: Option<&Value>, current: &Value, out: &mut Vec<Copy>) {
84 let Some(entries) = current["partsLibrary"].as_object() else {
85 return;
86 };
87 for (name, entry) in entries {
88 let base = baseline.map(|b| &b["partsLibrary"][name]).filter(|b| b.is_object());
89 let raw = entry["sourceKey"].as_str().unwrap_or("");
90 let key = canon(raw);
91 if writable(raw) && entry["document"].is_object() && !out.iter().any(|c| c.key == key) {
92 out.push(Copy {
93 raw: raw.to_string(),
94 key,
95 document: entry["document"].clone(),
96 baseline: base.map(|b| source_hash(&b["document"])),
97 });
98 }
99 embedded_copies(canon, base.map(|b| &b["document"]), &entry["document"], out);
100 }
101}
102
103fn retarget(document: &mut Value, canon: &dyn Fn(&str) -> String, old: &str, new_key: &str, body: &Value, signature: &str) -> usize {
106 let Some(entries) = document.get_mut("partsLibrary").and_then(Value::as_object_mut) else {
107 return 0;
108 };
109 let mut changed = 0;
110 for entry in entries.values_mut() {
111 if let Some(child) = entry.get_mut("document") {
112 changed += retarget(child, canon, old, new_key, body, signature);
113 }
114 let key = entry["sourceKey"].as_str().unwrap_or("");
115 if writable(key) && canon(key) == canon(old) {
116 entry["sourceKey"] = Value::String(new_key.to_string());
117 entry["sourceSignature"] = Value::String(signature.to_string());
118 entry["document"] = body.clone();
119 changed += 1;
120 }
121 }
122 changed
123}
124
125impl Documents {
126 pub fn tabs_holding(&self, key: &str, except: u64) -> Vec<(u64, String)> {
130 let canon = self.canon.clone();
131 self.open
132 .iter()
133 .filter(|doc| doc.id() != except)
134 .filter(|doc| {
135 let mut document: Value = serde_json::from_str(&doc.engine.history_request_json()).unwrap_or(Value::Null);
136 retarget(&mut document, &*canon, key, "", &Value::Null, "") > 0
137 })
138 .map(|doc| (doc.id(), doc.title()))
139 .collect()
140 }
141
142 pub fn retarget_copies(&mut self, id: u64, old: &str, new_key: &str, text: &str) -> Result<usize, String> {
146 let canon = self.canon.clone();
147 let doc = self.open.iter_mut().find(|doc| doc.id() == id).ok_or("that tab is closed")?;
148 let mut document: Value = serde_json::from_str(&doc.engine.history_request_json()).map_err(|e| e.to_string())?;
149 let body: Value = serde_json::from_str(text).map_err(|e| e.to_string())?;
150 let signature = crate::panels::parts_library::document_signature(text);
151 let moved = retarget(&mut document, &*canon, old, new_key, &body, &signature);
152 if moved > 0 {
153 doc.engine.edit_document_json(&document.to_string())?;
154 }
155 Ok(moved)
156 }
157}
158
159impl Documents {
160 fn sources_of(&self, index: usize) -> (Vec<Copy>, Value) {
163 let doc = &self.open[index];
164 let current: Value = serde_json::from_str(&doc.engine.history_request_json()).unwrap_or(Value::Null);
165 let mut baseline: Option<Value> = doc.saved_signature.as_deref().and_then(|s| serde_json::from_str(s).ok());
166 if let Some(object) = baseline.as_mut().and_then(Value::as_object_mut) {
169 if current.get("metadata").is_none() {
170 object.remove("metadata");
171 }
172 }
173 let mut copies = Vec::new();
174 if let Some(name) = doc.name().filter(|name| writable(name)) {
175 copies.push(Copy {
176 key: (self.canon)(name),
177 raw: name.to_string(),
178 document: current.clone(),
179 baseline: baseline.as_ref().map(source_hash),
180 });
181 }
182 embedded_copies(self.canon.as_ref(), baseline.as_ref(), ¤t, &mut copies);
183 (copies, current)
184 }
185
186 pub(super) fn publish_live(&mut self, index: usize) {
188 if index >= self.open.len() {
189 return;
190 }
191 let id = self.open[index].id();
192 let key = publish_key(&self.open[index]);
193 if self.live.published.get(&id) == Some(&key) {
194 return;
195 }
196 if self.open[index].is_its_baseline() && !self.live.seen.keys().any(|(tab, _)| *tab == id) {
199 self.live.published.insert(id, key);
200 return;
201 }
202 self.live.walks += 1;
203 let (copies, _) = self.sources_of(index);
204 self.live.published.insert(id, key);
205 for copy in copies {
206 let hash = source_hash(©.document);
207 let reference = self.live.seen.get(&(id, copy.key.clone())).copied().or(copy.baseline);
208 if reference != Some(hash) {
209 self.live.current.insert(copy.key.clone(), (hash, copy.document.to_string()));
210 self.live.seen.insert((id, copy.key), hash);
211 }
212 }
213 }
214
215 pub(super) fn pull_live(&mut self, index: usize) {
217 if index >= self.open.len() || self.live.current.is_empty() {
218 return;
219 }
220 let id = self.open[index].id();
221 let own = self.open[index].name().map(|name| (self.canon)(name));
222 let (copies, mut document) = self.sources_of(index);
223 let ecad = ECAD_BLOCKS.iter().any(|key| document.get(*key).is_some_and(|block| !block.is_null()));
224 let mut pulled_parts = Vec::new();
225 let mut own_state = None;
226 let mut embedded_changed = false;
227 let mut notes = Vec::new();
228 for copy in copies {
229 let Some((hash, text)) = self.live.current.get(©.key).cloned() else {
230 continue;
231 };
232 let is_own = own.as_deref() == Some(copy.key.as_str());
233 if self.live.seen.insert((id, copy.key.clone()), hash) != Some(hash) {
235 self.live.published.remove(&id);
236 }
237 if source_hash(©.document) == hash {
238 continue;
239 }
240 if is_own {
241 own_state = Some(text);
242 continue;
243 }
244 pulled_parts.push(crate::store::model_display_name(©.raw));
245 let Ok(state) = serde_json::from_str::<Value>(&text) else {
246 continue;
247 };
248 match super::work_part::refresh_parts(&mut document, &[], Some(©.raw), &state, &mut notes) {
249 Ok(changed) => embedded_changed |= changed,
250 Err(error) => notes.push(format!("'{}' not updated: {error}", copy.key)),
251 }
252 }
253 let engine = &mut self.open[index].engine;
254 let result = match (own_state, embedded_changed) {
257 (Some(text), _) => engine.edit_document_json(&text).map(drop),
258 (None, true) => engine.edit_document_json(&document.to_string()).map(drop),
259 (None, false) => Ok(()),
260 };
261 if let Err(error) = result {
262 engine.push_notice(format!("Linked documents: could not take the newer state: {error}"));
263 }
264 if ecad && !pulled_parts.is_empty() {
265 engine.push_notice_as(
269 brep_render::engine_state::NoticeSeverity::Warning,
270 format!(
271 "Linked documents: this board took the newer {} from its open tab{} — Undo takes it back",
272 pulled_parts.join(", "),
273 if notes.is_empty() { String::new() } else { format!(": {}", notes.join("; ")) }
274 ),
275 );
276 notes.clear();
277 }
278 for note in notes {
279 engine.push_notice(format!("Linked documents: {note}"));
280 }
281 }
282
283 pub(super) fn forget_live(&mut self, id: u64) {
285 self.live.seen.retain(|(tab, _), _| *tab != id);
286 self.live.published.remove(&id);
287 }
288
289 pub(super) fn forget_published(&mut self) {
292 self.live.published.clear();
293 }
294}
295