This commit is contained in:
Kingkor Roy Tirtho 2025-12-01 16:20:58 +06:00
parent 57553cdb2e
commit a1672594a2
21 changed files with 524 additions and 156 deletions

View File

@ -59,11 +59,16 @@ import 'package:yt_dlp_dart/yt_dlp_dart.dart';
import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart'; import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart';
const pluginJS = """ const pluginJS = """
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class CoreEndpoint { class CoreEndpoint {
async checkUpdate() { async checkUpdate() {
console.log('Core checkUpdate'); console.log('Core checkUpdate');
await timeout(5000);
console.log('Core checkUpdate done. No updates!');
} }
support() { get support() {
return 'Metadata'; return 'Metadata';
} }
} }
@ -117,22 +122,23 @@ Future<void> main(List<String> rawArgs) async {
await RustLib.init(); await RustLib.init();
final plugin = SpotubePlugin(); 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( final sender = SpotubePlugin.newContext(
pluginScript: pluginJS, pluginScript: pluginJS,
pluginConfig: const PluginConfiguration( pluginConfig: config,
entryPoint: "TestingPlugin",
abilities: [PluginAbility.metadata],
apis: [],
author: "KRTirtho",
description: "Testing Plugin",
name: "Testing Plugin",
pluginApiVersion: "2.0.0",
repository: null,
version: "0.1.0",
),
); );
await plugin.dispose(tx: sender); await plugin.core.checkUpdate(mpscTx: sender, pluginConfig: config);
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);

View File

@ -9,7 +9,7 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'senders.dart'; import 'senders.dart';
// These functions are ignored because they are not marked as `pub`: `js_executor_thread` // 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` // These functions are ignored (category: IgnoreBecauseExplicitAttribute): `create_context`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>> // Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>

130
rust/Cargo.lock generated
View File

@ -69,6 +69,15 @@ dependencies = [
"log", "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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.75" version = "1.0.75"
@ -81,6 +90,17 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 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]] [[package]]
name = "atomic" name = "atomic"
version = "0.5.3" version = "0.5.3"
@ -362,6 +382,28 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -584,6 +626,26 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "fast-float2" name = "fast-float2"
version = "0.2.3" version = "0.2.3"
@ -1052,6 +1114,30 @@ dependencies = [
"windows-registry", "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]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.0.0" version = "2.0.0"
@ -1767,6 +1853,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "relative-path"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.24" version = "0.12.24"
@ -1821,6 +1916,37 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "rust_lib_spotube" name = "rust_lib_spotube"
version = "0.1.0" version = "0.1.0"
@ -1830,10 +1956,12 @@ dependencies = [
"boa_gc", "boa_gc",
"boa_runtime", "boa_runtime",
"flutter_rust_bridge", "flutter_rust_bridge",
"futures", "futures-concurrency",
"futures-lite",
"heck", "heck",
"http", "http",
"reqwest", "reqwest",
"rquickjs",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",

View File

@ -20,9 +20,12 @@ reqwest = { version = "0.12.x" }
http = { version = "1.3.1" } http = { version = "1.3.1" }
serde_json = "1" serde_json = "1"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
futures = "0.3.x" rquickjs = { version = "0", features = ["chrono", "futures"] }
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }
heck = "0.5.0" heck = "0.5.0"
futures-concurrency = "7.6.3"
futures-lite = "2.6.1"
[lints.rust] [lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] } unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }

View File

