use std::collections::HashMap;
use std::sync::Mutex;
use blinc_platform::assets::{AssetLoader, AssetPath};
use blinc_platform::{PlatformError, Result};
#[derive(Debug, Default)]
pub struct WebAssetLoader {
cache: Mutex<HashMap<String, Vec<u8>>>,
}
impl WebAssetLoader {
pub fn new() -> Self {
Self {
cache: Mutex::new(HashMap::new()),
}
}
pub fn insert_raw(&self, key: impl Into<String>, bytes: Vec<u8>) {
if let Ok(mut cache) = self.cache.lock() {
cache.insert(key.into(), bytes);
}
}
pub fn len(&self) -> usize {
self.cache.lock().map(|c| c.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[cfg(target_arch = "wasm32")]
pub async fn preload(&self, urls: &[&str]) -> Result<()> {
for url in urls {
let bytes = Self::fetch_bytes(url).await?;
self.insert_raw(*url, bytes);
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn preload(&self, _urls: &[&str]) -> Result<()> {
Ok(())
}
#[cfg(target_arch = "wasm32")]
pub async fn fetch_bytes(url: &str) -> Result<Vec<u8>> {
fetch_as_bytes(url).await
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn fetch_bytes(_url: &str) -> Result<Vec<u8>> {
Err(PlatformError::Unsupported(
"WebAssetLoader::fetch_bytes is wasm32-only".to_string(),
))
}
fn key_for(path: &AssetPath) -> String {
match path {
AssetPath::Relative(rel) => rel.clone(),
AssetPath::Absolute(abs) => abs.clone(),
AssetPath::Embedded(name) => name.to_string(),
}
}
}
#[derive(Clone)]
pub struct SharedWebAssetLoader(pub std::sync::Arc<WebAssetLoader>);
impl AssetLoader for SharedWebAssetLoader {
fn load(&self, path: &AssetPath) -> Result<Vec<u8>> {
self.0.load(path)
}
fn exists(&self, path: &AssetPath) -> bool {
self.0.exists(path)
}
fn platform_name(&self) -> &'static str {
"web"
}
}
impl AssetLoader for WebAssetLoader {
fn load(&self, path: &AssetPath) -> Result<Vec<u8>> {
let key = Self::key_for(path);
let cache = self
.cache
.lock()
.map_err(|e| PlatformError::AssetLoad(format!("WebAssetLoader cache poisoned: {e}")))?;
cache.get(&key).cloned().ok_or_else(|| {
PlatformError::AssetLoad(format!(
"Asset '{key}' not preloaded — call WebAssetLoader::preload before run"
))
})
}
fn exists(&self, path: &AssetPath) -> bool {
let key = Self::key_for(path);
self.cache
.lock()
.map(|cache| cache.contains_key(&key))
.unwrap_or(false)
}
fn platform_name(&self) -> &'static str {
"web"
}
}
#[cfg(target_arch = "wasm32")]
async fn fetch_as_bytes(url: &str) -> Result<Vec<u8>> {
use js_sys::Uint8Array;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Request, RequestInit, RequestMode, Response};
let opts = RequestInit::new();
opts.set_method("GET");
opts.set_mode(RequestMode::Cors);
let request = Request::new_with_str_and_init(url, &opts).map_err(|e| {
PlatformError::AssetLoad(format!("Failed to build request for {url}: {e:?}"))
})?;
let window = web_sys::window()
.ok_or_else(|| PlatformError::AssetLoad("No global window object".to_string()))?;
let resp_val = JsFuture::from(window.fetch_with_request(&request))
.await
.map_err(|e| PlatformError::AssetLoad(format!("fetch({url}) failed: {e:?}")))?;
let response: Response = resp_val
.dyn_into()
.map_err(|_| PlatformError::AssetLoad(format!("fetch({url}) returned non-Response")))?;
if !response.ok() {
return Err(PlatformError::AssetLoad(format!(
"fetch({url}) returned HTTP {}",
response.status()
)));
}
let buf_val = JsFuture::from(
response
.array_buffer()
.map_err(|e| PlatformError::AssetLoad(format!("array_buffer() error: {e:?}")))?,
)
.await
.map_err(|e| PlatformError::AssetLoad(format!("array_buffer() rejected: {e:?}")))?;
let array = Uint8Array::new(&buf_val);
let mut bytes = vec![0u8; array.length() as usize];
array.copy_to(&mut bytes);
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_asset_returns_error() {
let loader = WebAssetLoader::new();
let result = loader.load(&AssetPath::Relative("nope.ttf".into()));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(format!("{err}").contains("nope.ttf"));
}
#[test]
fn insert_then_load_round_trips() {
let loader = WebAssetLoader::new();
loader.insert_raw("logo.png", vec![1, 2, 3, 4]);
let bytes = loader
.load(&AssetPath::Relative("logo.png".into()))
.expect("preloaded asset should be present");
assert_eq!(bytes, vec![1, 2, 3, 4]);
assert!(loader.exists(&AssetPath::Relative("logo.png".into())));
assert!(!loader.exists(&AssetPath::Relative("missing.png".into())));
}
#[test]
fn embedded_paths_match_relative_lookup() {
let loader = WebAssetLoader::new();
loader.insert_raw("hero.svg", vec![42]);
assert_eq!(
loader.load(&AssetPath::Embedded("hero.svg")).unwrap(),
vec![42]
);
}
#[test]
fn platform_name_is_web() {
let loader = WebAssetLoader::new();
assert_eq!(loader.platform_name(), "web");
}
#[test]
fn empty_loader_reports_empty() {
let loader = WebAssetLoader::new();
assert_eq!(loader.len(), 0);
assert!(loader.is_empty());
}
}