mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 22:02:33 +00:00
feat(js): timers keep and fire in the document's script realm (#169)
Ported from #4751 by @pyatsysh onto #5699's per-document realm. `app.setInterval` and `app.setTimeOut` (an expression or a function) are kept by the retained realm and reported in every Outcome (`timers`); the caller owns the clock and fires one by running a Job with `timer` set, in the realm that set it. `clearInterval`/`clearTimeOut` forget one. Bounds kept from #4751: at most 64 timers per document, periods of at least 10 ms. A one-off run keeps none (the functions return an inert object, as before), and changed document scripts start a realm without the old timers. Co-authored-by: Peter Yatsyshin <p.yatsyshin@googlemail.com>
This commit is contained in:
1 parent
4efcfa6ea3
commit
f56a146b22
7 files changed
+261
-13
No files matched your search
@@ -660,6 +660,7 @@ impl Scripts for JsRunner {
|
||||
fields: states,
|
||||
doc_scripts: self.doc_scripts.clone(),
|
||||
calculations,
|
||||
timer: None,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let remaining = self.callback_time_left();
|
||||
@@ -748,6 +749,7 @@ impl Scripts for JsRunner {
|
||||
fields: states,
|
||||
doc_scripts: self.doc_scripts.clone(),
|
||||
calculations: calculations.clone(),
|
||||
timer: None,
|
||||
};
|
||||
let remaining = self.callback_time_left();
|
||||
let outcome = if remaining.is_zero() {
|
||||
@@ -1028,6 +1030,7 @@ impl Session {
|
||||
fields: states.clone(),
|
||||
doc_scripts: runner.doc_scripts,
|
||||
calculations,
|
||||
timer: None,
|
||||
},
|
||||
answers,
|
||||
);
|
||||
@@ -1495,6 +1498,7 @@ impl Session {
|
||||
fields: states.clone(),
|
||||
doc_scripts: runner.doc_scripts,
|
||||
calculations: calculation_scripts(cos, &fields),
|
||||
timer: None,
|
||||
};
|
||||
let receiver =
|
||||
doc.js_runtime.lock().map_err(|_| EditError::Invalid("script runtime unavailable".into()))?.start(job).map_err(EditError::Invalid)?;
|
||||
|
||||
@@ -416,11 +416,6 @@ fn js_number(v: f64) -> String {
|
||||
if v.fract() == 0.0 && v.abs() < 1e15 { format!("{}", v as i64) } else { format!("{v}") }
|
||||
}
|
||||
|
||||
fn timer(_: &JsValue, _: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
// Timers never fire in the sandbox; scripts get an object they can pass to clear*.
|
||||
Ok(ObjectInitializer::new(ctx).build().into())
|
||||
}
|
||||
|
||||
/// The `color` object: each named color (`transparent`, `black`, `red`, `cyan`, `gray`, …) as
|
||||
/// an array of its color space (`T`, `G`, `RGB` or `CMYK`) followed by its components.
|
||||
fn color_object(ctx: &mut Context) -> JsObject {
|
||||
@@ -556,10 +551,10 @@ pub(crate) fn install(ctx: &mut Context) -> JsResult<()> {
|
||||
.function(NativeFunction::from_fn_ptr(launch_url), js_string!("launchURL"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(crate::interaction::popup_menu), js_string!("popUpMenuEx"), 0)
|
||||
.function(NativeFunction::from_fn_ptr(exec_menu_item), js_string!("execMenuItem"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(timer), js_string!("setTimeOut"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(timer), js_string!("setInterval"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("clearTimeOut"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(noop), js_string!("clearInterval"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(crate::timers::set_time_out), js_string!("setTimeOut"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(crate::timers::set_interval), js_string!("setInterval"), 2)
|
||||
.function(NativeFunction::from_fn_ptr(crate::timers::clear), js_string!("clearTimeOut"), 1)
|
||||
.function(NativeFunction::from_fn_ptr(crate::timers::clear), js_string!("clearInterval"), 1)
|
||||
.property(js_string!("viewerType"), s("Exchange-Pro"), Attribute::READONLY)
|
||||
.property(js_string!("viewerVariation"), s("Full"), Attribute::READONLY)
|
||||
.property(js_string!("viewerVersion"), JsValue::from(24.0), Attribute::READONLY)
|
||||
|
||||
@@ -73,6 +73,8 @@ mod geometry;
|
||||
pub use geometry::{rotate_rect, unrotate_rect};
|
||||
|
||||
mod icons;
|
||||
mod timers;
|
||||
pub use timers::{MAX_TIMERS, MIN_TIMER_MS, Timer};
|
||||
|
||||
pub mod interaction;
|
||||
pub mod session;
|
||||
|
||||
@@ -382,6 +382,9 @@ pub struct Outcome {
|
||||
pub doc_errors: Vec<String>,
|
||||
/// The script's completion value (what the console prints), unless undefined.
|
||||
pub result: Option<String>,
|
||||
/// The timers of a document's retained realm as they stand after the run, in the order they
|
||||
/// were set (`app.setInterval`, `app.setTimeOut`); none from a one-off run.
|
||||
pub timers: Vec<crate::Timer>,
|
||||
}
|
||||
|
||||
/// Bounds that keep a script from hanging the viewer.
|
||||
|
||||
+11
-2
@@ -1,7 +1,9 @@
|
||||
use super::globals::{event_object, install};
|
||||
use super::host::{Host, Shared, host, text};
|
||||
use super::model::{DocInfo, Event, FieldState, Limits, Outcome};
|
||||
use super::{Arc, BTreeSet, Context, HostHooks, JsNativeError, JsResult, JsString, JsValue, NativeFunction, Realm, RefCell, Source, js_string};
|
||||
use super::{
|
||||
Arc, BTreeSet, Context, HostHooks, JsNativeError, JsObject, JsResult, JsString, JsValue, NativeFunction, Realm, RefCell, Source, js_string,
|
||||
};
|
||||
|
||||
/// Longest script, in bytes.
|
||||
pub(crate) const MAX_SCRIPT_BYTES: usize = 256 * 1024;
|
||||
@@ -148,6 +150,7 @@ pub(crate) fn run_shared_here(
|
||||
reset_calls: 0,
|
||||
calculation_evaluations: 0,
|
||||
calculation_limit_exceeded: false,
|
||||
call: None,
|
||||
},
|
||||
limits,
|
||||
)
|
||||
@@ -200,6 +203,8 @@ pub(crate) struct ScriptSetup<'a> {
|
||||
pub(crate) reset_calls: usize,
|
||||
pub(crate) calculation_evaluations: usize,
|
||||
pub(crate) calculation_limit_exceeded: bool,
|
||||
/// A timer's function to call in place of the script (#169).
|
||||
pub(crate) call: Option<JsObject>,
|
||||
}
|
||||
|
||||
/// Create a restricted temporary Boa context and convert initialization failures into script outcomes.
|
||||
@@ -296,7 +301,10 @@ pub(crate) fn run_in_context(
|
||||
doc_errors.push((i, doc_error(i, &e)));
|
||||
}
|
||||
}
|
||||
let r = ctx.eval(Source::from_bytes(script))?;
|
||||
let r = match &setup.call {
|
||||
Some(f) => f.call(&JsValue::undefined(), &[], ctx)?,
|
||||
None => ctx.eval(Source::from_bytes(script))?,
|
||||
};
|
||||
if !r.is_undefined() {
|
||||
out.result = Some(text(&r, ctx)?);
|
||||
}
|
||||
@@ -332,6 +340,7 @@ pub(crate) fn run_in_context(
|
||||
}
|
||||
ctx.insert_data(RefCell::new(h));
|
||||
}
|
||||
out.timers = crate::timers::report(ctx);
|
||||
out
|
||||
}
|
||||
|
||||
|
||||
+105
-2
@@ -16,6 +16,10 @@ pub struct Job {
|
||||
pub doc_scripts: Vec<String>,
|
||||
/// Target positions in this job's field snapshot and their ordered script sources.
|
||||
pub calculations: Vec<(usize, String)>,
|
||||
/// Fire this timer of the realm (`app.setInterval`, `app.setTimeOut`) in place of `script`
|
||||
/// (#169). A timer the realm no longer has runs nothing; nor does any timer of a realm that
|
||||
/// has not started, or whose document scripts changed (its timers went with it).
|
||||
pub timer: Option<u32>,
|
||||
}
|
||||
/// Live field instances are independent of restorable PDF object-number allocation.
|
||||
#[derive(Default)]
|
||||
@@ -129,6 +133,9 @@ impl Realm {
|
||||
}
|
||||
fn run_job(&mut self, job: Job) -> Outcome {
|
||||
let failed = |why: String| Outcome { rc: true, execution_refused: true, realm_untouched: true, error: Some(why), ..Default::default() };
|
||||
if let Some(id) = job.timer {
|
||||
return self.fire(job, id);
|
||||
}
|
||||
// A script too long or too deeply nested to run is that script's error, as on `main`: the
|
||||
// value stays and the edit's other scripts still run. The realm is not touched.
|
||||
if let Some(why) = crate::refuse(&job.script) {
|
||||
@@ -147,7 +154,10 @@ impl Realm {
|
||||
let initialize = self.context.is_none() || self.scripts != job.doc_scripts;
|
||||
if initialize {
|
||||
self.context = match crate::Context::builder().host_hooks(std::rc::Rc::new(crate::run::NoCodeFromStrings)).build() {
|
||||
Ok(c) => Some(c),
|
||||
Ok(mut c) => {
|
||||
crate::timers::enable(&mut c);
|
||||
Some(c)
|
||||
}
|
||||
Err(e) => return failed(e.to_string()),
|
||||
};
|
||||
self.scripts = job.doc_scripts.clone();
|
||||
@@ -186,6 +196,7 @@ impl Realm {
|
||||
reset_calls: 0,
|
||||
calculation_evaluations: 0,
|
||||
calculation_limit_exceeded: false,
|
||||
call: None,
|
||||
},
|
||||
Limits::default(),
|
||||
);
|
||||
@@ -212,6 +223,7 @@ impl Realm {
|
||||
reset_calls,
|
||||
calculation_evaluations,
|
||||
calculation_limit_exceeded: initial.calculation_limit_exceeded,
|
||||
call: None,
|
||||
},
|
||||
Limits::default(),
|
||||
);
|
||||
@@ -251,6 +263,7 @@ impl Realm {
|
||||
reset_calls: 0,
|
||||
calculation_evaluations: 0,
|
||||
calculation_limit_exceeded: false,
|
||||
call: None,
|
||||
},
|
||||
Limits::default(),
|
||||
)
|
||||
@@ -258,6 +271,43 @@ impl Realm {
|
||||
}
|
||||
}
|
||||
|
||||
impl Realm {
|
||||
/// Run timer `id` that fell due, with the job's form and document: its expression, or the
|
||||
/// function it was given, in the realm that set it (#169).
|
||||
fn fire(&mut self, job: Job, id: u32) -> Outcome {
|
||||
let idle = |timers: Vec<crate::Timer>| Outcome { rc: true, realm_untouched: true, timers, ..Default::default() };
|
||||
let Some(ctx) = self.context.as_mut().filter(|_| self.scripts == job.doc_scripts) else { return idle(Vec::new()) };
|
||||
let Some(code) = crate::timers::due(ctx, id) else { return idle(crate::timers::report(ctx)) };
|
||||
let (script, call) = match code {
|
||||
crate::timers::Code::Source(source) => {
|
||||
if let Some(why) = crate::refuse(&source) {
|
||||
return Outcome { rc: true, realm_untouched: true, error: Some(why), timers: crate::timers::report(ctx), ..Default::default() };
|
||||
}
|
||||
(source, None)
|
||||
}
|
||||
crate::timers::Code::Function(f) => (String::new(), Some(f)),
|
||||
};
|
||||
crate::run::run_in_context(
|
||||
ctx,
|
||||
&script,
|
||||
&job.event,
|
||||
&job.doc,
|
||||
job.fields,
|
||||
crate::run::ScriptSetup {
|
||||
doc_scripts: &[],
|
||||
skipped: &[],
|
||||
calculations: &job.calculations,
|
||||
initialize: false,
|
||||
reset_calls: 0,
|
||||
calculation_evaluations: 0,
|
||||
calculation_limit_exceeded: false,
|
||||
call,
|
||||
},
|
||||
Limits::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
static LIVE_WORKERS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -577,8 +627,57 @@ mod tests {
|
||||
fields: Arc::new(vec![FieldState::new("go", crate::FieldType::Button, vec!["old".into()])]),
|
||||
doc_scripts: vec!["var buffer='', init=1, opened=event.name;var held=getField('go');function digit(v){buffer+=v;return buffer;}".into()],
|
||||
calculations: Vec::new(),
|
||||
timer: None,
|
||||
}
|
||||
}
|
||||
/// A job firing timer `id` of the realm, as the viewer runs one that fell due (#169).
|
||||
fn fire(id: u32) -> Job {
|
||||
Job { script: String::new(), event: Event { kind: "App".into(), ..Event::doc("Timer") }, timer: Some(id), ..job("") }
|
||||
}
|
||||
#[test]
|
||||
/// Timers a script sets stay in the document's realm and fire there: an interval until it is
|
||||
/// cleared, a time-out once; the field writes a timer makes are reported like any script's.
|
||||
fn timers_fire_in_the_document_realm_until_cleared() {
|
||||
let mut s = ScriptSession::default();
|
||||
let set = s.run(
|
||||
job("var n = 0; var t = app.setInterval(function () { n++; getField('go').value = 'frame ' + n; }, 40); app.setTimeOut('n += 100', 0);"),
|
||||
&[],
|
||||
);
|
||||
assert_eq!(set.error, None);
|
||||
assert_eq!(
|
||||
set.timers,
|
||||
[crate::Timer { id: 1, period_ms: 40, repeat: true }, crate::Timer { id: 2, period_ms: crate::MIN_TIMER_MS, repeat: false }]
|
||||
);
|
||||
let first = s.run(fire(1), &[]);
|
||||
assert_eq!(first.error, None);
|
||||
assert_eq!(first.changed.iter().map(|f| f.value.clone()).collect::<Vec<_>>(), [vec!["frame 1".to_string()]]);
|
||||
let once = s.run(fire(2), &[]);
|
||||
assert_eq!(once.timers.len(), 1, "a time-out runs once");
|
||||
assert_eq!(s.run(fire(2), &[]).realm_untouched, true, "and is gone after");
|
||||
s.run(fire(1), &[]);
|
||||
assert_eq!(s.run(job("n;"), &[]).result.as_deref(), Some("102"));
|
||||
let cleared = s.run(job("app.clearInterval(t); app.clearInterval({}); n;"), &[]);
|
||||
assert!(cleared.timers.is_empty());
|
||||
assert!(s.run(fire(1), &[]).changed.is_empty(), "a cleared timer runs nothing");
|
||||
}
|
||||
#[test]
|
||||
/// A document keeps at most 64 timers, a timer runs at most every 10 ms, a one-off run keeps
|
||||
/// none, and changed document scripts start a realm without the old timers.
|
||||
fn timers_are_bounded_and_belong_to_one_realm() {
|
||||
let mut s = ScriptSession::default();
|
||||
let many = s.run(
|
||||
job("var made = 0; try { for (var i = 0; i < 70; i++) { app.setInterval('1', -5); made++; } } catch (e) { made + ': ' + e.message; }"),
|
||||
&[],
|
||||
);
|
||||
assert_eq!(many.result.as_deref(), Some("64: a document's scripts can keep at most 64 timers"));
|
||||
assert!(many.timers.iter().all(|t| t.period_ms == crate::MIN_TIMER_MS));
|
||||
let one_off = crate::run("app.setInterval('1', 50); 'set';", &Event::doc("Console"), &DocInfo::default(), &[], &[], Limits::default());
|
||||
assert_eq!(one_off.result.as_deref(), Some("set"));
|
||||
assert!(one_off.timers.is_empty());
|
||||
let changed = Job { doc_scripts: vec!["var other = 1;".into()], ..fire(1) };
|
||||
let gone = s.run(changed, &[]);
|
||||
assert!(gone.timers.is_empty() && gone.changed.is_empty(), "{gone:?}");
|
||||
}
|
||||
#[test]
|
||||
/// Retained Field identity, custom properties and keypad buffers persist locally while separate documents stay isolated.
|
||||
fn document_realm_keeps_keypad_state_and_isolates_documents() {
|
||||
@@ -980,6 +1079,7 @@ mod reset_budget_tests {
|
||||
fields: Arc::new(vec![]),
|
||||
doc_scripts: vec!["for(var i=0;i<64;i++)resetForm();".into()],
|
||||
calculations: vec![],
|
||||
timer: None,
|
||||
};
|
||||
let out = runtime.run(job.clone(), &[]);
|
||||
assert!(out.reset_limit_exceeded);
|
||||
@@ -1006,6 +1106,7 @@ mod event_identity_tests {
|
||||
fields: Arc::new(vec![field]),
|
||||
doc_scripts: vec![],
|
||||
calculations: vec![],
|
||||
timer: None,
|
||||
};
|
||||
let out = ScriptSession::default().run(job, &[]);
|
||||
assert_eq!(out.error, None);
|
||||
@@ -1027,7 +1128,7 @@ mod cache_capacity_tests {
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let job=Job{script:"for(var i=0;i<4100;i++)getField('f'+i);var held=getField('f4099');held.custom=42;held===getField('f4099') && getField('f4099').custom===42;".into(),event:Event::doc("Console"),doc:Default::default(),fields,doc_scripts:vec![],calculations:vec![]};
|
||||
let job=Job{script:"for(var i=0;i<4100;i++)getField('f'+i);var held=getField('f4099');held.custom=42;held===getField('f4099') && getField('f4099').custom===42;".into(),event:Event::doc("Console"),doc:Default::default(),fields,doc_scripts:vec![],calculations:vec![],timer:None};
|
||||
let mut session = ScriptSession::default();
|
||||
let out = session.run(job.clone(), &[]);
|
||||
assert_eq!(out.error, None);
|
||||
@@ -1051,6 +1152,7 @@ mod cache_lifecycle_tests {
|
||||
fields: Arc::new(vec![field.clone()]),
|
||||
doc_scripts: vec![],
|
||||
calculations: vec![],
|
||||
timer: None,
|
||||
};
|
||||
let mut realm = Realm::new();
|
||||
assert_eq!(realm.run(job.clone()).error, None);
|
||||
@@ -1088,6 +1190,7 @@ mod review13_tests {
|
||||
fields: Arc::new(fields),
|
||||
doc_scripts: vec![],
|
||||
calculations: vec![],
|
||||
timer: None,
|
||||
}
|
||||
}
|
||||
/// A retired accessor must not resolve a replacement even if no job observed the deletion.
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//! `app.setInterval`, `app.setTimeOut`, `app.clearInterval` and `app.clearTimeOut` in a
|
||||
//! document's retained realm (#169, ported from #4751). The realm keeps the timers its scripts
|
||||
//! set and reports them in every [`crate::Outcome`]; the caller owns the clock and fires one
|
||||
//! by running a [`crate::session::Job`] with its `timer` when it falls due, in the same realm. A
|
||||
//! one-off run (a fresh context) keeps none: there the functions return an object and nothing
|
||||
//! ever fires, as before.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::host::{arg, error, text};
|
||||
use crate::{Context, JsObject, JsResult, JsValue, MAX_SCRIPT_BYTES, ObjectInitializer};
|
||||
|
||||
/// Most timers one document's scripts keep at once; setting another is an error.
|
||||
pub const MAX_TIMERS: usize = 64;
|
||||
/// The shortest period, in milliseconds: a timer asked to run sooner or more often runs at this.
|
||||
pub const MIN_TIMER_MS: u32 = 10;
|
||||
|
||||
/// A timer a script set, as the realm reports it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Timer {
|
||||
/// The realm's name for it, never reused while the realm lives.
|
||||
pub id: u32,
|
||||
/// Milliseconds between runs (`setInterval`) or before the one run (`setTimeOut`), at least
|
||||
/// [`MIN_TIMER_MS`].
|
||||
pub period_ms: u32,
|
||||
/// `setInterval`: it runs until it is cleared. `setTimeOut`: once.
|
||||
pub repeat: bool,
|
||||
}
|
||||
|
||||
/// What a timer runs: its expression, or a function the script gave instead.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Code {
|
||||
Source(String),
|
||||
Function(JsObject),
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
/// The object `set*` returned, which `clear*` is given back.
|
||||
handle: JsObject,
|
||||
code: Code,
|
||||
period_ms: u32,
|
||||
repeat: bool,
|
||||
}
|
||||
|
||||
/// A realm's timers by id, in the order they were set (ids only grow).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Timers {
|
||||
last: u32,
|
||||
table: BTreeMap<u32, Entry>,
|
||||
}
|
||||
|
||||
type Shared = RefCell<Timers>;
|
||||
|
||||
/// Let the timers of a retained realm run: the functions keep what they are given from now on.
|
||||
pub(crate) fn enable(ctx: &mut Context) {
|
||||
if ctx.get_data::<Shared>().is_none() {
|
||||
ctx.insert_data(RefCell::new(Timers::default()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The realm's timers as they stand, in the order they were set; none in a one-off run.
|
||||
pub(crate) fn report(ctx: &Context) -> Vec<Timer> {
|
||||
ctx.get_data::<Shared>()
|
||||
.map(|t| t.borrow().table.iter().map(|(id, e)| Timer { id: *id, period_ms: e.period_ms, repeat: e.repeat }).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// What timer `id` runs now that it fell due; a `setTimeOut` timer is gone after this. `None`
|
||||
/// when it was cleared (or the realm has no timers).
|
||||
pub(crate) fn due(ctx: &Context, id: u32) -> Option<Code> {
|
||||
let mut timers = ctx.get_data::<Shared>()?.borrow_mut();
|
||||
let entry = timers.table.get(&id)?;
|
||||
let code = entry.code.clone();
|
||||
if !entry.repeat {
|
||||
timers.table.remove(&id);
|
||||
}
|
||||
Some(code)
|
||||
}
|
||||
|
||||
/// `app.setInterval(cExpr, nMilliseconds)`: run the expression (or a function) every period
|
||||
/// until it is cleared.
|
||||
pub(crate) fn set_interval(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
set(args, true, ctx)
|
||||
}
|
||||
|
||||
/// `app.setTimeOut(cExpr, nMilliseconds)`: run the expression (or a function) once, after the
|
||||
/// period.
|
||||
pub(crate) fn set_time_out(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
set(args, false, ctx)
|
||||
}
|
||||
|
||||
fn set(args: &[JsValue], repeat: bool, ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let handle = ObjectInitializer::new(ctx).build();
|
||||
if ctx.get_data::<Shared>().is_none() {
|
||||
return Ok(handle.into());
|
||||
}
|
||||
let what = arg(args, 0);
|
||||
let code = match what.as_callable() {
|
||||
Some(f) => Code::Function(f.clone()),
|
||||
None => {
|
||||
let source = text(&what, ctx)?;
|
||||
if source.len() > MAX_SCRIPT_BYTES {
|
||||
return Err(error(format!("the timer's script is too long (the limit is {} KiB)", MAX_SCRIPT_BYTES / 1024)));
|
||||
}
|
||||
Code::Source(source)
|
||||
}
|
||||
};
|
||||
let ms = arg(args, 1).to_number(ctx)?;
|
||||
// A missing, negative or NaN period runs as soon as allowed; the float cast saturates.
|
||||
let period_ms = if ms.is_nan() { MIN_TIMER_MS } else { (ms.min(f64::from(u32::MAX)) as u32).max(MIN_TIMER_MS) };
|
||||
let Some(timers) = ctx.get_data::<Shared>() else { return Ok(handle.into()) };
|
||||
let mut timers = timers.borrow_mut();
|
||||
if timers.table.len() >= MAX_TIMERS {
|
||||
return Err(error(format!("a document's scripts can keep at most {MAX_TIMERS} timers")));
|
||||
}
|
||||
let id = timers.last.checked_add(1).ok_or_else(|| error("no more timers can be set"))?;
|
||||
timers.last = id;
|
||||
timers.table.insert(id, Entry { handle: handle.clone(), code, period_ms, repeat });
|
||||
Ok(handle.into())
|
||||
}
|
||||
|
||||
/// `app.clearInterval(oInterval)` and `app.clearTimeOut(oTime)`: forget the timer the object
|
||||
/// stands for. Anything else, or a timer already gone, is ignored.
|
||||
pub(crate) fn clear(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
if let Some(o) = arg(args, 0).as_object()
|
||||
&& let Some(timers) = ctx.get_data::<Shared>()
|
||||
{
|
||||
timers.borrow_mut().table.retain(|_, e| !JsObject::equals(&e.handle, &o));
|
||||
}
|
||||
Ok(JsValue::undefined())
|
||||
}
|
||||
Reference in new issue
Block a user