@ -14,6 +14,7 @@ use crate::api::plugin::models::track::SpotubeTrackObject;
use crate::api::plugin::models::user::SpotubeUserObject; use crate::api::plugin::models::user::SpotubeUserObject;
use tokio::sync::oneshot; use tokio::sync::oneshot;
#[derive(Debug)]
pub enum ArtistCommands { pub enum ArtistCommands {
GetArtist { GetArtist {
id: String, id: String,
@ -47,6 +48,7 @@ pub enum ArtistCommands {
}, },
} }
#[derive(Debug)]
pub enum AlbumCommands { pub enum AlbumCommands {
GetAlbum { GetAlbum {
id: String, id: String,
@ -73,6 +75,7 @@ pub enum AlbumCommands {
}, },
} }
#[derive(Debug)]
pub enum AudioSourceCommands { pub enum AudioSourceCommands {
Matches { Matches {
track: SpotubeTrackObject, track: SpotubeTrackObject,
@ -84,6 +87,7 @@ pub enum AudioSourceCommands {
}, },
} }
#[derive(Debug)]
pub enum AuthCommands { pub enum AuthCommands {
Authenticate { Authenticate {
response_tx: oneshot::Sender<anyhow::Result<()>>, response_tx: oneshot::Sender<anyhow::Result<()>>,
@ -96,6 +100,7 @@ pub enum AuthCommands {
}, },
} }
#[derive(Debug)]
pub enum BrowseCommands { pub enum BrowseCommands {
Sections { Sections {
offset: Option<u32>, offset: Option<u32>,
@ -110,6 +115,7 @@ pub enum BrowseCommands {
}, },
} }
#[derive(Debug)]
pub enum CoreCommands { pub enum CoreCommands {
CheckUpdate { CheckUpdate {
plugin_config: PluginConfiguration, plugin_config: PluginConfiguration,
@ -124,6 +130,7 @@ pub enum CoreCommands {
}, },
} }
#[derive(Debug)]
pub enum PlaylistCommands { pub enum PlaylistCommands {
GetPlaylist { GetPlaylist {
id: String, id: String,
@ -176,6 +183,7 @@ pub enum PlaylistCommands {
}, },
} }
#[derive(Debug)]
pub enum SearchCommands { pub enum SearchCommands {
Chips { Chips {
response_tx: oneshot::Sender<anyhow::Result<Vec<String>>>, response_tx: oneshot::Sender<anyhow::Result<Vec<String>>>,
@ -210,6 +218,7 @@ pub enum SearchCommands {
}, },
} }
#[derive(Debug)]
pub enum TrackCommands { pub enum TrackCommands {
GetTrack { GetTrack {
id: String, id: String,
@ -229,6 +238,7 @@ pub enum TrackCommands {
}, },
} }
#[derive(Debug)]
pub enum UserCommands { pub enum UserCommands {
Me { Me {
response_tx: oneshot::Sender<anyhow::Result<SpotubeUserObject>>, response_tx: oneshot::Sender<anyhow::Result<SpotubeUserObject>>,
@ -255,6 +265,7 @@ pub enum UserCommands {
}, },
} }
#[derive(Debug)]
#[frb(unignore)] #[frb(unignore)]
pub enum PluginCommand { pub enum PluginCommand {
Artist(ArtistCommands), Artist(ArtistCommands),

View File

@ -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<VecDeque<NativeAsyncJob>>,
promise_jobs: RefCell<VecDeque<PromiseJob>>,
timeout_jobs: RefCell<BTreeMap<JsInstant, TimeoutJob>>,
generic_jobs: RefCell<VecDeque<GenericJob>>,
}
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<Self>, 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<Self>, 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<Self>, 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
}
}
}

View File

@ -2,4 +2,5 @@ pub mod commands;
pub mod plugin; pub mod plugin;
pub mod executors; pub mod executors;
pub mod senders; pub mod senders;
pub mod models; pub mod models;
mod event_loop;

View File

@ -1,4 +1,5 @@
use crate::api::plugin::commands::PluginCommand; use crate::api::plugin::commands::PluginCommand;
use crate::api::plugin::event_loop::Queue;
use crate::api::plugin::executors::{ use crate::api::plugin::executors::{
execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse, execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse,
execute_core, execute_playlist, execute_search, execute_track, execute_user, 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 crate::internal::apis::fetcher::ReqwestFetcher;
use anyhow::anyhow; use anyhow::anyhow;
use boa_engine::job::JobExecutor;
use boa_engine::{Context, Source}; use boa_engine::{Context, Source};
use boa_runtime::{fetch, interval, microtask, text, Console, DefaultLogger}; use boa_runtime::{fetch, interval, microtask, text, Console, DefaultLogger};
use flutter_rust_bridge::frb; use flutter_rust_bridge::frb;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use std::thread; use std::thread;
use std::time::Duration;
use tokio::runtime::Runtime; use tokio::runtime::Runtime;
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tokio::sync::{mpsc, Mutex};
use tokio::task;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[frb(opaque)] #[frb(opaque)]
@ -25,51 +32,89 @@ pub struct OpaqueSender {
pub sender: Sender<PluginCommand>, pub sender: Sender<PluginCommand>,
} }
#[frb(ignore)] async fn js_poller_thread(context: Arc<Mutex<Context>>, queue: Rc<Queue>) -> anyhow::Result<()> {
async fn js_executor_thread( let local_set = task::LocalSet::new();
plugin_script: String,
plugin_config: PluginConfiguration,
mut rx: mpsc::Receiver<PluginCommand>,
) -> anyhow::Result<()> {
let mut context = create_context()?;
let injection = format!(
"const pluginInstance = new {}();",
plugin_config.entry_point
);
let script = format!("{}\n{}", plugin_script, injection);
context local_set
.eval(Source::from_bytes(script.as_bytes())) .run_until(async {
let mut ctx = context.lock().await;
queue.run_jobs_async(&RefCell::new(&mut *ctx)).await
})
.await
.map_err(|e| anyhow!("{}", e))?; .map_err(|e| anyhow!("{}", e))?;
Ok(())
}
while let Some(command) = rx.blocking_recv() { // #[frb(ignore)]
match command { async fn js_executor_thread(
PluginCommand::Artist(commands) => execute_artists(commands, &mut context).await?, rx: &mut mpsc::Receiver<PluginCommand>,
PluginCommand::Album(commands) => execute_albums(commands, &mut context).await?, context: Arc<Mutex<Context>>,
PluginCommand::AudioSource(commands) => { ) -> anyhow::Result<()> {
execute_audio_source(commands, &mut context).await? if let Some(command) = rx.recv().await {
} let result = {
PluginCommand::Auth(commands) => execute_auth(commands, &mut context).await?, println!("JS Executor thread received command: {:?}", command);
PluginCommand::Browse(commands) => execute_browse(commands, &mut context).await?, match command {
PluginCommand::Core(commands) => execute_core(commands, &mut context).await?, PluginCommand::Artist(commands) => {
PluginCommand::Playlist(commands) => execute_playlist(commands, &mut context).await?, let mut ctx = context.lock().await;
PluginCommand::Search(commands) => execute_search(commands, &mut context).await?, execute_artists(commands, &mut *ctx).await
PluginCommand::Track(commands) => execute_track(commands, &mut context).await?, }
PluginCommand::User(commands) => execute_user(commands, &mut context).await?, PluginCommand::Album(commands) => {
PluginCommand::Shutdown => { let mut ctx = context.lock().await;
println!("JS Executor thread shutting down."); execute_albums(commands, &mut *ctx).await
// This command doesn't send a response; break the loop instead. }
return Ok(()); 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(()) Ok(())
} }
#[frb(ignore)] #[frb(ignore)]
pub fn create_context() -> anyhow::Result<Context> { pub async fn create_context() -> anyhow::Result<(Context, Rc<Queue>)> {
let mut context = Context::default(); 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))?; Console::register_with_logger(DefaultLogger, &mut context).map_err(|e| anyhow!("{}", e))?;
fetch::register(ReqwestFetcher::new(), None, &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))?; interval::register(&mut context).map_err(|e| anyhow!("{}", e))?;
@ -78,7 +123,7 @@ pub fn create_context() -> anyhow::Result<Context> {
interval::register(&mut context).map_err(|e| anyhow!("{}", e))?; interval::register(&mut context).map_err(|e| anyhow!("{}", e))?;
microtask::register(None, &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 { pub struct SpotubePlugin {
@ -111,20 +156,61 @@ impl SpotubePlugin {
} }
} }
#[frb(sync)] // #[frb(sync)]
pub fn new_context( pub fn new_context(
plugin_script: String, plugin_script: String,
plugin_config: PluginConfiguration, plugin_config: PluginConfiguration,
) -> anyhow::Result<OpaqueSender> { ) -> anyhow::Result<OpaqueSender> {
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(); let rt = Runtime::new().unwrap();
rt.block_on(async { if let Err(e) = rt.block_on(async {
if let Err(e) = js_executor_thread(plugin_script, plugin_config, command_rx).await { let (context, queue) = create_context().await.unwrap();
eprintln!("JS Executor thread encountered a fatal error: {:?}", e); 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 }) Ok(OpaqueSender { sender: command_tx })

View File

@ -1,3 +1,4 @@
use std::backtrace::Backtrace;
use crate::api::plugin::commands::{ use crate::api::plugin::commands::{
AlbumCommands, ArtistCommands, AudioSourceCommands, AuthCommands, BrowseCommands, CoreCommands, AlbumCommands, ArtistCommands, AudioSourceCommands, AuthCommands, BrowseCommands, CoreCommands,
PlaylistCommands, PluginCommand, SearchCommands, TrackCommands, UserCommands, PlaylistCommands, PluginCommand, SearchCommands, TrackCommands, UserCommands,
@ -377,6 +378,7 @@ impl PluginCoreSender {
mpsc_tx: OpaqueSender, mpsc_tx: OpaqueSender,
plugin_config: PluginConfiguration, plugin_config: PluginConfiguration,
) -> anyhow::Result<Option<PluginUpdateAvailable>> { ) -> anyhow::Result<Option<PluginUpdateAvailable>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
mpsc_tx mpsc_tx
.sender .sender
@ -386,7 +388,11 @@ impl PluginCoreSender {
})) }))
.await?; .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<String> { pub async fn support(&self, mpsc_tx: OpaqueSender) -> anyhow::Result<String> {

View File

@ -40,7 +40,7 @@ impl<'a> PluginAlbumEndpoint<'a> {
let args = [JsValue::from(js_string!(id))]; let args = [JsValue::from(js_string!(id))];
let res_json = 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)) 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }
@ -105,7 +105,7 @@ impl<'a> PluginAlbumEndpoint<'a> {
]; ];
let res_json = 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)) 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 ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; 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(()) Ok(())
} }
@ -141,7 +141,7 @@ impl<'a> PluginAlbumEndpoint<'a> {
let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; 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(()) Ok(())
} }

View File

@ -40,7 +40,7 @@ impl<'a> PluginArtistEndpoint<'a> {
let args = [JsValue::from(js_string!(id))]; let args = [JsValue::from(js_string!(id))];
let res_json = 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }
@ -73,7 +73,7 @@ impl<'a> PluginArtistEndpoint<'a> {
]; ];
let res_json = 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)) 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)) 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)) 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 ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; let args = [ids_val.into()];
utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0)?; utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0).await
Ok(())
} }
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> { pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
@ -173,8 +173,6 @@ impl<'a> PluginArtistEndpoint<'a> {
let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; let args = [ids_val.into()];
utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0)?; utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0).await
Ok(())
} }
} }

