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 client: Option<u64>,
36}
37
38impl Pending {
39 fn expired(&self, frame: u64) -> bool {
40 if self.deadline.is_some_and(|d| std::time::Instant::now() >= d) {
41 let _ = self.reply.send(Reply::err(self.envelope.id, frame,
42 "host_deadline_expired: command expired before dispatch; no effects", vec![]));
43 true
44 } else { false }
45 }
46}
47
48struct Awaiting {
49 token: u64,
50 region: crate::automation::cmd_capture::Region,
51 document: Option<String>,
52 pending: Pending,
53}
54
55struct Polling {
56 pending: Pending,
57 phase: Phase,
58 poller: crate::automation::command::Poller,
59 bound: Option<String>,
63}
64
65#[derive(Default)]
66struct Inner {
67 pending: VecDeque<Pending>,
68 awaiting: Vec<Awaiting>,
69 polling: Vec<Polling>,
70 notices: Vec<Notice>,
71 notices_replied: usize,
74 pointer: Pointer,
75 next_token: u64,
76 frame: u64,
77 poisoned: Option<String>,
78 view: Option<(String, u64)>,
81 layout_at: Option<web_time::Instant>,
83 layout_dirty: bool,
86 input_this_pass: bool,
90}
91
92impl Inner {
93 fn layout_current(&self) -> bool {
95 !self.layout_dirty && self.layout_at.is_some_and(|at| at.elapsed() < LAYOUT_FRESH)
96 }
97
98 fn need_at(&self, index: usize) -> Need {
100 let p = &self.pending[index];
101 lookup(&p.envelope.cmd).map_or(Need::State, |spec| need(spec, &p.envelope.args))
102 }
103
104 fn dispatchable(&self, index: usize) -> bool {
106 match self.need_at(index) {
107 Need::State => true,
108 Need::Layout => self.layout_current(),
109 Need::Pass => false,
110 }
111 }
112
113 fn is_next(&self, index: usize) -> bool {
117 let Some(client) = self.pending[index].client else { return index == 0 };
118 self.pending.iter().take(index).all(|p| p.client.is_some_and(|other| other != client))
119 }
120
121 fn next_dispatchable(&self) -> Option<usize> {
125 (0..self.pending.len()).find(|&i| self.is_next(i) && self.dispatchable(i))
126 }
127
128 fn waits_for_pass(&self) -> bool {
130 (0..self.pending.len()).any(|i| self.is_next(i) && !self.dispatchable(i))
131 }
132}
133
134const MAX_NOTICES: usize = 256;
139
140impl Inner {
141 fn reply_notices(&mut self) -> Vec<Notice> {
153 let from = self.notices_replied.min(self.notices.len());
154 self.notices_replied = self.notices.len();
155 self.notices[from..].to_vec()
156 }
157}
158
159pub type Waker = Arc<dyn Fn() + Send + Sync>;
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub enum Need {
165 State,
167 Layout,
172 Pass,
175}
176
177pub const PASS_BOUND: &[&str] = &["screenshot", "illustration_presentation", "ping"];
181
182pub const LAYOUT_BOUND: &[&str] = &["hit_rects", "state_list", "state_get", "state_schema", "locate", "pick"];
186
187pub const LAYOUT_FRESH: std::time::Duration = std::time::Duration::from_millis(16);
192
193pub fn need(spec: &crate::automation::command::CommandSpec, args: &serde_json::Value) -> Need {
195 match spec.handler {
196 Handler::Input(_) if spec.phase == Phase::Input && spec.annotations.read_only => Need::State,
200 Handler::Input(_) => Need::Pass,
201 Handler::App(_) if spec.phase == Phase::Input || PASS_BOUND.contains(&spec.name) => Need::Pass,
202 Handler::App(_) if matches!(spec.name, "state_get" | "state_schema") => {
203 let names = args.get("names").and_then(serde_json::Value::as_array);
204 let state = names.is_some_and(|names| {
205 names.iter().all(|n| n.as_str().is_some_and(crate::automation::state_blobs::is_state_blob))
206 });
207 if state { Need::State } else { Need::Layout }
208 }
209 Handler::App(_) if LAYOUT_BOUND.contains(&spec.name) => Need::Layout,
210 Handler::App(_) => Need::State,
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
217pub enum Withdrawn {
218 NotDispatched,
220 Accepted,
223}
224
225#[derive(Default)]
226pub struct AutomationQueue {
227 inner: Mutex<Inner>,
228 ctx: Mutex<Option<egui::Context>>,
229 waker: Mutex<Option<Waker>>,
230}
231
232impl AutomationQueue {
233 pub fn new() -> Arc<Self> {
234 Arc::new(Self::default())
235 }
236
237 pub fn attach(&self, ctx: &egui::Context) {
239 *self.ctx.lock().unwrap() = Some(ctx.clone());
240 if self.has_pending() { ctx.request_repaint(); }
241 }
242
243 pub fn set_waker(&self, waker: Waker) {
246 *self.waker.lock().unwrap() = Some(waker.clone());
247 if self.has_pending() { waker(); }
248 }
249
250 fn wake(&self) {
251 let waker = self.waker.lock().unwrap().clone();
252 if let Some(waker) = waker { waker(); }
253 if let Some(ctx) = self.ctx.lock().unwrap().as_ref() {
254 ctx.request_repaint();
255 }
256 }
257
258 pub fn submit(&self, envelope: Envelope) -> Receiver<Reply> {
262 self.submit_before(envelope, None)
263 }
264
265 pub fn submit_before(&self, envelope: Envelope, deadline: Option<std::time::Instant>) -> Receiver<Reply> {
267 self.submit_for(envelope, deadline, None)
268 }
269
270 pub fn submit_for(&self, envelope: Envelope, deadline: Option<std::time::Instant>, client: Option<u64>) -> Receiver<Reply> {
276 let (tx, rx) = channel();
277 let mut inner = self.inner.lock().unwrap();
278 if let Some(why) = &inner.poisoned {
279 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("session poisoned: {why}"), vec![]));
280 return rx;
281 }
282 if lookup(&envelope.cmd).is_none() {
283 let _ = tx.send(Reply::err(envelope.id, inner.frame, format!("unknown command `{}`", envelope.cmd), vec![]));
284 return rx;
285 }
286 inner.pending.push_back(Pending { envelope, reply: tx, deadline, client });
287 drop(inner);
290 self.wake();
291 rx
292 }
293
294 pub fn withdraw(&self, id: u64) -> Withdrawn {
297 let mut inner = self.inner.lock().unwrap();
298 let frame = inner.frame;
299 if let Some(index) = inner.pending.iter().position(|p| p.envelope.id == id) {
300 let p = inner.pending.remove(index).expect("index from position");
301 let _ = p.reply.send(Reply::err(id, frame, "host_deadline_expired: command expired before dispatch; no effects", vec![]));
302 return Withdrawn::NotDispatched;
303 }
304 Withdrawn::Accepted
305 }
306
307 pub fn has_frameless_work(&self) -> bool {
310 let inner = self.inner.lock().unwrap();
311 !inner.polling.is_empty() || inner.next_dispatchable().is_some()
312 }
313
314 pub fn needs_pass(&self) -> bool {
318 self.inner.lock().unwrap().waits_for_pass()
319 }
320
321 pub fn mark_layout_dirty(&self) {
324 self.inner.lock().unwrap().layout_dirty = true;
325 }
326
327 pub fn note_pass(&self) {
330 let mut inner = self.inner.lock().unwrap();
331 inner.layout_at = Some(web_time::Instant::now());
332 inner.layout_dirty = std::mem::take(&mut inner.input_this_pass);
333 }
334
335 pub fn push_notice(&self, kind: NoticeKind, text: impl Into<String>) {
339 let mut inner = self.inner.lock().unwrap();
340 let frame = inner.frame;
341 inner.notices.push(Notice { kind, frame, text: text.into() });
342 let overflow = inner.notices.len().saturating_sub(MAX_NOTICES);
343 if overflow > 0 {
344 inner.notices.drain(0..overflow);
345 inner.notices_replied = inner.notices_replied.saturating_sub(overflow);
346 }
347 }
348
349 pub fn take_notices(&self) -> Vec<Notice> {
353 let mut inner = self.inner.lock().unwrap();
354 inner.notices_replied = 0;
355 std::mem::take(&mut inner.notices)
356 }
357
358 pub fn pointer(&self) -> Pointer {
359 self.inner.lock().unwrap().pointer.clone()
360 }
361
362 pub fn frame(&self) -> u64 {
363 self.inner.lock().unwrap().frame
364 }
365
366 pub fn has_pending(&self) -> bool {
367 let inner = self.inner.lock().unwrap();
368 !inner.pending.is_empty() || !inner.awaiting.is_empty() || !inner.polling.is_empty()
369 }
370
371 pub fn is_polling(&self, id: u64) -> bool {
375 self.inner.lock().unwrap().polling.iter().any(|p| p.pending.envelope.id == id)
376 }
377
378 pub fn poison(&self, why: impl Into<String>) {
381 let why = why.into();
382 let mut inner = self.inner.lock().unwrap();
383 inner.poisoned = Some(why.clone());
384 let frame = inner.frame;
385 for p in inner.pending.drain(..) {
386 let _ = p.reply.send(Reply::err(p.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
387 }
388 for a in inner.awaiting.drain(..) {
389 let _ = a.pending.reply.send(Reply::err(a.pending.envelope.id, frame, format!("session poisoned: {why}"), vec![]));
390 }
391 }
392
393 pub fn is_poisoned(&self) -> bool {
394 self.inner.lock().unwrap().poisoned.is_some()
395 }
396
397 pub fn drain_input(&self, raw: &mut egui::RawInput, frame: u64, ppp: f32, view: Option<egui::Rect>, ctx: &egui::Context) {
404 let mut inner = self.inner.lock().unwrap();
405 inner.frame = frame;
406 inner.pointer.now = raw.time.unwrap_or_else(|| ctx.input(|i| i.time) + f64::from(raw.predicted_dt));
407
408 if !inner.awaiting.is_empty() {
410 let mut done = Vec::new();
411 for event in &raw.events {
412 if let egui::Event::Screenshot { user_data, image, .. } = event {
413 let token = user_data.data.as_ref().and_then(|d| d.downcast_ref::<u64>()).copied();
414 if let Some(token) = token {
415 if let Some(pos) = inner.awaiting.iter().position(|a| a.token == token) {
416 let a = inner.awaiting.remove(pos);
417 done.push((a, image.clone()));
418 }
419 }
420 }
421 }
422 for (a, image) in done {
423 let notices = inner.reply_notices();
424 let shown = inner.view.clone();
425 let switched = match (&a.document, &shown) {
429 (Some(asked), Some((drawn, _))) => asked != drawn,
430 _ => false,
431 };
432 let result = if switched {
433 Err("document_not_active: the visible document changed between the request and the capture".to_string())
434 } else {
435 crate::automation::cmd_capture::encode_capture(&image, ppp, view, &a.region).map(|(mut json, png)| {
436 if let Some((handle, revision)) = &shown {
437 json["document"] = serde_json::json!({ "documentId": handle, "revision": revision.to_string() });
438 }
439 (json, png)
440 })
441 };
442 let reply = match result {
443 Ok((json, png)) => {
444 let mut r = Reply::ok(a.pending.envelope.id, frame, json, notices);
445 r.blob = Some(png);
446 r
447 }
448 Err(e) => Reply::err(a.pending.envelope.id, frame, e, notices),
449 };
450 let _ = a.pending.reply.send(reply);
451 }
452 }
453
454 let mut keep = VecDeque::new();
456 let mut events = Vec::new();
457 while let Some(p) = inner.pending.pop_front() {
458 if p.expired(frame) { continue; }
459 let Some(spec) = lookup(&p.envelope.cmd) else { continue };
460 match (&spec.handler, spec.phase) {
461 (Handler::Input(f), Phase::Input) => {
462 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
463 let notices = inner.reply_notices();
464 let reply = match result {
465 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
466 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
467 };
468 let _ = p.reply.send(reply);
469 }
470 _ => keep.push_back(p),
471 }
472 }
473 inner.pending = keep;
474 if std::mem::take(&mut inner.pointer.reset_clicks) {
478 ctx.input_mut(|i| i.pointer = egui::PointerState::default());
479 }
480 raw.modifiers = inner.pointer.modifiers.egui();
481 inner.input_this_pass = !events.is_empty();
482 raw.events.extend(events);
483 }
484
485 pub fn drain_app(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
488 let batch: Vec<Pending> = {
489 let mut inner = self.inner.lock().unwrap();
490 inner.frame = frame;
491 let mut keep = VecDeque::new();
492 let mut batch = Vec::new();
493 while let Some(p) = inner.pending.pop_front() {
494 match lookup(&p.envelope.cmd) {
495 Some(spec) if spec.phase == phase => batch.push(p),
496 _ => keep.push_back(p),
497 }
498 }
499 inner.pending = keep;
500 batch
501 };
502 for p in batch {
503 self.dispatch(p, phase, ctx, frame);
504 }
505 if self.poll(phase, ctx, frame) {
506 ctx.egui.request_repaint();
507 }
508 }
509
510 pub fn drain_frameless(&self, ctx: &mut Ctx<'_>) -> bool {
518 let frame = self.frame();
519 loop {
520 let next = {
521 let mut inner = self.inner.lock().unwrap();
522 let ready = inner.next_dispatchable();
523 match ready.and_then(|i| lookup(&inner.pending[i].envelope.cmd).map(|spec| (i, spec))) {
524 Some((index, spec)) => {
525 let p = inner.pending.remove(index).expect("index from next_dispatchable");
526 if let Handler::Input(f) = &spec.handler {
527 if !p.expired(frame) {
530 let mut events = Vec::new();
531 let result = f(&mut inner.pointer, p.envelope.args.clone(), &mut events);
532 debug_assert!(events.is_empty(), "{} is read-only but emitted input", spec.name);
533 let notices = inner.reply_notices();
534 let reply = match result {
535 Ok(v) => Reply::ok(p.envelope.id, frame, v, notices),
536 Err(e) => Reply::err(p.envelope.id, frame, e, notices),
537 };
538 let _ = p.reply.send(reply);
539 }
540 continue;
541 }
542 Some((p, spec.phase))
543 }
544 _ => None,
545 }
546 };
547 let Some((p, phase)) = next else { break };
548 self.dispatch(p, phase, ctx, frame);
549 }
550 let mutate = self.poll(Phase::Mutate, ctx, frame);
551 let read = self.poll(Phase::Read, ctx, frame);
552 mutate || read
553 }
554
555 fn dispatch(&self, p: Pending, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) {
557 if p.expired(frame) { return; }
558 let spec = lookup(&p.envelope.cmd).expect("checked at submit");
559 let Handler::App(f) = &spec.handler else { return };
560 if !spec.annotations.read_only {
561 self.mark_layout_dirty();
564 }
565 let (args, bound) = match bind_context(&ctx.app.docs, phase, &p.envelope.cmd, p.envelope.args.clone()) {
569 Ok(bound) => bound,
570 Err(e) => {
571 let mut inner = self.inner.lock().unwrap();
572 let notices = inner.reply_notices();
573 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
574 return;
575 }
576 };
577 let before = refused_counts(&ctx.app.docs);
578 let route = bound.as_ref().and_then(|b| b.route).map(|index| ctx.app.docs.route_begin(index));
579 let result = f(ctx, args);
580 if let Some(visible) = route {
581 ctx.app.docs.route_end(visible);
582 }
583 let result = refused_by_lock(&ctx.app.docs, &before, result);
584 let handle = bound.map(|b| b.handle);
585 let result = match &handle {
586 Some(handle) => tag_context(&ctx.app.docs, handle, result),
587 None => result,
588 };
589 let mut inner = self.inner.lock().unwrap();
590 answer(&mut inner, p, phase, frame, result, handle);
591 }
592
593 fn poll(&self, phase: Phase, ctx: &mut Ctx<'_>, frame: u64) -> bool {
596 let polling: Vec<Polling> = {
597 let mut inner = self.inner.lock().unwrap();
598 let (mine, rest) = std::mem::take(&mut inner.polling).into_iter().partition(|p| p.phase == phase);
599 inner.polling = rest;
600 mine
601 };
602 let mut still = false;
603 for mut entry in polling {
604 match (entry.poller)(ctx) {
605 None => {
606 still = true;
607 self.inner.lock().unwrap().polling.push(entry);
608 }
609 Some(result) => {
610 self.mark_layout_dirty();
612 let result = match &entry.bound {
613 Some(handle) => tag_context(&ctx.app.docs, handle, result),
614 None => result,
615 };
616 let mut inner = self.inner.lock().unwrap();
617 answer(&mut inner, entry.pending, phase, frame, result, entry.bound);
618 still |= !inner.polling.is_empty();
619 }
620 }
621 }
622 still
623 }
624
625 pub fn publish_view(&self, handle: &str, revision: u64) {
628 let mut inner = self.inner.lock().unwrap();
629 if inner.view.as_ref().is_none_or(|(h, r)| h != handle || *r != revision) {
630 inner.view = Some((handle.to_string(), revision));
631 }
632 }
633
634 pub fn fail_captures(&self, error: &str) {
637 let mut inner = self.inner.lock().unwrap();
638 let frame = inner.frame;
639 for a in std::mem::take(&mut inner.awaiting) {
640 let notices = inner.reply_notices();
641 let _ = a.pending.reply.send(Reply::err(a.pending.envelope.id, frame, error.to_string(), notices));
642 }
643 }
644
645 pub fn next_token(&self) -> u64 {
647 let mut inner = self.inner.lock().unwrap();
648 inner.next_token += 1;
649 inner.next_token
650 }
651}
652
653#[derive(serde::Deserialize)]
658#[serde(deny_unknown_fields, rename_all = "camelCase")]
659struct DocumentContext {
660 document_id: String,
661 expected_revision: String,
662}
663
664pub const ENGINE_ONLY: &[&str] = &[
670 "feature_stage",
671 "feature_stage_result",
672 "run_cancel",
673 "history_group_begin",
674 "history_group_end",
675 "feature_add_many",
676 "doc_import",
677 "doc_export",
678 "model_inspect",
679 "mass_properties",
680 "history_listing",
681 "frame_info",
682 "metadata_set",
683 "metadata_set_many",
684 "metadata_remove",
685];
686
687#[derive(Debug)]
690struct Bound {
691 handle: String,
692 route: Option<usize>,
693}
694
695fn bind_context(
706 docs: &crate::document::Documents,
707 phase: Phase,
708 cmd: &str,
709 mut args: serde_json::Value,
710) -> Result<(serde_json::Value, Option<Bound>), String> {
711 let Some(raw) = args.as_object_mut().and_then(|o| o.remove("context")) else {
712 return Ok((args, None));
713 };
714 let context: DocumentContext = serde_json::from_value(raw)
716 .map_err(|e| format!("invalid_arguments: context must be {{documentId, expectedRevision}}: {e}"))?;
717 let expected = parse_revision(&context.expected_revision)?;
718 match crate::handles::parse("doc", &context.document_id) {
719 crate::handles::Parsed::Current => {}
720 crate::handles::Parsed::OtherEpoch => {
721 return Err("handle_epoch_mismatch: the document handle is from another run of the app".into())
722 }
723 crate::handles::Parsed::Malformed => return Err("invalid_arguments: documentId is not a document handle".into()),
724 }
725 let Some(index) = docs.index_of_handle(&context.document_id) else {
726 return Err("document_unknown: no open document has that handle (closed, or never opened)".into());
727 };
728 if docs.work_part_in_place() {
729 return Err("document_busy: a Work part is edited in place; a document command would write the part".into());
730 }
731 let route = if index == docs.active_index() {
732 None
733 } else if ENGINE_ONLY.contains(&cmd) {
734 Some(index)
735 } else {
736 return Err("document_not_active: this command acts on the visible document; activate the named one first".into());
737 };
738 let current = docs.get(index).map_or(0, |d| d.committed_revision());
739 if phase == Phase::Mutate && current != expected {
740 return Err(format!(
741 "revision_conflict: the document is at revision {current}, not {expected}; read it again before writing"
742 ));
743 }
744 Ok((args, Some(Bound { handle: context.document_id, route })))
745}
746
747fn parse_revision(text: &str) -> Result<u64, String> {
749 let canonical = text == "0" || (!text.is_empty() && !text.starts_with('0') && text.bytes().all(|b| b.is_ascii_digit()));
750 text.parse::<u64>()
751 .ok()
752 .filter(|_| canonical)
753 .ok_or_else(|| "invalid_arguments: expectedRevision must be a canonical unsigned decimal string".into())
754}
755
756fn tag_context(
762 docs: &crate::document::Documents,
763 handle: &str,
764 result: Result<Outcome, String>,
765) -> Result<Outcome, String> {
766 let context = docs.index_of_handle(handle).and_then(|i| docs.get(i)).map(|doc| {
767 serde_json::json!({ "documentId": handle, "expectedRevision": doc.committed_revision().to_string() })
768 });
769 let tag = |v: &mut serde_json::Value| {
770 if let (Some(object), Some(context)) = (v.as_object_mut(), context.clone()) {
771 object.insert("context".into(), context);
772 }
773 };
774 match result {
775 Ok(Outcome::Done(mut v)) => {
776 tag(&mut v);
777 Ok(Outcome::Done(v))
778 }
779 Ok(Outcome::Blob { mut json, bytes }) => {
780 tag(&mut json);
781 Ok(Outcome::Blob { json, bytes })
782 }
783 other => other,
784 }
785}
786
787fn answer(inner: &mut Inner, p: Pending, phase: Phase, frame: u64, result: Result<Outcome, String>, bound: Option<String>) {
789 match result {
790 Ok(Outcome::Done(v)) => {
791 let notices = inner.reply_notices();
792 let _ = p.reply.send(Reply::ok(p.envelope.id, frame, v, notices));
793 }
794 Ok(Outcome::Blob { json, bytes }) => {
795 let notices = inner.reply_notices();
796 let mut r = Reply::ok(p.envelope.id, frame, json, notices);
797 r.blob = Some(bytes);
798 let _ = p.reply.send(r);
799 }
800 Ok(Outcome::AwaitScreenshot { token, region, document }) => {
803 inner.awaiting.push(Awaiting { token, region, document, pending: p });
804 }
805 Ok(Outcome::Poll(poller)) => inner.polling.push(Polling { pending: p, phase, poller, bound }),
806 Err(e) => {
807 let notices = inner.reply_notices();
808 let _ = p.reply.send(Reply::err(p.envelope.id, frame, e, notices));
809 }
810 }
811}
812
813fn refused_counts(docs: &crate::document::Documents) -> Vec<(u64, u64)> {
815 docs.iter().map(|d| (d.id(), d.engine.history.refused_user_edits())).collect()
816}
817
818fn refused_by_lock(
824 docs: &crate::document::Documents,
825 before: &[(u64, u64)],
826 result: Result<Outcome, String>,
827) -> Result<Outcome, String> {
828 if result.is_err() {
829 return result;
830 }
831 for doc in docs.iter() {
832 let was = before.iter().find(|(id, _)| *id == doc.id()).map(|(_, n)| *n);
833 if was.is_some_and(|was| doc.engine.history.refused_user_edits() > was) {
834 if let crate::document::Access::ReadOnly { reason } = doc.access() {
835 return Err(format!("{} is read-only: {reason}", doc.title()));
836 }
837 }
838 }
839 result
840}
841