Scripts: a canceled request stops a sleeping or waiting run at its next slice

script_pump read vectorcraft_doc::cancel only after the guard that keeps
a run waiting, so a run asleep in $.sleep went on waiting until its wake
time (up to 1,000 ms), and a run waiting at a dialog or for approval to
grow waited until someone answered. The check now runs before that
guard, after the dialog-failure check, so a canceled request ends the
run at the next slice and every document goes back.
This commit is contained in:
dotCooCoo
2026-11-11 20:01:25 -08:00
parent 6b8be09699
commit 8bd437687a
6 changed files with 81 additions and 4 deletions
+4 -3
View File
@@ -1787,6 +1787,10 @@ impl Session {
if let Some(e) = job.host.dialogs.take_failure() {
return Some(self.script_failed(job, &e));
}
// A canceled request ends a sleep or a wait at this pump.
if vectorcraft_doc::cancel::check().is_err() {
self.script_stop.set();
}
if (job.host.waiting.get() || job.host.asleep().is_some() || job.host.dialogs.is_waiting()) && !self.script_stop.is_set() {
JOBS.with(|jobs| jobs.borrow_mut().insert(key.id, job));
self.script_key = Some(key);
@@ -1798,9 +1802,6 @@ impl Session {
let e = RunError { kind: RunErrorKind::Limit(kind), message: format!("limit reached: {kind:?}"), line: 0, column: 0 };
return Some(self.script_failed(job, &e));
}
if vectorcraft_doc::cancel::check().is_err() {
self.script_stop.set();
}
job.host.calls_this_slice.set(0);
let outcome = run_slice(self, &mut job);
match outcome {
+20
View File
@@ -217,6 +217,26 @@ fn dialog_loop_stops_or_exhausts() {
assert_eq!(art(&s), before);
}
#[test]
fn a_run_waiting_at_a_dialog_stops_at_the_next_slice_once_its_work_is_canceled() {
let mut s = one_doc();
let before = art(&s);
let token = crate::cancel::CancelToken::new();
s.script_frames = true;
let source = "vc.run('shape.rectangle', {x: 20, y: 0, width: 5, height: 5}); alert('x'); vc.log('answered');";
assert_eq!(s.execute("script.run", &json!({"source": source, "name": "d.js"})).unwrap()["running"], true);
assert!(crate::cancel::scope(&token, None, || s.script_pump()).is_none());
assert!(s.script_dialog().is_some());
token.cancel();
let ended = crate::cancel::scope(&token, None, || s.script_pump());
assert!(ended.expect("the run goes on waiting for an answer").is_err());
let report = s.script_report().unwrap();
assert_eq!((report["kind"].as_str(), &report["log"]), (Some("stopped"), &json!([])), "{report}");
assert_eq!(report["dialogs"][0]["answer"], "stop");
assert_eq!(s.script_dialog(), None);
assert_eq!(art(&s), before);
}
#[test]
fn dialog_text_limits_and_controls() {
let mut s = one_doc();
+31
View File
@@ -5,6 +5,7 @@ use std::time::Duration;
use serde_json::{Value, json};
use crate::cancel::{self, CancelToken};
use crate::script::{TEST_ELAPSED, TEST_NO_CLOCK};
use crate::*;
@@ -204,6 +205,36 @@ fn a_sleeping_run_stops_when_asked() {
TEST_ELAPSED.set(None);
}
#[test]
fn a_sleeping_run_stops_at_the_next_slice_once_its_work_is_canceled() {
for (asleep_until, when) in
[(None, "before its first slice"), (Some(Duration::ZERO), "while it sleeps"), (Some(Duration::from_secs(1)), "at its wake time")]
{
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
let before = (format!("{:?}", s.documents()[0].doc.layers), s.documents()[0].history.undo.len());
s.script_frames = true;
TEST_ELAPSED.set(Some(Duration::ZERO));
let source = "vc.run('shape.rectangle', {x: 0, y: 0, width: 5, height: 5}); $.sleep(1000); vc.log('woke'); \
vc.run('shape.rectangle', {x: 9, y: 0, width: 5, height: 5});";
s.execute("script.run", &json!({"source": source, "name": "t.js"})).unwrap();
let token = CancelToken::new();
if let Some(at) = asleep_until {
assert!(cancel::scope(&token, None, || s.script_pump()).is_none(), "{when}");
assert_eq!(s.script_sleep_left(), Some(Duration::from_secs(1)), "{when}");
TEST_ELAPSED.set(Some(at));
}
token.cancel();
let ended = cancel::scope(&token, None, || s.script_pump());
TEST_ELAPSED.set(None);
assert!(ended.unwrap_or_else(|| panic!("{when}: the run goes on waiting")).is_err(), "{when}");
let report = s.script_report().unwrap();
assert_eq!((report["kind"].as_str(), &report["log"]), (Some("stopped"), &json!([])), "{when}: {report}");
assert!(!s.script_running(), "{when}");
assert_eq!((format!("{:?}", s.documents()[0].doc.layers), s.documents()[0].history.undo.len()), before, "{when}");
}
}
#[test]
fn sleep_takes_milliseconds_from_0_to_1000() {
let mut s = Session::new();
+24
View File
@@ -845,6 +845,30 @@ fn a_person_can_let_a_growing_run_go_on_or_stop_it() {
assert_eq!(state(&s), before);
}
#[test]
fn a_run_waiting_to_grow_stops_at_the_next_slice_once_its_work_is_canceled() {
TEST_GROWTH.with(|g| g.set(Some(20)));
let mut s = two_docs();
let before = state(&s);
s.script_frames = true;
run(&mut s, "for (var i = 0; i < 25; i++) vc.run('shape.rectangle', {x:i,y:1,width:1,height:1});").unwrap();
let token = crate::cancel::CancelToken::new();
for _ in 0..1_000 {
if s.script_waiting() {
break;
}
assert!(crate::cancel::scope(&token, None, || s.script_pump()).is_none());
}
assert!(s.script_waiting());
token.cancel();
let ended = crate::cancel::scope(&token, None, || s.script_pump());
TEST_GROWTH.with(|g| g.set(None));
assert!(ended.expect("the run goes on waiting for Continue").is_err());
assert_eq!(s.script_report().unwrap()["kind"], "stopped");
assert!(!s.script_running());
assert_eq!(state(&s), before);
}
#[test]
fn a_file_saved_before_a_failure_stays_written_and_is_named() {
let path = std::env::temp_dir().join(format!("vc-script-save-{}.svg", std::process::id()));
+1 -1
View File
@@ -1742,7 +1742,7 @@ VectorCraft's own, and `vc.api.has("dollar")` is true where they are available:
| `localize` | `false`; while `true`, `String(object)` of a localization object gives its text for `$.locale` |
| `os` | `Windows`, `macOS`, `Linux`, `FreeBSD`, `Unix` or `Web`, without a version |
| `screens` | an empty array |
| `sleep(ms)` | waits from 0 to 1,000 milliseconds between two frames of the app, without stopping it from drawing; `script.stop` ends the wait; it does not wait on the web; called with more than 0 inside a getter, a conversion, a callback of a built-in function or a file `$.evalFile` runs, it throws an `Error` instead of waiting |
| `sleep(ms)` | waits from 0 to 1,000 milliseconds between two frames of the app, without stopping it from drawing; `script.stop` ends the wait, and so does `notifications/cancelled` for a run its `tools/call` waits for, without waiting for the time to pass; it does not wait on the web; called with more than 0 inside a getter, a conversion, a callback of a built-in function or a file `$.evalFile` runs, it throws an `Error` instead of waiting |
| `strict` | `false` |
Members that are values (`build`, `level`, `os` and the like) cannot be changed. A wrong value for a member that can
+1
View File
@@ -740,6 +740,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Scripts: File › Scripts › Other Script… and `script.run` run a JavaScript file against the open documents as one transaction with one undo step for each changed document. A failed or stopped run puts every document back. `crates/engine/src/tests/script_run.rs`.
- Scripts: Window › Script Console shows each run's `$.write` and `$.writeln` output, `vc.log` entries, uncaught error with the script's name, line and column, and a report of its time, host calls and slowest commands, keeping at most 2,048 lines and counting the lines it dropped. `script.run`'s report carries the same data, and `script.console`, `script.console.clear` and `ui.copyScriptConsole` read, clear and copy it. `crates/engine/src/tests/script_console.rs`.
- Scripts: the `$` object has the other members the Dollar object page lists (https://extendscript.docsforadobe.dev/extendscript-tools-features/dollar-object/), from `$.about()` to `$.version`, and scripts have the global `localize(object, ...values)` (https://extendscript.docsforadobe.dev/extendscript-tools-features/localizing-extendscript-strings/), `toSource(value)` and `Object.prototype.toSource()`. Each run starts with the interface's locale, and what a script changes in `$` lasts only for that run. `crates/engine/src/tests/script_dollar.rs`, `crates/js/src/extension_tests.rs`.
- Scripts: a canceled request stops a run that sleeps in `$.sleep`, waits at a dialog or waits for approval to grow at the run's next slice, before its wake time or an answer, and every document goes back to its state before the run. VectorCraft's own rule. `crates/engine/src/tests/script_dollar.rs`, `crates/engine/src/tests/script_dialogs.rs`, `crates/engine/src/tests/script_run.rs`.
- Scripts: the Script Console panel's report line reads as `script.console` gives it, `time unavailable` included on a platform without a clock; counts of host calls, dropped lines and cut lines take the singular for one, in each language's own plural forms; and a run that is no longer available still gets its end line (`crates/engine/src/tests/script_console.rs`, `crates/ui-egui/src/panels/tests_script_console.rs`).
- Scripts: an error `$.evalFile` throws for a file it cannot run is reported at that file's line and column only while no `catch` has taken it. Caught and thrown again later, in the script or in another file, it is reported where it was thrown again, and a new error whose message holds its text is reported where that error was thrown. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`.
- Scripts: an uncaught error that one or more `finally` blocks pass on unchanged, in the script or in a function it called, is reported at the line and column where it was thrown, in `script.run`'s report and the Script Console. An error a `finally` block throws is reported at that throw, and one its `return`, `break` or `continue` discards is not reported. VectorCraft's own rule. `crates/engine/src/tests/script_include.rs`, `crates/js/src/host_tests.rs`.