load assets locally from indexeddb

This commit is contained in:
Lee-Orr
2024-03-29 14:42:48 -04:00
parent da152b8585
commit 299d8f6e9b
8 changed files with 673 additions and 69 deletions
+147 -3
View File
@@ -6,6 +6,7 @@ use bevy::{
prelude::*,
utils::BoxedFuture,
};
use futures::Future;
use reqwest::StatusCode;
use super::response::GetMediaFileSuccessResponse;
@@ -78,6 +79,29 @@ impl RemoteAssetReader {
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
#[cfg(feature = "wasm")]
async fn load_local<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
let token = path
.ext_file_prefix()
.ok_or_else(|| {
AssetReaderError::Io(std::io::Error::new(
ErrorKind::Other,
format!("Failed to parse token from '{path:?}'"),
))
})?
.to_str()
.unwrap();
let bytes =
wasm::LoadLocalFromIdb::new(token);
info!("Sent request to bevy");
let bytes = bytes.await.map_err(|e| std::io::Error::new(ErrorKind::Other, format!("{e:?}")))?;
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
}
impl AssetReader for RemoteAssetReader {
@@ -85,15 +109,21 @@ impl AssetReader for RemoteAssetReader {
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
let unboxed = async move { self.load(path).await };
if cfg!(not(feature = "wasm")) {
info!("wasm mode not detected");
// NB: Headless mode (server rendering) is running without a Tokio runtime.
// The reqwest HTTP client requires async-compat to function at runtime.
Box::pin(Compat::new(unboxed))
Box::pin(Compat::new( async move { self.load(path).await }))
} else {
info!("wasm mode");
Box::pin(unboxed)
if let Some(window) = web_sys::window() {
if let Ok(search) = window.location().search() {
if search.contains("mock-api") {
return Box::pin( async move { self.load_local(path).await });
}
}
}
Box::pin( async move { self.load(path).await })
}
}
@@ -128,3 +158,117 @@ impl IntoError<AssetReaderError> for reqwest::Error {
AssetReaderError::Io(std::io::Error::new(ErrorKind::Other, format!("{self}")))
}
}
#[cfg(feature = "wasm")]
mod wasm {
use std::task::Waker;
use bevy::{log::info, utils::{HashMap, HashSet}};
use dashmap::{DashMap, DashSet};
use futures::Future;
use js_sys::Uint8Array;
use once_cell::sync::Lazy;
use wasm_bindgen::{prelude::*, JsValue};
use web_sys::{CustomEvent, CustomEventInit};
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
#[wasm_bindgen(typescript_custom_section)]
const TIMELINE_TYPES: &str = r#"
export interface RequestFileEvent extends CustomEvent {
type: "request-file";
detail: string;
}
declare global {
export interface GlobalEventHandlersEventMap {
"request-file": RequestFileEvent;
}
}
"#;
static FILE_WAKERS: Lazy<DashMap<String, Waker>> = Lazy::new(|| DashMap::new());
static FILE_RESPONSES : Lazy<DashMap<String, Result<Vec<u8>, String>>> = Lazy::new(|| DashMap::new());
#[wasm_bindgen(js_name = requestedFileFailed)]
pub fn requested_file_failed(token: String, error: JsValue) {
FILE_RESPONSES.insert(token.clone(), Err(format!("Error: {error:?}")));
let waker = FILE_WAKERS.remove(&token);
if let Some((_, waker)) = waker {
waker.wake();
}
}
#[wasm_bindgen(js_name = requestedFileReceived)]
pub fn requested_file_received(token: String, file: Uint8Array) {
info!("Received File From Mock API {token}");
FILE_RESPONSES.insert(token.clone(), Ok(file.to_vec()));
let waker = FILE_WAKERS.remove(&token);
if let Some((_, waker)) = waker {
waker.wake();
}
}
pub enum LoadLocalFromIdb {
RequestingFile {
token: String
},
CheckingForFile {
token: String
},
}
impl LoadLocalFromIdb {
pub fn new (file: impl ToString) -> Self {
Self::RequestingFile { token: file.to_string() }
}
}
impl Future for LoadLocalFromIdb {
type Output = Result<Vec<u8>, String>;
fn poll(mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
match self.as_ref().get_ref() {
LoadLocalFromIdb::RequestingFile { token } => {
info!("Requesting file {token}");
let Some(window) = web_sys::window() else {
return std::task::Poll::Ready(Err("Couldn't find browser window".to_string()));
};
let event = CustomEvent::new_with_event_init_dict(
"request-file",
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&JsValue::from_str(token)),
)
.map_err(|err| format!("{err:?}"));
let event = match event {
Ok(e) => e,
Err(err) => {
return std::task::Poll::Ready(Err(err));
},
};
FILE_WAKERS.insert(token.clone(), cx.waker().clone());
if let Err(err) = window.document().ok_or(JsValue::null()).and_then(|document| document.dispatch_event(&event)) {
return std::task::Poll::Ready(Err(format!("Error Dispatching Message: {err:?}")));
}
self.set(LoadLocalFromIdb::CheckingForFile { token: token.clone() });
std::task::Poll::Pending
},
LoadLocalFromIdb::CheckingForFile { token } => {
info!("Checking on file {token}");
match FILE_RESPONSES.remove(token) {
Some((_, value)) => {
info!("Found {token}");
std::task::Poll::Ready(value)
},
None => {
std::task::Poll::Pending
}
}
}
}
}
}
}