1use crate::automation::command::{lookup, Ctx, Envelope, Handler, Notice, NoticeKind, Outcome, Phase, Reply};
24use crate::automation::pointer::Pointer;
25use std::collections::VecDeque;
26use std::sync::mpsc::{channel, Receiver, Sender};
27use std::sync::{Arc, Mutex};
28
29pub struct Pending {
30 pub envelope: Envelope,
31 pub reply: Sender<Reply>,
32 deadline: Option<std::time::Instant>,
33}
34
35impl Pending {
36 fn expired(&self, frame: u64) -> bool {
37 if self.deadline.is_some_and(|d| std::time::Instant::now() >= d) {
38 let _ = self.reply.send(Reply::err(self.envelope.id, frame,
39 "host_deadline_expired: command expired before dispatch; no effects", vec![]));
40 true
41 } else { false }
42 }
43}
44
45struct Awaiting {
46 token: u64,
47 region: crate::automation::cmd_capture::Region,
48 document: Option<String>,
49 pending: Pending,
50}
51
52struct Polling {
53 pending: Pending,
54 phase: Phase,
55 poller: crate::automation::command::Poller,
56 bound: Option<String>,
60}
61
62#[derive(Default)]
63struct Inner {
64 pending: VecDeque<Pending>,
65 awaiting: Vec<Awaiting>,
66 polling: Vec<Polling>,
67 notices: Vec<Notice>,
68 notices_replied: usize,
71 pointer: Pointer,
72 next_token: u64,
73 frame: u64,
74 poisoned: Option<String>,
75 view: Option<(String, u64)>,
78 layout_at: Option<web_time::Instant>,
80 layout_dirty: bool,
83 input_this_pass: bool,
87}
88
89impl Inner {
90 fn layout_current(&self) -> bool {
92 !self.layout_dirty && self.layout_at.is_some_and(|at| at.elapsed() < LAYOUT_FRESH)
93 }
94
95 fn front_need(&self) -> Option<Need> {
97 let p = self.pending.front()?;
98 Some(lookup(&p.envelope.cmd).map_or(Need::State, |spec| need(spec, &p.envelope.args)))
99 }
100
101 fn front_dispatchable(&self) -> bool {
103 match self.front_need() {
104 Some(Need::State) => true,
105 Some(Need::Layout) => self.layout_current(),
106 _ => false,
107 }
108 }
109}
110
111const MAX_NOTICES: usize = 256;
116
117impl Inner {
118 fn reply_notices(&mut self) -> Vec<Notice> {
130 let from = self.notices_replied.min(self.notices.len());
131 self.notices_replied = self.notices.len();
132 self.notices[from..].to_vec()
133 }
134}
135
136pub type Waker = Arc<dyn Fn() + Send + Sync>;
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
141pub enum Need {
142 State,
144 Layout,
149 Pass,
152}
153
154pub const PASS_BOUND: &[&str] = &["screenshot", "illustration_presentation", "ping"];
158
159pub const LAYOUT_BOUND: &[&str] = &["hit_rects", "state_list", "state_get", "state_schema", "locate", "pick"];
163
164pub const LAYOUT_FRESH: std::time::Duration = std::time::Duration::from_millis(16);
169
170pub fn need(spec: &crate::automation::command::CommandSpec, args: &serde_json::Value) -> Need {
172 match spec.handler {
173 Handler::Input(_) if spec.phase == Phase::Input && spec.annotations.read_only => Need::State,
177 Handler::Input(_) => Need::Pass,
178 Handler::App(_) if spec.phase == Phase::Input || PASS_BOUND.contains(&spec.name) => Need::Pass,
179 Handler::App(_) if matches!(spec.name, "state_get" | "state_schema") => {
180 let names = args.get("names").and_then(serde_json::Value::as_array);
181 let state = names.is_some_and(|names| {
182 names.iter().all(|n| n.as_str().is_some_and(crate::automation::state_blobs::is_state_blob))
183 });
184 if state { Need::State } else { Need::Layout }
185 }
186 Handler::App(_) if LAYOUT_BOUND.contains(&spec.name) => Need::Layout,
187 Handler::App(_) => Need::State,
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub enum Withdrawn {
195 NotDispatched,
197 Accepted,
200}
201
202#[derive(Default)]
203pub struct AutomationQueue {
204 inner: Mutex<Inner>,
205 ctx: Mutex<Option<egui::Context>>,
206 waker: Mutex<Option<Waker>>,
207}
208
209impl AutomationQueue {
210 pub fn new() -> Arc<Self> {
211 Arc::new(Self::default())
212 }
213
214 pub fn attach(&self, ctx: &egui::Context) {
216 *self.ctx.lock().unwrap() = Some(ctx.clone());
217 if self.has_pending() { ctx.request_repaint(); }
218 }
219
220 pub fn set_waker(&self, waker: Waker) {
223 *self.waker.lock().unwrap() = Some(waker.clone());
224 if self.has_pending() { waker(); }
225 }
226
227 fn wake(&self) {
228 let waker = self.waker.lock().unwrap().clone();
229 if let Some(waker) = waker { waker(); }
230 if let Some(ctx) = self.ctx.lock().unwrap().as_ref() {
231 ctx.request_repaint();
232 }
233 }
234
235 pub fn submit(&self, envelope: Envelope) -> Receiver<Reply> {
239 self.submit_before(envelope, None)
240 }
241
242 pub fn submit_before(&self, envelope: Envelope, deadline: Option<std::time::Instant>) -> Receiver<Reply> {
244 let (tx, rx) = channel();
245 let mut inner = self.inner.lock().unwrap();
246 if let Some(why) = &inner.poisoned {
247 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("session poisoned: {why}"), vec![]));
248 return rx;
249 }
250 if lookup(&envelope.cmd).is_none() {
251 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("unknown command `{}`", envelope.cmd), vec![]));
252 return rx;
253 }
254 inner.pending.push_back(Pending { envelope, reply: tx, deadline });
255 drop(inner);
258 self.wake();
259 rx
260 }
261
262 pub fn withdraw(&self, id: u64) -> Withdrawn {
265 let mut inner = self.inner.lock().unwrap();
266 let frame = inner.frame;
267 if let Some(index) = inner.pending.iter().position(|p| p.envelope.id == id) {
268 let p = inner.pending.remove(index).expect("index from position");
269 let _ = p.reply.send(Reply::err(id, frame, "host_deadline_expired: command expired before dispatch; no effects", vec![]));
270 return Withdrawn::NotDispatched;
271 }
272 Withdrawn::Accepted
273 }
274
275 pub fn has_frameless_work(&self) -> bool {
278 let inner = self.inner.lock().unwrap();
279 !inner.polling.is_empty() || inner.front_dispatchable()
280 }
281
282 pub fn needs_pass(&self) -> bool {
285 let inner = self.inner.lock().unwrap();
286 inner.front_need().is_some() && !inner.front_dispatchable()
287 }
288
289 pub fn mark_layout_dirty(&self) {
292 self.inner.lock().unwrap().layout_dirty = true;
293 }
294
295 pub fn note_pass(&self) {
298 let mut inner = self.inner.lock().unwrap();
299 inner.layout_at = Some(web_time::Instant::now());
300 inner.layout_dirty = std::mem::take(&mut inner.input_this_pass);
301 }
302
303 pub fn push_notice(&self, kind: NoticeKind, text: impl Into<String>) {
307 let mut inner = self.inner.lock().unwrap();
308 let frame = inner.frame;
309 inner.notices.push(Notice { kind, frame, text: text.into() });
310 let overflow = inner.notices.len().saturating_sub(MAX_NOTICES);
311 if overflow > 0 {
312 inner.notices.drain(0..overflow);
313 inner.notices_replied = inner.notices_replied.saturating_sub(overflow);
314 }
315 }
316
317 pub fn take_notices(&self) -> Vec<Notice> {
321 let mut inner = self.inner.lock().unwrap();
322 inner.notices_replied = 0;
323 std::mem::take(&mut inner.notices)
324 }
325
326 pub fn pointer(&self) -> Pointer {
327 self.inner.lock().unwrap().pointer.clone()
328 }
329
330 pub fn frame(&self) -> u64 {
331 self.inner.lock().unwrap().frame
332 }
333
334 pub fn has_pending(&self) -> bool {
335 let inner = self.inner.lock().unwrap();
336 !inner.pending.is_empty() || !inner.awaiting.is_empty() || !inner.polling.is_empty()
337 }
338
339 pub fn is_polling(&self, id: u64) -> bool {
343 self.inner.lock().unwrap().polling.iter().any(|p| p.pending.envelope.id == id)
344 }
345
346 pub fn poison(&self, why: impl Into<String>) {
349 let why = why.into();
350 let mut inner = self.inner.lock().unwrap();
351 inner.poisoned = Some(why.clone());
352 let frame = inner.frame;
353 for p in inner.pending.drain(..) {
354 let _ = p.reply.send(Reply::err(p.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
355 }
356 for a in inner.awaiting.drain(..) {
357 let _ = a.pending.reply.send(Reply::err(a.pending.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
358 }
359 }
360
361 pub fn is_poisoned(&self) -> bool {
362 self.inner.lock().unwrap().poisoned.is_some()
363 }
364
365 pub fn drain_input(&self, raw: &mut egui::RawInput, frame: u64, ppp: f32, view: Option<egui::Rect>, ctx: &egui::Context) {
372 let mut inner = self.inner.lock().unwrap();
373 inner.frame = frame;
374 inner.pointer.now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time) + f64::from(raw.predicted_dt));
375
376 if !inner.awaiting.is_empty() {
378 let mut done = Vec::new();
379 for event in &raw.events {
380 if let egui::Event::Screenshot { user_data, image, .. } = event {
381 let token = user_data.data.as_ref().and_then(|d| d.downcast_ref::<u64>()).copied();
382 if let Some(token) = token {
383 if let Some(pos) = inner.awaiting.iter().position(|a| a.token == token) {
384 let a = inner.awaiting.remove(pos);
385 done.push((a, image.clone()));
386 }
387 }
388 }
389 }
390 for (a, image) in done {
391 let notices = inner.reply_notices();
392 let shown = inner.view.clone();
393 let switched = match (&a.document, &shown) {
397 (Some(asked), Some((drawn, _))) => asked != drawn,
398 _ => false,
399 };
400 let result = if switched {
401 Err("document_not_active: the visible document changed between the request and the capture".to_string())
402 } else {
403 crate::automation::cmd_capture::encode_capture(&image, ppp, view, &a.region).map(|(mut json, png)| {
404 if let Some((handle, revision)) = &shown {
405 json["document"] = serde_json::json!({ "documentId": handle, "revision": revision.to_string() });
406 }
407 (json, png)
408 })
409 };
410 let reply = match result {
411 Ok((json, png)) => {
412 let mut r = Reply::ok(a.pending.envelope.id, frame, json, notices);
413 r.blob = Some(png);
414 r
415 }
416 Err(e) => Reply::err(a.pending.envelope.id, frame, e, notices),
417 };
418 let _ = a.pending.reply.send(reply);
419 }
420 }
421
422 let mut keep = VecDeque::new();
424 let mut events = Vec::new();
425 while let Some(p) = inner.pending.pop_front() {
426 if p.expired(frame) { continue; }
427 let Some(spec) = lookup(&p.envelope.cmd) else { continue };
428 match (&spec.handler, spec.phase) {
429 (Handler::Input(f), Phase::Input) => {
430 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
431 let notices = inner.reply_notices();
432 let reply = match result {
433 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
434 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
435 };
436 let _ = p.reply.send(reply);
437 }
438 _ => keep.push_back(p),
439 }
440 }
441 inner.pending = keep;
442 if std::mem::take(&mut inner.pointer.reset_clicks) {
446 ctx.input_mut(|i| i.pointer = egui::PointerState::default());
447 }
448 raw.modifiers = inner.pointer.modifiers.egui();
449 inner.input_this_pass = !events.is_empty();
450 raw.events.extend(events);
451 }
452
453 pub fn drain_app(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
456 let batch: Vec<Pending> = {
457 let mut inner = self.inner.lock().unwrap();
458 inner.frame = frame;
459 let mut keep = VecDeque::new();
460 let mut batch = Vec::new();
461 while let Some(p) = inner.pending.pop_front() {
462 match lookup(&p.envelope.cmd) {
463 Some(spec) if spec.phase == phase => batch.push(p),
464 _ => keep.push_back(p),
465 }
466 }
467 inner.pending = keep;
468 batch
469 };
470 for p in batch {
471 self.dispatch(p, phase, ctx, frame);
472 }
473 if self.poll(phase, ctx, frame) {
474 ctx.egui.request_repaint();
475 }
476 }
477
478 pub fn drain_frameless(&self, ctx: &mut Ctx<'_>) -> bool {
485 let frame = self.frame();
486 loop {
487 let next = {
488 let mut inner = self.inner.lock().unwrap();
489 let ready = inner.front_dispatchable();
490 match inner.pending.front().and_then(|p| lookup(&p.envelope.cmd)) {
491 Some(spec) if ready => {
492 let p = inner.pending.pop_front().expect("front exists");
493 if let Handler::Input(f) = &spec.handler {
494 if !p.expired(frame) {
497 let mut events = Vec::new();
498 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
499 debug_assert!(events.is_empty(), "{} is read-only but emitted input", spec.name);
500 let notices = inner.reply_notices();
501 let reply = match result {
502 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
503 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
504 };
505 let _ = p.reply.send(reply);
506 }
507 continue;
508 }
509 Some((p, spec.phase))
510 }
511 _ => None,
512 }
513 };
514 let Some((p, phase)) = next else { break };
515 self.dispatch(p, phase, ctx, frame);
516 }
517 let mutate = self.poll(Phase::Mutate, ctx, frame);
518 let read = self.poll(Phase::Read, ctx, frame);
519 mutate || read
520 }
521
522 fn dispatch(&self, p: Pending, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
524 if p.expired(frame) { return; }
525 let spec = lookup(&p.envelope.cmd).expect("checked at submit");
526 let Handler::App(f) = &spec.handler else { return };
527 if !spec.annotations.read_only {
528 self.mark_layout_dirty();
531 }
532 let (args, bound) = match bind_context(&ctx.app.docs, phase, &p.envelope.cmd, p.envelope.args.clone()) {
536 Ok(bound) => bound,
537 Err(e) => {
538 let mut inner = self.inner.lock().unwrap();
539 let notices = inner.reply_notices();
540 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
541 return;
542 }
543 };
544 let before = refused_counts(&ctx.app.docs);
545 let route = bound.as_ref().and_then(|b| b.route).map(|index| ctx.app.docs.route_begin(index));
546 let result = f(ctx, args);
547 if let Some(visible) = route {
548 ctx.app.docs.route_end(visible);
549 }
550 let result = refused_by_lock(&ctx.app.docs, &before, result);
551 let handle = bound.map(|b| b.handle);
552 let result = match &handle {
553 Some(handle) => tag_context(&ctx.app.docs, handle, result),
554 None => result,
555 };
556 let mut inner = self.inner.lock().unwrap();
557 answer(&mut inner, p, phase, frame, result, handle);
558 }
559
560 fn poll(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) -> bool {
563 let polling: Vec<Polling> = {
564 let mut inner = self.inner.lock().unwrap();
565 let (mine, rest) = std::mem::take(&mut inner.polling).into_iter().partition(|p| p.phase == phase);
566 inner.polling = rest;
567 mine
568 };
569 let mut still = false;
570 for mut entry in polling {
571 match (entry.poller)(ctx) {
572 None => {
573 still = true;
574 self.inner.lock().unwrap().polling.push(entry);
575 }
576 Some(result) => {
577 self.mark_layout_dirty();
579 let result = match &entry.bound {
580 Some(handle) => tag_context(&ctx.app.docs, handle, result),
581 None => result,
582 };
583 let mut inner = self.inner.lock().unwrap();
584 answer(&mut inner, entry.pending, phase, frame, result, entry.bound);
585 still |= !inner.polling.is_empty();
586 }
587 }
588 }
589 still
590 }
591
592 pub fn publish_view(&self, handle: &str, revision: u64) {
595 let mut inner = self.inner.lock().unwrap();
596 if inner.view.as_ref().is_none_or(|(h, r)| h != handle || *r != revision) {
597 inner.view = Some((handle.to_string(), revision));
598 }
599 }
600
601 pub fn fail_captures(&self, error: &str) {
604 let mut inner = self.inner.lock().unwrap();
605 let frame = inner.frame;
606 for a in std::mem::take(&mut inner.awaiting) {
607 let notices = inner.reply_notices();
608 let _ = a.pending.reply.send(Reply::err(a.pending.envelope.id, frame, error.to_string(), notices));
609 }
610 }
611
612 pub fn next_token(&self) -> u64 {
614 let mut inner = self.inner.lock().unwrap();
615 inner.next_token += 1;
616 inner.next_token
617 }
618}
619
620#[derive(serde::Deserialize)]
625#[serde(deny_unknown_fields, rename_all = "camelCase")]
626struct DocumentContext {
627 document_id: String,
628 expected_revision: String,
629}
630
631pub const ENGINE_ONLY: &[&str] = &[
637 "feature_stage",
638 "feature_stage_result",
639 "run_cancel",
640 "history_group_begin",
641 "history_group_end",
642 "feature_add_many",
643 "doc_import",
644 "doc_export",
645 "model_inspect",
646 "mass_properties",
647 "history_listing",
648 "frame_info",
649 "metadata_set",
650 "metadata_set_many",
651 "metadata_remove",
652];
653
654#[derive(Debug)]
657struct Bound {
658 handle: String,
659 route: Option<usize>,
660}
661
662fn bind_context(
673 docs: &crate::document::Documents,
674 phase: Phase,
675 cmd: &str,
676 mut args: serde_json::Value,
677) -> Result<(serde_json::Value, Option<Bound>), String> {
678 let Some(raw) = args.as_object_mut().and_then(|o| o.remove("context")) else {
679 return Ok((args, None));
680 };
681 let context: DocumentContext = serde_json::from_value(raw)
683 .map_err(|e| format!("invalid_arguments: context must be {{documentId, expectedRevision}}: {e}"))?;
684 let expected = parse_revision(&context.expected_revision)?;
685 match crate::handles::parse("doc", &context.document_id) {
686 crate::handles::Parsed::Current => {}
687 crate::handles::Parsed::OtherEpoch => {
688 return Err("handle_epoch_mismatch: the document handle is from another run of the app".into())
689 }
690 crate::handles::Parsed::Malformed => return Err("invalid_arguments: documentId is not a document handle".into()),
691 }
692 let Some(index) = docs.index_of_handle(&context.document_id) else {
693 return Err("document_unknown: no open document has that handle (closed, or never opened)".into());
694 };
695 if docs.work_part_in_place() {
696 return Err("document_busy: a Work part is edited in place; a document command would write the part".into());
697 }
698 let route = if index == docs.active_index() {
699 None
700 } else if ENGINE_ONLY.contains(&cmd) {
701 Some(index)
702 } else {
703 return Err("document_not_active: this command acts on the visible document; activate the named one first".into());
704 };
705 let current = docs.get(index).map_or(0, |d| d.committed_revision());
706 if phase == Phase::Mutate && current != expected {
707 return Err(format!(
708 "revision_conflict: the document is at revision {current}, not {expected}; read it again before writing"
709 ));
710 }
711 Ok((args, Some(Bound { handle: context.document_id, route })))
712}
713
714fn parse_revision(text: &str) -> Result<u64, String> {
716 let canonical = text == "0" || (!text.is_empty() && !text.starts_with('0') && text.bytes().all(|b| b.is_ascii_digit()));
717 text.parse::<u64>()
718 .ok()
719 .filter(|_| canonical)
720 .ok_or_else(|| "invalid_arguments: expectedRevision must be a canonical unsigned decimal string".into())
721}
722
723fn tag_context(
729 docs: &crate::document::Documents,
730 handle: &str,
731 result: Result<Outcome, String>,
732) -> Result<Outcome, String> {
733 let context = docs.index_of_handle(handle).and_then(|i| docs.get(i)).map(|doc| {
734 serde_json::json!({ "documentId": handle, "expectedRevision": doc.committed_revision().to_string() })
735 });
736 let tag = |v: &mut serde_json::Value| {
737 if let (Some(object), Some(context)) = (v.as_object_mut(), context.clone()) {
738 object.insert("context".into(), context);
739 }
740 };
741 match result {
742 Ok(Outcome::Done(mut v)) => {
743 tag(&mut v);
744 Ok(Outcome::Done(v))
745 }
746 Ok(Outcome::Blob { mut json, bytes }) => {
747 tag(&mut json);
748 Ok(Outcome::Blob { json, bytes })
749 }
750 other => other,
751 }
752}
753
754fn answer(inner: &mut Inner, p: Pending, phase: Phase, frame: u64, result: Result<Outcome, String>, bound: Option<String>) {
756 match result {
757 Ok(Outcome::Done(v)) => {
758 let notices = inner.reply_notices();
759 let _ = p.reply.send(Reply::ok(p.envelope.id, frame, v, notices));
760 }
761 Ok(Outcome::Blob { json, bytes }) => {
762 let notices = inner.reply_notices();
763 let mut r = Reply::ok(p.envelope.id, frame, json, notices);
764 r.blob = Some(bytes);
765 let _ = p.reply.send(r);
766 }
767 Ok(Outcome::AwaitScreenshot { token, region, document }) => {
770 inner.awaiting.push(Awaiting { token, region, document, pending: p });
771 }
772 Ok(Outcome::Poll(poller)) => inner.polling.push(Polling { pending: p, phase, poller, bound }),
773 Err(e) => {
774 let notices = inner.reply_notices();
775 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
776 }
777 }
778}
779
780fn refused_counts(docs: &crate::document::Documents) -> Vec<(u64, u64)> {
782 docs.iter().map(|d| (d.id(), d.engine.history.refused_user_edits())).collect()
783}
784
785fn refused_by_lock(
791 docs: &crate::document::Documents,
792 before: &[(u64, u64)],
793 result: Result<Outcome, String>,
794) -> Result<Outcome, String> {
795 if result.is_err() {
796 return result;
797 }
798 for doc in docs.iter() {
799 let was = before.iter().find(|(id, _)| *id == doc.id()).map(|(_, n)| *n);
800 if was.is_some_and(|was| doc.engine.history.refused_user_edits() > was) {
801 if let crate::document::Access::ReadOnly { reason } = doc.access() {
802 return Err(format!("{} is read-only: {reason}", doc.title()));
803 }
804 }
805 }
806 result
807}
808