View File

@ -49,7 +49,7 @@ impl<'a> PluginAudioSourceEndpoint<'a> {
let args = [track_val]; let args = [track_val];
let res = 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)) serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
} }
@ -74,7 +74,7 @@ impl<'a> PluginAudioSourceEndpoint<'a> {
let args = [matched_val]; let args = [matched_val];
let res = 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)) serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
} }

View File

@ -37,7 +37,7 @@ impl<'a> PluginAuthEndpoint<'a> {
let args = []; 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<bool> { pub fn is_authenticated(&mut self) -> anyhow::Result<bool> {
@ -69,6 +69,6 @@ impl<'a> PluginAuthEndpoint<'a> {
let args = []; 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
} }
} }

View File

@ -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)) serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
} }
@ -84,7 +84,7 @@ impl<'a> PluginBrowseEndpoint<'a> {
]; ];
let res = 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)) serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
} }

View File

@ -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 config_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [config_val]; 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() { if res.is_null() {
Ok(None) Ok(None)
} else { } 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 details_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [details_val]; 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
} }
} }

View File

@ -40,7 +40,8 @@ impl<'a> PluginPlaylistEndpoint<'a> {
let args = [JsValue::from(js_string!(id))]; let args = [JsValue::from(js_string!(id))];
let res_json = 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }
@ -74,7 +75,7 @@ impl<'a> PluginPlaylistEndpoint<'a> {
]; ];
let res_json = 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }
@ -115,7 +116,7 @@ impl<'a> PluginPlaylistEndpoint<'a> {
]; ];
let res_json = 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() { if res_json.is_null() {
Ok(None) 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( 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( pub async fn remove_tracks(
@ -215,7 +216,7 @@ impl<'a> PluginPlaylistEndpoint<'a> {
utils::vec_string_to_js_array(track_ids, self.0)?, 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<()> { 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))]; 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<()> { 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))]; 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<()> { 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), delete_playlist_fn.call(&playlist_val, &args, self.0),
self.0, self.0,
) )
.await
} }
} }

