Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/src/frb/api/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,16 @@ abstract class JsEngine implements RustOpaqueInterface {

/// Sets the memory limit on the engine-owned runtime.
Future<void> setMemoryLimit({required BigInt limit});

/// Starts the background driver on the engine-owned runtime.
///
/// See [`JsAsyncRuntime::start_drive`].
Future<void> startDrive();

/// Stops the background driver on the engine-owned runtime.
///
/// See [`JsAsyncRuntime::stop_drive`]. Safe to call at any time.
Future<void> stopDrive();
}

/// Runtime configuration applied when constructing a high-level `JsEngine`.
Expand Down
30 changes: 30 additions & 0 deletions lib/src/frb/api/runtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,36 @@ abstract class JsAsyncRuntime implements RustOpaqueInterface {
/// await runtime.setMemoryLimit(limit: 16 * 1024 * 1024); // 16 MB
/// ```
Future<void> setMemoryLimit({required BigInt limit});

/// Starts a background task that keeps the runtime's async work moving, so
/// timers, `fetch`, and other background work finish promptly without the
/// host having to keep checking. Unlike [`idle()`](Self::idle), which only
/// returns once all work is done, this keeps running — so it's fine to leave
/// on for the whole life of the app, and `eval` and other calls still run in
/// between.
///
/// Calling it again while a driver is already running does nothing. The
/// driver runs until [`stop_drive()`](Self::stop_drive) is called or the
/// runtime is dropped.
///
/// ## Example
///
/// ```dart
/// await runtime.startDrive();
/// ```
Future<void> startDrive();

/// Stops the background driver started by [`start_drive()`](Self::start_drive).
///
/// Cancels the task if one is running, and does nothing if not. The runtime
/// is left usable — you can start a driver again or drain it by hand afterwards.
///
/// ## Example
///
/// ```dart
/// await runtime.stopDrive();
/// ```
Future<void> stopDrive();
}

// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<JsContext>>
Expand Down
432 changes: 300 additions & 132 deletions lib/src/frb/frb_generated.dart

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions libfjs/src/api/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,23 @@ impl JsEngine {
Ok(())
}

/// Starts the background driver on the engine-owned runtime.
///
/// See [`JsAsyncRuntime::start_drive`].
pub async fn start_drive(&self) -> anyhow::Result<()> {
self.ensure_runtime_accessible()?;
self.runtime.start_drive().await;
Ok(())
}

/// Stops the background driver on the engine-owned runtime.
///
/// See [`JsAsyncRuntime::stop_drive`]. Safe to call at any time.
pub async fn stop_drive(&self) -> anyhow::Result<()> {
self.runtime.stop_drive().await;
Ok(())
}

/// Returns whether the engine-owned runtime still has work pending.
pub async fn is_job_pending(&self) -> anyhow::Result<bool> {
self.ensure_runtime_accessible()?;
Expand Down Expand Up @@ -437,6 +454,10 @@ impl JsEngine {
let previous_state = self.begin_close()?;

if previous_state == STATE_CREATED || previous_state == STATE_RUNNING {
// Stop the background driver (if any) before draining and freeing
// the runtime, so it cannot race teardown.
self.runtime.stop_drive().await;

let _ = self
.context
.ctx
Expand Down
49 changes: 48 additions & 1 deletion libfjs/src/api/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use flutter_rust_bridge::frb;
use rquickjs::loader::{BuiltinLoader, BuiltinResolver, FileResolver, NativeLoader, ScriptLoader};
use rquickjs::promise::MaybePromise;
use rquickjs::{CatchResultExt, FromJs, Module, Promise};
use std::sync::{Arc, RwLock};
use std::sync::{Arc, Mutex, RwLock};

/// Memory usage statistics for the JavaScript runtime.
///
Expand Down Expand Up @@ -703,6 +703,11 @@ impl JsContext {
pub struct JsAsyncRuntime {
pub(crate) rt: rquickjs::AsyncRuntime,
pub(crate) global_attachment: Option<GlobalAttachment>,
/// Handle to the background task spawned by [`JsAsyncRuntime::start_drive`].
///
/// Shared across clones so that starting/stopping the driver is coherent
/// regardless of which clone the call lands on.
pub(crate) driver: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
}

impl JsAsyncRuntime {
Expand All @@ -727,6 +732,7 @@ impl JsAsyncRuntime {
Ok(Self {
rt: runtime,
global_attachment: None,
driver: Arc::new(Mutex::new(None)),
})
}

Expand Down Expand Up @@ -777,6 +783,7 @@ impl JsAsyncRuntime {
Ok(Self {
rt: runtime,
global_attachment: Some(global_attachment),
driver: Arc::new(Mutex::new(None)),
})
}

Expand Down Expand Up @@ -927,6 +934,46 @@ impl JsAsyncRuntime {
self.rt.idle().await;
}

/// Starts a background task that keeps the runtime's async work moving, so
/// timers, `fetch`, and other background work finish promptly without the
/// host having to keep checking. Unlike [`idle()`](Self::idle), which only
/// returns once all work is done, this keeps running — so it's fine to leave
/// on for the whole life of the app, and `eval` and other calls still run in
/// between.
///
/// Calling it again while a driver is already running does nothing. The
/// driver runs until [`stop_drive()`](Self::stop_drive) is called or the
/// runtime is dropped.
///
/// ## Example
///
/// ```dart
/// await runtime.startDrive();
/// ```
pub async fn start_drive(&self) {
let mut slot = self.driver.lock().unwrap();
if slot.as_ref().is_some_and(|handle| !handle.is_finished()) {
return;
}
*slot = Some(tokio::spawn(self.rt.drive()));
}

/// Stops the background driver started by [`start_drive()`](Self::start_drive).
///
/// Cancels the task if one is running, and does nothing if not. The runtime
/// is left usable — you can start a driver again or drain it by hand afterwards.
///
/// ## Example
///
/// ```dart
/// await runtime.stopDrive();
/// ```
pub async fn stop_drive(&self) {
if let Some(handle) = self.driver.lock().unwrap().take() {
handle.abort();
}
}

/// Sets runtime info string.
///
/// Sets informational metadata about the runtime instance.
Expand Down
Loading