diff --git a/lib/main.dart b/lib/main.dart index 3f0260c8..4e9b2b2f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -59,11 +59,16 @@ import 'package:yt_dlp_dart/yt_dlp_dart.dart'; import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart'; const pluginJS = """ +function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} class CoreEndpoint { async checkUpdate() { console.log('Core checkUpdate'); + await timeout(5000); + console.log('Core checkUpdate done. No updates!'); } - support() { + get support() { return 'Metadata'; } } @@ -117,22 +122,23 @@ Future main(List rawArgs) async { await RustLib.init(); final plugin = SpotubePlugin(); + const config = PluginConfiguration( + entryPoint: "TestingPlugin", + abilities: [PluginAbility.metadata], + apis: [], + author: "KRTirtho", + description: "Testing Plugin", + name: "Testing Plugin", + pluginApiVersion: "2.0.0", + repository: null, + version: "0.1.0", + ); final sender = SpotubePlugin.newContext( pluginScript: pluginJS, - pluginConfig: const PluginConfiguration( - entryPoint: "TestingPlugin", - abilities: [PluginAbility.metadata], - apis: [], - author: "KRTirtho", - description: "Testing Plugin", - name: "Testing Plugin", - pluginApiVersion: "2.0.0", - repository: null, - version: "0.1.0", - ), + pluginConfig: config, ); - await plugin.dispose(tx: sender); + await plugin.core.checkUpdate(mpscTx: sender, pluginConfig: config); if (kIsDesktop) { await windowManager.setPreventClose(true); diff --git a/lib/src/rust/api/plugin/plugin.dart b/lib/src/rust/api/plugin/plugin.dart index 62d3029f..33c3f8d3 100644 --- a/lib/src/rust/api/plugin/plugin.dart +++ b/lib/src/rust/api/plugin/plugin.dart @@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'senders.dart'; // These functions are ignored because they are not marked as `pub`: `js_executor_thread` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` // These functions are ignored (category: IgnoreBecauseExplicitAttribute): `create_context` // Rust type: RustOpaqueMoi> diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0a698aa2..ce61e4ce 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -69,6 +69,15 @@ dependencies = [ "log", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -81,6 +90,17 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "atomic" version = "0.5.3" @@ -362,6 +382,28 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -584,6 +626,26 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fast-float2" version = "0.2.3" @@ -1052,6 +1114,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -1767,6 +1853,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "reqwest" version = "0.12.24" @@ -1821,6 +1916,37 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rquickjs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a135375fbac5ba723bb6a48f432a72f81539cedde422f0121a86c7c4e96d8e0d" +dependencies = [ + "rquickjs-core", +] + +[[package]] +name = "rquickjs-core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bccb7121a123865c8ace4dea42e7ed84d78b90cbaf4ca32c59849d8d210c9672" +dependencies = [ + "async-lock", + "chrono", + "hashbrown 0.16.1", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b1b6528590d4d65dc86b5159eae2d0219709546644c66408b2441696d1d725" +dependencies = [ + "cc", +] + [[package]] name = "rust_lib_spotube" version = "0.1.0" @@ -1830,10 +1956,12 @@ dependencies = [ "boa_gc", "boa_runtime", "flutter_rust_bridge", - "futures", + "futures-concurrency", + "futures-lite", "heck", "http", "reqwest", + "rquickjs", "serde", "serde_json", "tokio", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2758e1af..ea093787 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -20,9 +20,12 @@ reqwest = { version = "0.12.x" } http = { version = "1.3.1" } serde_json = "1" serde = { version = "1.0.228", features = ["derive"] } -futures = "0.3.x" +rquickjs = { version = "0", features = ["chrono", "futures"] } tokio = { version = "1.48.0", features = ["full"] } heck = "0.5.0" +futures-concurrency = "7.6.3" +futures-lite = "2.6.1" + [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } diff --git a/rust/src/api/plugin/commands.rs b/rust/src/api/plugin/commands.rs index 4fe0e596..6e2b3db2 100644 --- a/rust/src/api/plugin/commands.rs +++ b/rust/src/api/plugin/commands.rs @@ -14,6 +14,7 @@ use crate::api::plugin::models::track::SpotubeTrackObject; use crate::api::plugin::models::user::SpotubeUserObject; use tokio::sync::oneshot; +#[derive(Debug)] pub enum ArtistCommands { GetArtist { id: String, @@ -47,6 +48,7 @@ pub enum ArtistCommands { }, } +#[derive(Debug)] pub enum AlbumCommands { GetAlbum { id: String, @@ -73,6 +75,7 @@ pub enum AlbumCommands { }, } +#[derive(Debug)] pub enum AudioSourceCommands { Matches { track: SpotubeTrackObject, @@ -84,6 +87,7 @@ pub enum AudioSourceCommands { }, } +#[derive(Debug)] pub enum AuthCommands { Authenticate { response_tx: oneshot::Sender>, @@ -96,6 +100,7 @@ pub enum AuthCommands { }, } +#[derive(Debug)] pub enum BrowseCommands { Sections { offset: Option, @@ -110,6 +115,7 @@ pub enum BrowseCommands { }, } +#[derive(Debug)] pub enum CoreCommands { CheckUpdate { plugin_config: PluginConfiguration, @@ -124,6 +130,7 @@ pub enum CoreCommands { }, } +#[derive(Debug)] pub enum PlaylistCommands { GetPlaylist { id: String, @@ -176,6 +183,7 @@ pub enum PlaylistCommands { }, } +#[derive(Debug)] pub enum SearchCommands { Chips { response_tx: oneshot::Sender>>, @@ -210,6 +218,7 @@ pub enum SearchCommands { }, } +#[derive(Debug)] pub enum TrackCommands { GetTrack { id: String, @@ -229,6 +238,7 @@ pub enum TrackCommands { }, } +#[derive(Debug)] pub enum UserCommands { Me { response_tx: oneshot::Sender>, @@ -255,6 +265,7 @@ pub enum UserCommands { }, } +#[derive(Debug)] #[frb(unignore)] pub enum PluginCommand { Artist(ArtistCommands), diff --git a/rust/src/api/plugin/event_loop.rs b/rust/src/api/plugin/event_loop.rs new file mode 100644 index 00000000..5aaca95b --- /dev/null +++ b/rust/src/api/plugin/event_loop.rs @@ -0,0 +1,121 @@ +use boa_engine::context::time::JsInstant; +use boa_engine::job::{GenericJob, Job, JobExecutor, NativeAsyncJob, PromiseJob, TimeoutJob}; +use boa_engine::{Context, JsResult}; +use flutter_rust_bridge::frb; +use futures_concurrency::future::FutureGroup; +use futures_lite::{future, StreamExt}; +use std::cell::RefCell; +use std::collections::{BTreeMap, VecDeque}; +use std::ops::DerefMut; +use std::rc::Rc; +use tokio::task; + +#[frb(ignore)] +pub struct Queue { + async_jobs: RefCell>, + promise_jobs: RefCell>, + timeout_jobs: RefCell>, + generic_jobs: RefCell>, +} + +impl Queue { + pub fn new() -> Self { + Self { + async_jobs: RefCell::default(), + promise_jobs: RefCell::default(), + timeout_jobs: RefCell::default(), + generic_jobs: RefCell::default(), + } + } + + fn drain_timeout_jobs(&self, context: &mut Context) { + let now = context.clock().now(); + + let mut timeouts_borrow = self.timeout_jobs.borrow_mut(); + let mut jobs_to_keep = timeouts_borrow.split_off(&now); + jobs_to_keep.retain(|_, job| !job.is_cancelled()); + let jobs_to_run = std::mem::replace(timeouts_borrow.deref_mut(), jobs_to_keep); + drop(timeouts_borrow); + + for job in jobs_to_run.into_values() { + if let Err(e) = job.call(context) { + eprintln!("Uncaught {e}"); + } + } + } + + fn drain_jobs(&self, context: &mut Context) { + // Run the timeout jobs first. + self.drain_timeout_jobs(context); + + let job = self.generic_jobs.borrow_mut().pop_front(); + if let Some(generic) = job { + if let Err(err) = generic.call(context) { + eprintln!("Uncaught {err}"); + } + } + + let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut()); + for job in jobs { + if let Err(e) = job.call(context) { + eprintln!("Uncaught {e}"); + } + } + context.clear_kept_objects(); + } +} + +impl JobExecutor for Queue { + fn enqueue_job(self: Rc, job: Job, context: &mut Context) { + match job { + Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job), + Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job), + Job::TimeoutJob(t) => { + let now = context.clock().now(); + self.timeout_jobs.borrow_mut().insert(now + t.timeout(), t); + } + Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g), + _ => panic!("unsupported job type"), + } + } + + // While the sync flavor of `run_jobs` will block the current thread until all the jobs have finished... + fn run_jobs(self: Rc, context: &mut Context) -> JsResult<()> { + task::block_in_place(|| { + let runtime = tokio::runtime::Handle::current(); // Get the existing runtime handle + + // Use LocalSet to run the async job on the current thread + runtime.block_on(self.run_jobs_async(&RefCell::new(context))) + }) + } + + // ...the async flavor won't, which allows concurrent execution with external async tasks. + async fn run_jobs_async(self: Rc, context: &RefCell<&mut Context>) -> JsResult<()> { + let mut group = FutureGroup::new(); + loop { + for job in std::mem::take(&mut *self.async_jobs.borrow_mut()) { + group.insert(job.call(context)); + } + + if group.is_empty() + && self.promise_jobs.borrow().is_empty() + && self.timeout_jobs.borrow().is_empty() + && self.generic_jobs.borrow().is_empty() + { + // All queues are empty. We can exit. + return Ok(()); + } + + // We have some jobs pending on the microtask queue. Try to poll the pending + // tasks once to see if any of them finished, and run the pending microtasks + // otherwise. + if let Some(Err(err)) = future::poll_once(group.next()).await.flatten() { + eprintln!("Uncaught {err}"); + }; + + // Only one macrotask can be executed before the next drain of the microtask queue. + self.drain_jobs(&mut context.borrow_mut()); + task::yield_now().await + } + } +} diff --git a/rust/src/api/plugin/mod.rs b/rust/src/api/plugin/mod.rs index 14a1d763..7eadff58 100644 --- a/rust/src/api/plugin/mod.rs +++ b/rust/src/api/plugin/mod.rs @@ -2,4 +2,5 @@ pub mod commands; pub mod plugin; pub mod executors; pub mod senders; -pub mod models; \ No newline at end of file +pub mod models; +mod event_loop; \ No newline at end of file diff --git a/rust/src/api/plugin/plugin.rs b/rust/src/api/plugin/plugin.rs index 3d54d87b..54daeffc 100644 --- a/rust/src/api/plugin/plugin.rs +++ b/rust/src/api/plugin/plugin.rs @@ -1,4 +1,5 @@ use crate::api::plugin::commands::PluginCommand; +use crate::api::plugin::event_loop::Queue; use crate::api::plugin::executors::{ execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse, execute_core, execute_playlist, execute_search, execute_track, execute_user, @@ -11,13 +12,19 @@ use crate::api::plugin::senders::{ }; use crate::internal::apis::fetcher::ReqwestFetcher; use anyhow::anyhow; +use boa_engine::job::JobExecutor; use boa_engine::{Context, Source}; use boa_runtime::{fetch, interval, microtask, text, Console, DefaultLogger}; use flutter_rust_bridge::frb; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::Arc; use std::thread; +use std::time::Duration; use tokio::runtime::Runtime; -use tokio::sync::mpsc; use tokio::sync::mpsc::Sender; +use tokio::sync::{mpsc, Mutex}; +use tokio::task; #[derive(Debug, Clone)] #[frb(opaque)] @@ -25,51 +32,89 @@ pub struct OpaqueSender { pub sender: Sender, } -#[frb(ignore)] -async fn js_executor_thread( - plugin_script: String, - plugin_config: PluginConfiguration, - mut rx: mpsc::Receiver, -) -> anyhow::Result<()> { - let mut context = create_context()?; - let injection = format!( - "const pluginInstance = new {}();", - plugin_config.entry_point - ); - let script = format!("{}\n{}", plugin_script, injection); +async fn js_poller_thread(context: Arc>, queue: Rc) -> anyhow::Result<()> { + let local_set = task::LocalSet::new(); - context - .eval(Source::from_bytes(script.as_bytes())) + local_set + .run_until(async { + let mut ctx = context.lock().await; + queue.run_jobs_async(&RefCell::new(&mut *ctx)).await + }) + .await .map_err(|e| anyhow!("{}", e))?; + Ok(()) +} - while let Some(command) = rx.blocking_recv() { - match command { - PluginCommand::Artist(commands) => execute_artists(commands, &mut context).await?, - PluginCommand::Album(commands) => execute_albums(commands, &mut context).await?, - PluginCommand::AudioSource(commands) => { - execute_audio_source(commands, &mut context).await? - } - PluginCommand::Auth(commands) => execute_auth(commands, &mut context).await?, - PluginCommand::Browse(commands) => execute_browse(commands, &mut context).await?, - PluginCommand::Core(commands) => execute_core(commands, &mut context).await?, - PluginCommand::Playlist(commands) => execute_playlist(commands, &mut context).await?, - PluginCommand::Search(commands) => execute_search(commands, &mut context).await?, - PluginCommand::Track(commands) => execute_track(commands, &mut context).await?, - PluginCommand::User(commands) => execute_user(commands, &mut context).await?, - PluginCommand::Shutdown => { - println!("JS Executor thread shutting down."); - // This command doesn't send a response; break the loop instead. - return Ok(()); +// #[frb(ignore)] +async fn js_executor_thread( + rx: &mut mpsc::Receiver, + context: Arc>, +) -> anyhow::Result<()> { + if let Some(command) = rx.recv().await { + let result = { + println!("JS Executor thread received command: {:?}", command); + match command { + PluginCommand::Artist(commands) => { + let mut ctx = context.lock().await; + execute_artists(commands, &mut *ctx).await + } + PluginCommand::Album(commands) => { + let mut ctx = context.lock().await; + execute_albums(commands, &mut *ctx).await + } + PluginCommand::AudioSource(commands) => { + let mut ctx = context.lock().await; + execute_audio_source(commands, &mut *ctx).await + } + PluginCommand::Auth(commands) => { + let mut ctx = context.lock().await; + execute_auth(commands, &mut *ctx).await + } + PluginCommand::Browse(commands) => { + let mut ctx = context.lock().await; + execute_browse(commands, &mut *ctx).await + } + PluginCommand::Core(commands) => { + let mut ctx = context.lock().await; + execute_core(commands, &mut *ctx).await + } + PluginCommand::Playlist(commands) => { + let mut ctx = context.lock().await; + execute_playlist(commands, &mut *ctx).await + } + PluginCommand::Search(commands) => { + let mut ctx = context.lock().await; + execute_search(commands, &mut *ctx).await + } + PluginCommand::Track(commands) => { + let mut ctx = context.lock().await; + execute_track(commands, &mut *ctx).await + } + PluginCommand::User(commands) => { + let mut ctx = context.lock().await; + execute_user(commands, &mut *ctx).await + } + PluginCommand::Shutdown => { + println!("JS Executor thread shutting down."); + return anyhow::Ok(()); + } } }; - } + println!("JS executor command completed"); + return result; + } Ok(()) } #[frb(ignore)] -pub fn create_context() -> anyhow::Result { - let mut context = Context::default(); +pub async fn create_context() -> anyhow::Result<(Context, Rc)> { + let queue = Rc::new(Queue::new()); + let mut context = Context::builder() + .job_executor(queue.clone()) + .build() + .map_err(|e| anyhow!("{}", e))?; + Console::register_with_logger(DefaultLogger, &mut context).map_err(|e| anyhow!("{}", e))?; fetch::register(ReqwestFetcher::new(), None, &mut context).map_err(|e| anyhow!("{}", e))?; interval::register(&mut context).map_err(|e| anyhow!("{}", e))?; @@ -78,7 +123,7 @@ pub fn create_context() -> anyhow::Result { interval::register(&mut context).map_err(|e| anyhow!("{}", e))?; microtask::register(None, &mut context).map_err(|e| anyhow!("{}", e))?; - Ok(context) + Ok((context, queue)) } pub struct SpotubePlugin { @@ -111,20 +156,61 @@ impl SpotubePlugin { } } - #[frb(sync)] + // #[frb(sync)] pub fn new_context( plugin_script: String, plugin_config: PluginConfiguration, ) -> anyhow::Result { - let (command_tx, command_rx) = mpsc::channel(32); + let (command_tx, mut command_rx) = mpsc::channel(32); - let _thread_handle = thread::spawn(|| { + let _thread_handle = thread::spawn(move || { let rt = Runtime::new().unwrap(); - rt.block_on(async { - if let Err(e) = js_executor_thread(plugin_script, plugin_config, command_rx).await { - eprintln!("JS Executor thread encountered a fatal error: {:?}", e); + if let Err(e) = rt.block_on(async { + let (context, queue) = create_context().await.unwrap(); + let context_arc_mutex = Arc::new(Mutex::new(context)); + + let injection = format!( + "globalThis.pluginInstance = new {}();", + plugin_config.entry_point + ); + let script = format!("{}\n{}", plugin_script, injection); + + { + let context_refcell = context_arc_mutex.clone(); + + context_refcell + .lock() + .await + .eval(Source::from_bytes(script.as_bytes())) + .map_err(|e| anyhow!("{}", e))?; } - }); + + loop { + let executor = js_executor_thread(&mut command_rx, context_arc_mutex.clone()); + let poller = js_poller_thread(context_arc_mutex.clone(), queue.clone()); + let sleep_timer = tokio::time::sleep(Duration::from_millis(10)); + + tokio::select!( + res = executor => { + if let Err(e) = res { + eprintln!("JS Executor task error: {}", e); + break; + } + }, + res = poller => { + if let Err(e) = res { + eprintln!("JS Poller task error: {}", e); + break; + } + }, + _ = sleep_timer => {}, + ); + } + + anyhow::Ok(()) + }) { + eprintln!("JS Executor thread error: {}", e); + } }); Ok(OpaqueSender { sender: command_tx }) diff --git a/rust/src/api/plugin/senders.rs b/rust/src/api/plugin/senders.rs index b35f386f..2a8a1303 100644 --- a/rust/src/api/plugin/senders.rs +++ b/rust/src/api/plugin/senders.rs @@ -1,3 +1,4 @@ +use std::backtrace::Backtrace; use crate::api::plugin::commands::{ AlbumCommands, ArtistCommands, AudioSourceCommands, AuthCommands, BrowseCommands, CoreCommands, PlaylistCommands, PluginCommand, SearchCommands, TrackCommands, UserCommands, @@ -377,6 +378,7 @@ impl PluginCoreSender { mpsc_tx: OpaqueSender, plugin_config: PluginConfiguration, ) -> anyhow::Result> { + let (tx, rx) = oneshot::channel(); mpsc_tx .sender @@ -386,7 +388,11 @@ impl PluginCoreSender { })) .await?; - rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o) + rx.await.map_err(|e| { + eprintln!("RecvError: {}", e); + eprintln!("Stack trace:\n{:?}", Backtrace::capture()); + anyhow!("{e}") + }).and_then(|o| o) } pub async fn support(&self, mpsc_tx: OpaqueSender) -> anyhow::Result { diff --git a/rust/src/internal/album.rs b/rust/src/internal/album.rs index 3c19c978..accdca46 100644 --- a/rust/src/internal/album.rs +++ b/rust/src/internal/album.rs @@ -40,7 +40,7 @@ impl<'a> PluginAlbumEndpoint<'a> { let args = [JsValue::from(js_string!(id))]; let res_json = - utils::js_call_to_json(get_album_fn.call(&album_val, &args, self.0), self.0)?; + utils::js_call_to_json(get_album_fn.call(&album_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -73,7 +73,7 @@ impl<'a> PluginAlbumEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(tracks_fn.call(&album_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(tracks_fn.call(&album_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -105,7 +105,7 @@ impl<'a> PluginAlbumEndpoint<'a> { ]; let res_json = - utils::js_call_to_json(releases_fn.call(&album_val, &args, self.0), self.0)?; + utils::js_call_to_json(releases_fn.call(&album_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -123,7 +123,7 @@ impl<'a> PluginAlbumEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(save_fn.call(&album_val, &args, self.0), self.0)?; + utils::js_call_to_void(save_fn.call(&album_val, &args, self.0), self.0).await?; Ok(()) } @@ -141,7 +141,7 @@ impl<'a> PluginAlbumEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(unsave_fn.call(&album_val, &args, self.0), self.0)?; + utils::js_call_to_void(unsave_fn.call(&album_val, &args, self.0), self.0).await?; Ok(()) } diff --git a/rust/src/internal/artist.rs b/rust/src/internal/artist.rs index 4584a32d..6e98591c 100644 --- a/rust/src/internal/artist.rs +++ b/rust/src/internal/artist.rs @@ -40,7 +40,7 @@ impl<'a> PluginArtistEndpoint<'a> { let args = [JsValue::from(js_string!(id))]; let res_json = - utils::js_call_to_json(get_artist_fn.call(&artist_val, &args, self.0), self.0)?; + utils::js_call_to_json(get_artist_fn.call(&artist_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -73,7 +73,7 @@ impl<'a> PluginArtistEndpoint<'a> { ]; let res_json = - utils::js_call_to_json(top_tracks_fn.call(&artist_val, &args, self.0), self.0)?; + utils::js_call_to_json(top_tracks_fn.call(&artist_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -105,7 +105,8 @@ impl<'a> PluginArtistEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(albums_fn.call(&artist_val, &args, self.0), self.0)?; + let res_json = + utils::js_call_to_json(albums_fn.call(&artist_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -137,7 +138,8 @@ impl<'a> PluginArtistEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(related_fn.call(&artist_val, &args, self.0), self.0)?; + let res_json = + utils::js_call_to_json(related_fn.call(&artist_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -155,9 +157,7 @@ impl<'a> PluginArtistEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0)?; - - Ok(()) + utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0).await } pub async fn unsave(&mut self, ids: Vec) -> anyhow::Result<()> { @@ -173,8 +173,6 @@ impl<'a> PluginArtistEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0)?; - - Ok(()) + utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0).await } } diff --git a/rust/src/internal/audio_source.rs b/rust/src/internal/audio_source.rs index c2b2a9f5..ca4dd15a 100644 --- a/rust/src/internal/audio_source.rs +++ b/rust/src/internal/audio_source.rs @@ -49,7 +49,7 @@ impl<'a> PluginAudioSourceEndpoint<'a> { let args = [track_val]; let res = - utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0)?; + utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0).await?; serde_json::from_value(res).map_err(|e| anyhow!("{}", e)) } @@ -74,7 +74,7 @@ impl<'a> PluginAudioSourceEndpoint<'a> { let args = [matched_val]; let res = - utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0)?; + utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0).await?; serde_json::from_value(res).map_err(|e| anyhow!("{}", e)) } diff --git a/rust/src/internal/auth.rs b/rust/src/internal/auth.rs index 4d02fad3..a6ac7632 100644 --- a/rust/src/internal/auth.rs +++ b/rust/src/internal/auth.rs @@ -37,7 +37,7 @@ impl<'a> PluginAuthEndpoint<'a> { let args = []; - utils::js_call_to_void(authenticate_fn.call(&auth_val, &args, self.0), self.0) + utils::js_call_to_void(authenticate_fn.call(&auth_val, &args, self.0), self.0).await } pub fn is_authenticated(&mut self) -> anyhow::Result { @@ -69,6 +69,6 @@ impl<'a> PluginAuthEndpoint<'a> { let args = []; - utils::js_call_to_void(logout_fn.call(&auth_val, &args, self.0), self.0) + utils::js_call_to_void(logout_fn.call(&auth_val, &args, self.0), self.0).await } } diff --git a/rust/src/internal/browse.rs b/rust/src/internal/browse.rs index d79ef694..1c1bf292 100644 --- a/rust/src/internal/browse.rs +++ b/rust/src/internal/browse.rs @@ -51,7 +51,7 @@ impl<'a> PluginBrowseEndpoint<'a> { }, ]; - let res = utils::js_call_to_json(sections_fn.call(&browse_val, &args, self.0), self.0)?; + let res = utils::js_call_to_json(sections_fn.call(&browse_val, &args, self.0), self.0).await?; serde_json::from_value(res).map_err(|e| anyhow!("{}", e)) } @@ -84,7 +84,7 @@ impl<'a> PluginBrowseEndpoint<'a> { ]; let res = - utils::js_call_to_json(section_items_fn.call(&browse_val, &args, self.0), self.0)?; + utils::js_call_to_json(section_items_fn.call(&browse_val, &args, self.0), self.0).await?; serde_json::from_value(res).map_err(|e| anyhow!("{}", e)) } diff --git a/rust/src/internal/core.rs b/rust/src/internal/core.rs index d48ae898..c7b8b95e 100644 --- a/rust/src/internal/core.rs +++ b/rust/src/internal/core.rs @@ -43,8 +43,7 @@ impl<'a> PluginCoreEndpoint<'a> { let config_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?; let args = [config_val]; - let res = utils::js_call_to_json(check_update_fn.call(&core_val, &args, self.0), self.0)?; - + let res = utils::js_call_to_json(check_update_fn.call(&core_val, &args, self.0), self.0).await?; if res.is_null() { Ok(None) } else { @@ -81,6 +80,6 @@ impl<'a> PluginCoreEndpoint<'a> { let details_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?; let args = [details_val]; - utils::js_call_to_void(scrobble_fn.call(&core_val, &args, self.0), self.0) + utils::js_call_to_void(scrobble_fn.call(&core_val, &args, self.0), self.0).await } } diff --git a/rust/src/internal/playlist.rs b/rust/src/internal/playlist.rs index e70c3653..9d07ba5e 100644 --- a/rust/src/internal/playlist.rs +++ b/rust/src/internal/playlist.rs @@ -40,7 +40,8 @@ impl<'a> PluginPlaylistEndpoint<'a> { let args = [JsValue::from(js_string!(id))]; let res_json = - utils::js_call_to_json(get_playlist_fn.call(&playlist_val, &args, self.0), self.0)?; + utils::js_call_to_json(get_playlist_fn.call(&playlist_val, &args, self.0), self.0) + .await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -74,7 +75,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { ]; let res_json = - utils::js_call_to_json(tracks_fn.call(&playlist_val, &args, self.0), self.0)?; + utils::js_call_to_json(tracks_fn.call(&playlist_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -115,7 +116,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { ]; let res_json = - utils::js_call_to_json(create_fn.call(&playlist_val, &args, self.0), self.0)?; + utils::js_call_to_json(create_fn.call(&playlist_val, &args, self.0), self.0).await?; if res_json.is_null() { Ok(None) @@ -164,7 +165,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { }, ]; - utils::js_call_to_void(update_fn.call(&playlist_val, &args, self.0), self.0) + utils::js_call_to_void(update_fn.call(&playlist_val, &args, self.0), self.0).await } pub async fn add_tracks( @@ -192,7 +193,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { }, ]; - utils::js_call_to_void(add_tracks_fn.call(&playlist_val, &args, self.0), self.0) + utils::js_call_to_void(add_tracks_fn.call(&playlist_val, &args, self.0), self.0).await } pub async fn remove_tracks( @@ -215,7 +216,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { utils::vec_string_to_js_array(track_ids, self.0)?, ]; - utils::js_call_to_void(remove_tracks_fn.call(&playlist_val, &args, self.0), self.0) + utils::js_call_to_void(remove_tracks_fn.call(&playlist_val, &args, self.0), self.0).await } pub async fn save(&mut self, playlist_id: String) -> anyhow::Result<()> { @@ -230,7 +231,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { let args = [JsValue::from(js_string!(playlist_id))]; - utils::js_call_to_void(save_fn.call(&playlist_val, &args, self.0), self.0) + utils::js_call_to_void(save_fn.call(&playlist_val, &args, self.0), self.0).await } pub async fn unsave(&mut self, playlist_id: String) -> anyhow::Result<()> { @@ -245,7 +246,7 @@ impl<'a> PluginPlaylistEndpoint<'a> { let args = [JsValue::from(js_string!(playlist_id))]; - utils::js_call_to_void(unsave_fn.call(&playlist_val, &args, self.0), self.0) + utils::js_call_to_void(unsave_fn.call(&playlist_val, &args, self.0), self.0).await } pub async fn delete_playlist(&mut self, playlist_id: String) -> anyhow::Result<()> { @@ -264,5 +265,6 @@ impl<'a> PluginPlaylistEndpoint<'a> { delete_playlist_fn.call(&playlist_val, &args, self.0), self.0, ) + .await } } diff --git a/rust/src/internal/search.rs b/rust/src/internal/search.rs index 3e7f5004..ff10a2b0 100644 --- a/rust/src/internal/search.rs +++ b/rust/src/internal/search.rs @@ -65,7 +65,7 @@ impl<'a> PluginSearchEndpoint<'a> { let args = [JsValue::from(js_string!(query))]; - let res_json = utils::js_call_to_json(all_fn.call(&search_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(all_fn.call(&search_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -97,7 +97,7 @@ impl<'a> PluginSearchEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(albums_fn.call(&search_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(albums_fn.call(&search_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -129,7 +129,7 @@ impl<'a> PluginSearchEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(artists_fn.call(&search_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(artists_fn.call(&search_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -162,7 +162,7 @@ impl<'a> PluginSearchEndpoint<'a> { ]; let res_json = - utils::js_call_to_json(playlists_fn.call(&search_val, &args, self.0), self.0)?; + utils::js_call_to_json(playlists_fn.call(&search_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -194,7 +194,7 @@ impl<'a> PluginSearchEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(tracks_fn.call(&search_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(tracks_fn.call(&search_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } diff --git a/rust/src/internal/track.rs b/rust/src/internal/track.rs index 21c576f3..fdba2efd 100644 --- a/rust/src/internal/track.rs +++ b/rust/src/internal/track.rs @@ -39,7 +39,7 @@ impl<'a> PluginTrackEndpoint<'a> { let args = [JsValue::from(js_string!(id))]; let res_json = - utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0)?; + utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -57,9 +57,7 @@ impl<'a> PluginTrackEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0)?; - - Ok(()) + utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0).await } pub async fn unsave(&mut self, ids: Vec) -> anyhow::Result<()> { @@ -75,9 +73,7 @@ impl<'a> PluginTrackEndpoint<'a> { let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let args = [ids_val.into()]; - utils::js_call_to_void(unsave_fn.call(&track_val, &args, self.0), self.0)?; - - Ok(()) + utils::js_call_to_void(unsave_fn.call(&track_val, &args, self.0), self.0).await } pub async fn radio(&mut self, id: String) -> anyhow::Result> { @@ -93,7 +89,7 @@ impl<'a> PluginTrackEndpoint<'a> { let args = [JsValue::from(js_string!(id))]; let res_json = - utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0)?; + utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } diff --git a/rust/src/internal/user.rs b/rust/src/internal/user.rs index bb1e1e36..dc9c6821 100644 --- a/rust/src/internal/user.rs +++ b/rust/src/internal/user.rs @@ -37,7 +37,7 @@ impl<'a> PluginUserEndpoint<'a> { .as_function() .ok_or(anyhow!("me is not a function"))?; - let res_json = utils::js_call_to_json(me_fn.call(&user_val, &[], self.0), self.0)?; + let res_json = utils::js_call_to_json(me_fn.call(&user_val, &[], self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } @@ -68,7 +68,7 @@ impl<'a> PluginUserEndpoint<'a> { }, ]; - let res_json = utils::js_call_to_json(saved_fn.call(&user_val, &args, self.0), self.0)?; + let res_json = utils::js_call_to_json(saved_fn.call(&user_val, &args, self.0), self.0).await?; serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e)) } diff --git a/rust/src/internal/utils.rs b/rust/src/internal/utils.rs index d99e47ca..79187834 100644 --- a/rust/src/internal/utils.rs +++ b/rust/src/internal/utils.rs @@ -1,8 +1,6 @@ use anyhow::anyhow; use boa_engine::property::PropertyKey; -use boa_engine::{ - object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue, -}; +use boa_engine::{object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue}; use serde_json::{Map, Value}; pub fn vec_string_to_js_array( @@ -22,14 +20,15 @@ pub fn vec_string_to_js_array( } #[allow(dead_code)] -pub fn js_call_to_string( +pub async fn js_call_to_string( result: JsResult, context: &mut Context, ) -> anyhow::Result { let res = result .map_err(|e| anyhow!("{}", e)) .and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? - .await_blocking(context) + .into_js_future(context) + .await .map_err(|e| anyhow!("{}", e))? .as_string() .ok_or(anyhow!("No response string returned"))? @@ -39,21 +38,29 @@ pub fn js_call_to_string( Ok(res) } -pub fn js_call_to_json(result: JsResult, context: &mut Context) -> anyhow::Result { +pub async fn js_call_to_json( + result: JsResult, + context: &mut Context, +) -> anyhow::Result { let res = result .map_err(|e| anyhow!("{}", e)) .and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? - .await_blocking(context) + .into_js_future(context) + .await .map_err(|e| anyhow!("{}", e))?; let ls = js_value_to_json(&res, context)?; Ok(ls) } -pub fn js_call_to_void(result: JsResult, context: &mut Context) -> anyhow::Result<()> { +pub async fn js_call_to_void( + result: JsResult, + context: &mut Context, +) -> anyhow::Result<()> { result .map_err(|e| anyhow!("{}", e)) .and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? - .await_blocking(context) + .into_js_future(context) + .await .map_err(|e| anyhow!("{}", e))?; Ok(()) diff --git a/rust/src/main.rs b/rust/src/main.rs index 9cf68c35..873ff4bc 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -1,48 +1,52 @@ -pub mod api; -pub mod internal; -pub mod frb_generated; -use api::plugin::models::core::{PluginAbility, PluginConfiguration}; -use api::plugin::plugin::SpotubePlugin; +use rquickjs::function::Async; +use rquickjs::prelude::Func; +use rquickjs::{ + async_with, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Function, Object, Promise, + Result, +}; +use std::time::Duration; -const PLUGIN_JS: &str = "\ -class Core { - async checkUpdate() { - console.log('Core checkUpdate'); - } - support() { - return 'Metadata'; - } +fn print(msg: String) { + println!("{}", msg); } -class TestingPlugin { - constructor() { - this.core = new Core(); - } +async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> { + tokio::time::sleep(Duration::from_millis(number as u64)).await; + cb.call::<_, ()>(()) } -"; #[tokio::main] -async fn main() -> anyhow::Result<()> { - let sp_plugin = SpotubePlugin::new(); - let sender = SpotubePlugin::new_context( - PLUGIN_JS.to_string(), - PluginConfiguration { - entry_point: "TestingPlugin".to_string(), - abilities: vec![PluginAbility::Metadata], - apis: vec![], - author: "KRTirtho".to_string(), - description: "Testing Plugin".to_string(), - name: "Testing Plugin".to_string(), - plugin_api_version: "2.0.0".to_string(), - repository: None, - version: "0.1.0".to_string(), +async fn main() -> Result<()> { + let rt = AsyncRuntime::new()?; + let ctx = AsyncContext::full(&rt).await?; + + async_with!(ctx => |ctx| { + let global = ctx.globals(); + let console = Object::new(ctx.clone()).unwrap(); + console.set("log", Func::from(print)).unwrap(); + global.set("console", console).unwrap(); + + global.set("setTimeout", + Function::new(ctx.clone(), Async(set_timeout)).unwrap().with_name("setTimeout") + ).unwrap(); + + if let Ok(function) = ctx.eval::(r#" + (function(){ + return new Promise((resolve, reject) => { + setTimeout(() => { + console.log("hello world"); + resolve(); + }, 100); + }) + }) + "#) { + let promise: Promise = function.call(()).unwrap(); + if let Err(err) = promise.into_future::<()>().await.catch(&ctx) { + eprintln!("{:?}", err); + } } - )?; - let result = sp_plugin.core.support(sender.clone()).await?; - - println!("Result: {:?}", result); - - sp_plugin.dispose(sender.clone()).await?; + }) + .await; Ok(()) }