View File

@ -65,7 +65,7 @@ impl<'a> PluginSearchEndpoint<'a> {
let args = [JsValue::from(js_string!(query))]; 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)) 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)) 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }
@ -162,7 +162,7 @@ impl<'a> PluginSearchEndpoint<'a> {
]; ];
let res_json = 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)) 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }

View File

@ -39,7 +39,7 @@ impl<'a> PluginTrackEndpoint<'a> {
let args = [JsValue::from(js_string!(id))]; let args = [JsValue::from(js_string!(id))];
let res_json = 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)) 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 ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; let args = [ids_val.into()];
utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0)?; utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0).await
Ok(())
} }
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> { pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
@ -75,9 +73,7 @@ impl<'a> PluginTrackEndpoint<'a> {
let ids_val = utils::vec_string_to_js_array(ids, self.0)?; let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()]; let args = [ids_val.into()];
utils::js_call_to_void(unsave_fn.call(&track_val, &args, self.0), self.0)?; utils::js_call_to_void(unsave_fn.call(&track_val, &args, self.0), self.0).await
Ok(())
} }
pub async fn radio(&mut self, id: String) -> anyhow::Result<Vec<SpotubeTrackObject>> { pub async fn radio(&mut self, id: String) -> anyhow::Result<Vec<SpotubeTrackObject>> {
@ -93,7 +89,7 @@ impl<'a> PluginTrackEndpoint<'a> {
let args = [JsValue::from(js_string!(id))]; let args = [JsValue::from(js_string!(id))];
let res_json = 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }

View File

@ -37,7 +37,7 @@ impl<'a> PluginUserEndpoint<'a> {
.as_function() .as_function()
.ok_or(anyhow!("me is not a 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)) 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)) serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
} }

View File

@ -1,8 +1,6 @@
use anyhow::anyhow; use anyhow::anyhow;
use boa_engine::property::PropertyKey; use boa_engine::property::PropertyKey;
use boa_engine::{ use boa_engine::{object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue};
object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue,
};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
pub fn vec_string_to_js_array( pub fn vec_string_to_js_array(
@ -22,14 +20,15 @@ pub fn vec_string_to_js_array(
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn js_call_to_string( pub async fn js_call_to_string(
result: JsResult<JsValue>, result: JsResult<JsValue>,
context: &mut Context, context: &mut Context,
) -> anyhow::Result<String> { ) -> anyhow::Result<String> {
let res = result let res = result
.map_err(|e| anyhow!("{}", e)) .map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? .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))? .map_err(|e| anyhow!("{}", e))?
.as_string() .as_string()
.ok_or(anyhow!("No response string returned"))? .ok_or(anyhow!("No response string returned"))?
@ -39,21 +38,29 @@ pub fn js_call_to_string(
Ok(res) Ok(res)
} }
pub fn js_call_to_json(result: JsResult<JsValue>, context: &mut Context) -> anyhow::Result<Value> { pub async fn js_call_to_json(
result: JsResult<JsValue>,
context: &mut Context,
) -> anyhow::Result<Value> {
let res = result let res = result
.map_err(|e| anyhow!("{}", e)) .map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? .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))?; .map_err(|e| anyhow!("{}", e))?;
let ls = js_value_to_json(&res, context)?; let ls = js_value_to_json(&res, context)?;
Ok(ls) Ok(ls)
} }
pub fn js_call_to_void(result: JsResult<JsValue>, context: &mut Context) -> anyhow::Result<()> { pub async fn js_call_to_void(
result: JsResult<JsValue>,
context: &mut Context,
) -> anyhow::Result<()> {
result result
.map_err(|e| anyhow!("{}", e)) .map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))? .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))?; .map_err(|e| anyhow!("{}", e))?;
Ok(()) Ok(())

View File

@ -1,48 +1,52 @@
pub mod api; use rquickjs::function::Async;
pub mod internal; use rquickjs::prelude::Func;
pub mod frb_generated; use rquickjs::{
use api::plugin::models::core::{PluginAbility, PluginConfiguration}; async_with, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Function, Object, Promise,
use api::plugin::plugin::SpotubePlugin; Result,
};
use std::time::Duration;
const PLUGIN_JS: &str = "\ fn print(msg: String) {
class Core { println!("{}", msg);
async checkUpdate() {
console.log('Core checkUpdate');
}
support() {
return 'Metadata';
}
} }
class TestingPlugin { async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> {
constructor() { tokio::time::sleep(Duration::from_millis(number as u64)).await;
this.core = new Core(); cb.call::<_, ()>(())
}
} }
";
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> Result<()> {
let sp_plugin = SpotubePlugin::new(); let rt = AsyncRuntime::new()?;
let sender = SpotubePlugin::new_context( let ctx = AsyncContext::full(&rt).await?;
PLUGIN_JS.to_string(),
PluginConfiguration { async_with!(ctx => |ctx| {
entry_point: "TestingPlugin".to_string(), let global = ctx.globals();
abilities: vec![PluginAbility::Metadata], let console = Object::new(ctx.clone()).unwrap();
apis: vec![], console.set("log", Func::from(print)).unwrap();
author: "KRTirtho".to_string(), global.set("console", console).unwrap();
description: "Testing Plugin".to_string(),
name: "Testing Plugin".to_string(), global.set("setTimeout",
plugin_api_version: "2.0.0".to_string(), Function::new(ctx.clone(), Async(set_timeout)).unwrap().with_name("setTimeout")
repository: None, ).unwrap();
version: "0.1.0".to_string(),
if let Ok(function) = ctx.eval::<Function, _>(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?; .await;
println!("Result: {:?}", result);
sp_plugin.dispose(sender.clone()).await?;
Ok(()) Ok(())
} }