use std::{cell::RefCell, collections::HashMap, ops::Range, rc::Rc};
use anyhow::{Context as _, Error};
use daicon::protocol::{FileAction, FileMessage, FileRead, ReadResult};
use js_sys::{ArrayBuffer, Uint8Array};
use stewart::{Actor, Context, Options, Sender, State, World};
use tracing::{event, instrument, Level};
use uuid::Uuid;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
#[instrument("SystemFile", skip_all)]
pub fn open_fetch_file(
ctx: &mut Context,
url: String,
hnd: WorldHandle,
) -> Result<Sender<FileMessage>, Error> {
let (mut ctx, sender) = ctx.create(Options::default())?;
let actor = FetchFile {
hnd,
sender: sender.clone(),
url,
pending: HashMap::new(),
};
ctx.start(actor)?;
Ok(sender.map(MessageImpl::Message))
}
struct FetchFile {
hnd: WorldHandle,
sender: Sender<MessageImpl>,
url: String,
pending: HashMap<Uuid, FileRead>,
}
impl Actor for FetchFile {
type Message = MessageImpl;
#[instrument("SystemFile", skip_all)]
fn process(&mut self, ctx: &mut Context, state: &mut State<Self>) -> Result<(), Error> {
while let Some(message) = state.next() {
match message {
MessageImpl::Message(message) => {
self.on_message(message);
}
MessageImpl::FetchResult { id, data } => {
self.on_fetch_result(ctx, id, data)?;
}
}
}
Ok(())
}
}
impl FetchFile {
fn on_message(&mut self, message: FileMessage) {
match message.action {
FileAction::Read(action) => {
self.on_read(message.id, action);
}
FileAction::Write { .. } => {
}
}
}
fn on_read(&mut self, id: Uuid, action: FileRead) {
event!(Level::INFO, "received read");
let range = action.offset..(action.offset + action.size);
self.pending.insert(id, action);
spawn_local(do_fetch(
self.hnd.clone(),
self.sender.clone(),
id,
self.url.clone(),
range,
));
}
fn on_fetch_result(&mut self, ctx: &mut Context, id: Uuid, data: Vec<u8>) -> Result<(), Error> {
event!(Level::INFO, "received fetch result");
let pending = self.pending.remove(&id).context("failed to find pending")?;
let message = ReadResult {
id,
offset: pending.offset,
data,
};
pending.on_result.send(ctx, message);
Ok(())
}
}
enum MessageImpl {
Message(FileMessage),
FetchResult { id: Uuid, data: Vec<u8> },
}
async fn do_fetch(
hnd: WorldHandle,
sender: Sender<MessageImpl>,
id: Uuid,
url: String,
range: Range<u64>,
) {
event!(Level::INFO, "fetching data");
let window = web_sys::window().unwrap();
let headers = Headers::new().unwrap();
let range_header = format!("bytes={}-{}", range.start, range.end - 1);
event!(Level::TRACE, range = range_header);
headers.append("Range", &range_header).unwrap();
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::Cors);
opts.headers(&headers);
let request = Request::new_with_str_and_init(&url, &opts).unwrap();
let response = window.fetch_with_request(&request);
let response = JsFuture::from(response).await.unwrap();
let response: Response = response.dyn_into().unwrap();
let data = response.array_buffer().unwrap();
let data = JsFuture::from(data).await.unwrap();
let data: ArrayBuffer = data.dyn_into().unwrap();
let data = Uint8Array::new(&data).to_vec();
let mut world = hnd.borrow_mut();
let mut ctx = world.root();
sender.send(&mut ctx, MessageImpl::FetchResult { id, data });
world.run_until_idle().unwrap();
}
pub type WorldHandle = Rc<RefCell<World>>;