///|
/// JS BytesFetcher using ReadableStream + AbortController.
///|
extern "js" fn js_stream_fetch(
url : String,
fetch_id : Int,
on_progress : (Int, Int, Int) -> Unit,
on_done : (Int, Int) -> Unit,
) -> Unit =
#| (url, id, onProgress, onDone) => {
#| if (!globalThis.__kagura_fetch) globalThis.__kagura_fetch = {};
#| const ctrl = new AbortController();
#| if (!globalThis.__kagura_fetch_ctrl) globalThis.__kagura_fetch_ctrl = {};
#| globalThis.__kagura_fetch_ctrl[id] = ctrl;
#| fetch(url, { signal: ctrl.signal })
#| .then(r => {
#| const total = parseInt(r.headers.get('content-length') || '-1', 10);
#| const reader = r.body.getReader();
#| const chunks = [];
#| let loaded = 0;
#| function pump() {
#| return reader.read().then(({ done, value }) => {
#| if (done) {
#| let size = 0;
#| for (const c of chunks) size += c.length;
#| const buf = new Uint8Array(size);
#| let off = 0;
#| for (const c of chunks) { buf.set(c, off); off += c.length; }
#| globalThis.__kagura_fetch[id] = buf;
#| onDone(id, size);
#| return;
#| }
#| chunks.push(value);
#| loaded += value.length;
#| onProgress(id, loaded, total);
#| return pump();
#| });
#| }
#| return pump();
#| })
#| .catch(() => { onDone(id, -1); });
#| }
///|
extern "js" fn js_stream_fetch_cancel(fetch_id : Int) -> Unit =
#| (id) => {
#| if (globalThis.__kagura_fetch_ctrl && globalThis.__kagura_fetch_ctrl[id]) {
#| globalThis.__kagura_fetch_ctrl[id].abort();
#| delete globalThis.__kagura_fetch_ctrl[id];
#| }
#| if (globalThis.__kagura_fetch && globalThis.__kagura_fetch[id]) {
#| delete globalThis.__kagura_fetch[id];
#| }
#| }
///|
extern "js" fn js_fetch_buf_copy(
fetch_id : Int,
dst : FixedArray[Byte],
len : Int,
) -> Unit =
#| (id, dst, len) => {
#| const src = globalThis.__kagura_fetch[id];
#| for (let i = 0; i < len; i++) dst[i] = src[i];
#| delete globalThis.__kagura_fetch[id];
#| }
///|
pub struct JsFetcher {
mut next_id : Int
}
///|
pub fn new_js_fetcher() -> JsFetcher {
{ next_id: 1 }
}
///|
pub impl BytesFetcher for JsFetcher with fetch(
self,
url,
on_progress,
on_complete,
) {
let id = self.next_id
self.next_id = self.next_id + 1
js_stream_fetch(
url,
id,
fn(_, loaded, total) { on_progress({ loaded, total }) },
fn(fid, len) {
if len < 0 {
on_complete(None)
return
}
let buf = FixedArray::make(len, b'\x00')
js_fetch_buf_copy(fid, buf, len)
on_complete(Some(Bytes::from_array(buf)))
},
)
{ id, }
}
///|
pub impl BytesFetcher for JsFetcher with cancel(_, handle) {
js_stream_fetch_cancel(handle.id)
}
///|
pub impl BytesFetcher for JsFetcher with poll(_) {
// no-op on JS — callbacks fire via microtask queue
}