use js_sys::{Function, Object, Promise, Reflect, Uint8Array};
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
pub(super) const MAX_CHUNK: usize = 8 * 1024 * 1024;
fn err(message: impl AsRef<str>) -> JsValue {
JsValue::from_str(message.as_ref())
}
fn method(target: &JsValue, name: &str) -> Result<Function, JsValue> {
let value = Reflect::get(target, &JsValue::from_str(name))?;
value
.dyn_into::<Function>()
.map_err(|_| err(format!("OPFS: `{name}` is unavailable on this host")))
}
async fn await_promise(value: JsValue, what: &str) -> Result<JsValue, JsValue> {
let promise: Promise = value
.dyn_into()
.map_err(|_| err(format!("OPFS: `{what}` did not return a Promise")))?;
JsFuture::from(promise).await
}
fn number(value: &JsValue, what: &str) -> Result<f64, JsValue> {
value
.as_f64()
.ok_or_else(|| err(format!("OPFS: `{what}` did not return a number")))
}
pub(super) async fn root_directory() -> Result<JsValue, JsValue> {
let global = js_sys::global();
let navigator = Reflect::get(&global, &JsValue::from_str("navigator"))?;
if navigator.is_undefined() || navigator.is_null() {
return Err(err("OPFS: this realm has no `navigator`"));
}
let storage = Reflect::get(&navigator, &JsValue::from_str("storage"))?;
if storage.is_undefined() || storage.is_null() {
return Err(err(
"OPFS: `navigator.storage` is unavailable; OPFS needs a secure context (https or localhost)",
));
}
let get_directory = method(&storage, "getDirectory")?;
await_promise(get_directory.call0(&storage)?, "getDirectory").await
}
pub(super) async fn directory_handle(
parent: &JsValue,
name: &str,
create: bool,
) -> Result<JsValue, JsValue> {
let options = Object::new();
Reflect::set(&options, &JsValue::from_str("create"), &create.into())?;
let get = method(parent, "getDirectoryHandle")?;
await_promise(
get.call2(parent, &JsValue::from_str(name), &options)?,
"getDirectoryHandle",
)
.await
}
pub(super) async fn file_handle(
dir: &JsValue,
name: &str,
create: bool,
) -> Result<JsValue, JsValue> {
let options = Object::new();
Reflect::set(&options, &JsValue::from_str("create"), &create.into())?;
let get = method(dir, "getFileHandle")?;
await_promise(
get.call2(dir, &JsValue::from_str(name), &options)?,
"getFileHandle",
)
.await
}
pub(super) async fn remove_entry(dir: &JsValue, name: &str) -> Result<(), JsValue> {
let remove = method(dir, "removeEntry")?;
await_promise(remove.call1(dir, &JsValue::from_str(name))?, "removeEntry").await?;
Ok(())
}
pub(super) async fn list_files(dir: &JsValue) -> Result<Vec<(String, JsValue)>, JsValue> {
let values = method(dir, "values")?;
let iterator = values.call0(dir)?;
let mut files = Vec::new();
loop {
let next = method(&iterator, "next")?;
let step = await_promise(next.call0(&iterator)?, "values().next").await?;
if Reflect::get(&step, &JsValue::from_str("done"))?
.as_bool()
.unwrap_or(true)
{
break;
}
let handle = Reflect::get(&step, &JsValue::from_str("value"))?;
let kind = Reflect::get(&handle, &JsValue::from_str("kind"))?;
if kind.as_string().as_deref() != Some("file") {
continue;
}
if let Some(name) = Reflect::get(&handle, &JsValue::from_str("name"))?.as_string() {
files.push((name, handle));
}
}
Ok(files)
}
pub(super) async fn sync_access_handle(file: &JsValue) -> Result<JsValue, JsValue> {
let create = method(file, "createSyncAccessHandle")?;
await_promise(create.call0(file)?, "createSyncAccessHandle").await
}
pub(super) fn read_at(handle: &JsValue, at: u64, buf: &mut [u8]) -> Result<usize, JsValue> {
let mut done = 0usize;
while done < buf.len() {
let want = (buf.len() - done).min(MAX_CHUNK);
let scratch = Uint8Array::new_with_length(want as u32);
let options = Object::new();
Reflect::set(
&options,
&JsValue::from_str("at"),
&JsValue::from_f64((at + done as u64) as f64),
)?;
let read = method(handle, "read")?;
let got = number(&read.call2(handle, &scratch, &options)?, "read")? as usize;
let got = got.min(want);
if got == 0 {
break;
}
scratch
.subarray(0, got as u32)
.copy_to(&mut buf[done..done + got]);
done += got;
}
Ok(done)
}
pub(super) fn write_at(handle: &JsValue, at: u64, buf: &[u8]) -> Result<usize, JsValue> {
let mut done = 0usize;
while done < buf.len() {
let want = (buf.len() - done).min(MAX_CHUNK);
let chunk = Uint8Array::from(&buf[done..done + want]);
let options = Object::new();
Reflect::set(
&options,
&JsValue::from_str("at"),
&JsValue::from_f64((at + done as u64) as f64),
)?;
let write = method(handle, "write")?;
let put = number(&write.call2(handle, &chunk, &options)?, "write")? as usize;
let put = put.min(want);
if put == 0 {
break;
}
done += put;
}
Ok(done)
}
pub(super) fn flush(handle: &JsValue) -> Result<(), JsValue> {
method(handle, "flush")?.call0(handle)?;
Ok(())
}
pub(super) fn truncate(handle: &JsValue, len: u64) -> Result<(), JsValue> {
method(handle, "truncate")?.call1(handle, &JsValue::from_f64(len as f64))?;
Ok(())
}
pub(super) fn close(handle: &JsValue) -> Result<(), JsValue> {
method(handle, "close")?.call0(handle)?;
Ok(())
}
pub(super) async fn write_whole_file(
dir: &JsValue,
name: &str,
data: &[u8],
) -> Result<(), JsValue> {
let handle = file_handle(dir, name, true).await?;
let create_writable = method(&handle, "createWritable")?;
let writable = await_promise(create_writable.call0(&handle)?, "createWritable").await?;
let write = method(&writable, "write")?;
await_promise(
write.call1(&writable, &Uint8Array::from(data))?,
"writable.write",
)
.await?;
let close = method(&writable, "close")?;
await_promise(close.call0(&writable)?, "writable.close").await?;
Ok(())
}
pub(super) async fn read_whole_file(file_handle: &JsValue) -> Result<Vec<u8>, JsValue> {
let get_file = method(file_handle, "getFile")?;
let file = await_promise(get_file.call0(file_handle)?, "getFile").await?;
let array_buffer = method(&file, "arrayBuffer")?;
let buffer = await_promise(array_buffer.call0(&file)?, "arrayBuffer").await?;
Ok(Uint8Array::new(&buffer).to_vec())
}
pub(super) fn monotonic_ms() -> Option<f64> {
let global = js_sys::global();
let performance = Reflect::get(&global, &JsValue::from_str("performance")).ok()?;
if performance.is_undefined() || performance.is_null() {
return None;
}
let now = Reflect::get(&performance, &JsValue::from_str("now"))
.ok()?
.dyn_into::<Function>()
.ok()?;
now.call0(&performance).ok()?.as_f64()
}
pub(super) fn wall_clock_ms() -> f64 {
js_sys::Date::now()
}