use crate::interop::completion_helper::{PromiseError, PromiseFuture};
use crate::interop::{storage_helper, stream_helper, JsObject};
use ferroui_base::logging::{LogArea, LogEventLevel, Logger};
use ferroui_base::threading::Dispatcher;
use std::cell::RefCell;
use std::future::Future;
use std::io::{self, Seek, SeekFrom, Write};
use std::rc::Rc;
use std::task::{Poll, Waker};
pub struct WriteableStream {
js_reference: Option<JsObject>,
item: Option<JsObject>,
length: u64,
position: u64,
requests: Rc<RefCell<Requests>>,
}
#[derive(Default)]
struct Requests {
pending: usize,
error: Option<PromiseError>,
wakers: Vec<Waker>,
}
fn disposed() -> io::Error {
io::Error::other("Cannot access a disposed object. Object name: 'WriteableStream'.")
}
impl WriteableStream {
pub(crate) fn new(js_reference: JsObject, item: Option<JsObject>, initial_length: u64) -> Self {
Self { js_reference: Some(js_reference), item, length: initial_length, position: 0, requests: Rc::default() }
}
fn js_reference(&self) -> io::Result<&JsObject> {
self.js_reference.as_ref().ok_or_else(disposed)
}
pub fn can_read(&self) -> bool {
false
}
pub fn can_seek(&self) -> bool {
true
}
pub fn can_write(&self) -> bool {
true
}
pub fn length(&self) -> u64 {
self.length
}
pub fn position(&self) -> u64 {
self.position
}
pub fn set_position(&mut self, position: u64) -> io::Result<()> {
self.seek(SeekFrom::Start(position)).map(|_| ())
}
pub fn set_length(&mut self, value: u64) -> io::Result<()> {
let js_reference = self.js_reference()?.clone();
self.length = value;
if self.position > self.length {
self.position = self.length;
}
self.observe(stream_helper::truncate(&js_reference, value));
Ok(())
}
pub async fn write_async(&mut self, buffer: &[u8]) -> io::Result<()> {
let task = self.write_internal(buffer)?;
task.await.map(|_| ()).map_err(io::Error::from)
}
fn write_internal(&mut self, buffer: &[u8]) -> io::Result<PromiseFuture<JsObject>> {
let js_reference = self.js_reference()?;
let task = stream_helper::write_async(js_reference, buffer);
self.position += buffer.len() as u64;
self.length = self.length.max(self.position);
Ok(task)
}
pub async fn flush_async(&mut self) -> io::Result<()> {
requests_settled(self.requests.clone()).await;
self.take_error()
}
pub async fn close_async(mut self) -> io::Result<()> {
let Some(js_reference) = self.js_reference.take() else {
return Ok(());
};
let close = stream_helper::close_async(&js_reference).await;
requests_settled(self.requests.clone()).await;
self.take_error()?;
close.map(|_| ()).map_err(io::Error::from)
}
fn observe(&self, task: PromiseFuture<JsObject>) {
self.requests.borrow_mut().pending += 1;
let requests = self.requests.clone();
Dispatcher::ui_thread().to_task_scheduler().start_local(async move {
let result = task.await;
let wakers = {
let mut requests = requests.borrow_mut();
if let Err(failure) = result {
requests.error.get_or_insert(failure);
}
requests.pending -= 1;
if requests.pending == 0 {
std::mem::take(&mut requests.wakers)
} else {
Vec::new()
}
};
wakers.into_iter().for_each(Waker::wake);
});
}
fn take_error(&self) -> io::Result<()> {
match self.requests.borrow_mut().error.take() {
Some(error) => Err(error.into()),
None => Ok(()),
}
}
}
fn requests_settled(requests: Rc<RefCell<Requests>>) -> impl Future<Output = ()> {
std::future::poll_fn(move |cx| {
let mut requests = requests.borrow_mut();
if requests.pending == 0 {
Poll::Ready(())
} else {
requests.wakers.push(cx.waker().clone());
Poll::Pending
}
})
}
impl Write for WriteableStream {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
self.take_error()?;
let task = self.write_internal(buffer)?;
self.observe(task);
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> {
self.take_error()
}
}
impl Seek for WriteableStream {
fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
let js_reference = self.js_reference()?.clone();
let position = seek_target(position, self.position, self.length)?;
self.observe(stream_helper::seek(&js_reference, position));
self.position = position;
Ok(position)
}
}
fn seek_target(position: SeekFrom, current: u64, length: u64) -> io::Result<u64> {
let position = match position {
SeekFrom::Current(offset) => current as i64 + offset,
SeekFrom::End(offset) => length as i64 + offset,
SeekFrom::Start(offset) => offset as i64,
};
u64::try_from(position).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"An attempt was made to move the position before the beginning of the stream.",
)
})
}
impl Drop for WriteableStream {
fn drop(&mut self) {
let Some(js_reference) = self.js_reference.take() else {
return;
};
let close = stream_helper::close_async(&js_reference);
let pending = Rc::new(PendingClose { item: self.item.take(), state: RefCell::default() });
PENDING_CLOSES.with(|closes| closes.borrow_mut().push(pending.clone()));
let requests = self.requests.clone();
Dispatcher::ui_thread().to_task_scheduler().start_local(async move {
let close = close.await;
requests_settled(requests.clone()).await;
let error = requests.borrow_mut().error.take().or(close.err());
if let Some(error) = &error {
if let Some(logger) = Logger::try_get(LogEventLevel::Error, LogArea::BROWSER_PLATFORM) {
logger.log(None, &format!("Writing a file failed: {error}"));
}
}
pending.settle(error);
});
}
}
#[derive(Default)]
struct CloseState {
settled: bool,
error: Option<PromiseError>,
wakers: Vec<Waker>,
}
struct PendingClose {
item: Option<JsObject>,
state: RefCell<CloseState>,
}
impl PendingClose {
fn settle(self: &Rc<Self>, error: Option<PromiseError>) {
let wakers = {
let mut state = self.state.borrow_mut();
state.settled = true;
state.error = error;
std::mem::take(&mut state.wakers)
};
if self.state.borrow().error.is_none() {
PENDING_CLOSES.with(|closes| closes.borrow_mut().retain(|close| !Rc::ptr_eq(close, self)));
}
wakers.into_iter().for_each(Waker::wake);
}
fn settled(self: Rc<Self>) -> impl Future<Output = ()> {
std::future::poll_fn(move |cx| {
let mut state = self.state.borrow_mut();
if state.settled {
Poll::Ready(())
} else {
state.wakers.push(cx.waker().clone());
Poll::Pending
}
})
}
fn take_error(self: &Rc<Self>) -> Option<PromiseError> {
let error = self.state.borrow_mut().error.take();
PENDING_CLOSES.with(|closes| closes.borrow_mut().retain(|close| !Rc::ptr_eq(close, self)));
error
}
}
ferroui_base::ferro_thread_local! {
static PENDING_CLOSES: RefCell<Vec<Rc<PendingClose>>> = const { RefCell::new(Vec::new()) };
}
pub(crate) async fn settle_pending_closes(item: &JsObject, report: bool) -> io::Result<()> {
let closes = PENDING_CLOSES.with(|closes| closes.borrow().clone());
let mut first_error = None;
for close in closes {
let Some(close_item) = &close.item else { continue };
if !is_same_entry(item, close_item).await {
continue;
}
close.clone().settled().await;
if report {
if let Some(error) = close.take_error() {
first_error.get_or_insert(error);
}
}
}
match first_error {
Some(error) => Err(io::Error::other(format!("Writing the file failed: {error}"))),
None => Ok(()),
}
}
async fn is_same_entry(item: &JsObject, other: &JsObject) -> bool {
if item == other {
return true;
}
storage_helper::is_same_entry(item, other).await.ok().and_then(|same| same.as_bool()).unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::task::Context;
#[test]
fn a_close_settles_for_its_waiters_and_keeps_only_its_failure() {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::task::Wake;
struct CountingWaker(AtomicUsize);
impl Wake for CountingWaker {
fn wake(self: Arc<Self>) {
self.0.fetch_add(1, Ordering::SeqCst);
}
}
let counter = Arc::new(CountingWaker(AtomicUsize::new(0)));
let waker = Waker::from(counter.clone());
let mut context = Context::from_waker(&waker);
let succeeded = Rc::new(PendingClose { item: None, state: RefCell::default() });
let failed = Rc::new(PendingClose { item: None, state: RefCell::default() });
PENDING_CLOSES.with(|closes| closes.borrow_mut().extend([succeeded.clone(), failed.clone()]));
let mut waiting = Box::pin(succeeded.clone().settled());
assert_eq!(waiting.as_mut().poll(&mut context), Poll::Pending);
succeeded.settle(None);
assert_eq!(counter.0.load(Ordering::SeqCst), 1);
assert_eq!(waiting.as_mut().poll(&mut context), Poll::Ready(()));
failed.settle(Some(PromiseError::new("", "The disk is full")));
assert_eq!(PENDING_CLOSES.with(|closes| closes.borrow().len()), 1, "only the failed close is kept");
assert_eq!(failed.take_error(), Some(PromiseError::new("", "The disk is full")));
assert_eq!(PENDING_CLOSES.with(|closes| closes.borrow().len()), 0, "a reported failure is dropped");
}
#[test]
fn requests_settled_waits_for_the_pending_requests() {
let requests = Rc::new(RefCell::new(Requests { pending: 1, ..Default::default() }));
let mut settled = Box::pin(requests_settled(requests.clone()));
let waker = Waker::noop();
let mut context = Context::from_waker(waker);
assert_eq!(settled.as_mut().poll(&mut context), Poll::Pending);
requests.borrow_mut().pending = 0;
assert_eq!(settled.as_mut().poll(&mut context), Poll::Ready(()));
}
#[test]
fn seek_target_is_relative_to_each_origin() {
assert_eq!(seek_target(SeekFrom::Start(3), 7, 10).unwrap(), 3);
assert_eq!(seek_target(SeekFrom::Current(2), 7, 10).unwrap(), 9);
assert_eq!(seek_target(SeekFrom::End(-4), 7, 10).unwrap(), 6);
assert_eq!(seek_target(SeekFrom::End(5), 7, 10).unwrap(), 15);
assert!(seek_target(SeekFrom::Current(-8), 7, 10).is_err());
}
}