Skip to main content

wry_bindgen/
batch.rs

1//! Batching system for grouping multiple JS operations into single messages.
2//!
3//! This module provides the batching infrastructure that allows multiple
4//! JS operations to be grouped together for efficient execution.
5
6use alloc::collections::{BTreeMap, BTreeSet};
7use alloc::vec::Vec;
8use core::any::Any;
9use core::cell::RefCell;
10use std::boxed::Box;
11
12use crate::encode::{BatchableResult, BinaryDecode};
13use crate::id_allocator::{BorrowIds, HeapIds, IdSlab, InstallIdBatch};
14use crate::ipc::DecodedData;
15use crate::ipc::{EncodedData, MessageType, OutboundIPCMessage};
16use crate::lazy::ThreadLocalKey;
17use crate::object_store::ObjectHandle;
18use crate::runtime::WryIPC;
19use crate::type_cache::TypeCache;
20use crate::value::JSIDX_RESERVED;
21
22/// One operation's deferred cleanup: heap slots and Rust object handles whose
23/// release was requested while the operation was encoding. Both are flushed
24/// together once the operation completes.
25#[derive(Default)]
26pub(crate) struct OperationFreeFrame {
27    /// Released heap slots to drop in JS and recycle at flush end.
28    heap_ids: Vec<u64>,
29    /// Rust object handles to remove from the store at flush end.
30    object_handles: Vec<u32>,
31}
32
33/// State for batching operations and object storage.
34/// Every evaluation is a batch - it may just have one operation.
35///
36/// Also stores exported Rust structs and callback functions.
37pub struct Runtime {
38    /// The encoder accumulating batched operations
39    encoder: EncodedData,
40    /// Heap IDs that mirror the JS runtime's reference slab.
41    heap_ids: HeapIds,
42    /// Borrow-stack IDs for borrowed references within an operation.
43    borrow_ids: BorrowIds,
44    /// Handles for Rust-owned exported objects. Rust controls allocation, so
45    /// handles are freed (released and recycled) immediately on removal.
46    object_handles: IdSlab<u32>,
47    /// Whether we're inside a batch() call
48    is_batching: bool,
49    /// Function-type definitions JS has been told about.
50    type_cache: TypeCache,
51    /// Exported Rust structs and callbacks stored by handle.
52    objects: BTreeMap<u32, Box<dyn Any>>,
53    /// Handles whose drop arrived while the object was checked out (taken out of
54    /// `objects` for a `with_object`/`with_object_mut` call). The drop is honored
55    /// when the checkout finishes instead of being lost.
56    pending_object_drops: BTreeSet<u32>,
57    /// Per-operation deferred cleanup frames. Each in-flight operation pushes
58    /// one frame; released heap IDs and object handles accumulate into the top
59    /// frame and are flushed when the operation completes.
60    op_free_stack: Vec<OperationFreeFrame>,
61    /// The ipc layer used to communicate with the JS runtime
62    ipc: WryIPC,
63    /// The id of the webview this is associated with
64    webview_id: u64,
65    /// Thread locals associated with the runtime
66    thread_locals: BTreeMap<ThreadLocalKey<'static>, Box<dyn Any>>,
67    /// How many JS→Rust callbacks (inbound Evaluates) are currently executing
68    /// on the stack. Zero means any outbound Evaluate is a fresh top-level call
69    /// from the app future; non-zero means it is a nested response inside a
70    /// callback and must travel back through the parked JS XHR.
71    inbound_evaluate_depth: u32,
72}
73
74impl Runtime {
75    pub(crate) fn new(ipc: WryIPC, webview_id: u64) -> Self {
76        let encoder = Self::new_encoder_for_evaluate();
77        Self {
78            encoder,
79            heap_ids: HeapIds::new(),
80            borrow_ids: BorrowIds::new(),
81            object_handles: IdSlab::new(1),
82            is_batching: false,
83            type_cache: TypeCache::new(),
84            // Object store starts empty
85            objects: BTreeMap::new(),
86            pending_object_drops: BTreeSet::new(),
87            op_free_stack: Vec::new(),
88            ipc,
89            webview_id,
90            thread_locals: BTreeMap::new(),
91            inbound_evaluate_depth: 0,
92        }
93    }
94
95    /// Mark that a JS→Rust callback (inbound Evaluate) has started executing.
96    pub(crate) fn enter_inbound_evaluate(&mut self) {
97        self.inbound_evaluate_depth += 1;
98    }
99
100    /// Mark that a JS→Rust callback has finished executing.
101    pub(crate) fn leave_inbound_evaluate(&mut self) {
102        self.inbound_evaluate_depth -= 1;
103    }
104
105    /// Whether we are currently executing inside a JS→Rust callback. When true,
106    /// outbound Evaluates are nested responses to the parked JS XHR rather than
107    /// fresh top-level calls.
108    fn in_inbound_evaluate(&self) -> bool {
109        self.inbound_evaluate_depth > 0
110    }
111
112    fn new_encoder_for_evaluate() -> EncodedData {
113        let mut encoder = EncodedData::new();
114        encoder.push_u8(MessageType::Evaluate as u8);
115        encoder
116    }
117
118    /// Record a JS-allocated heap ID from a response.
119    pub fn observe_js_heap_id(&mut self, id: u64) {
120        self.heap_ids.observe_js_heap_id(id);
121    }
122
123    /// Get the next heap ID for a return value placeholder.
124    pub fn get_next_placeholder_id(&mut self) -> u64 {
125        self.heap_ids.next_placeholder_id()
126    }
127
128    /// Allocate the next ID for a JS object sent without encoding an ID. The ID
129    /// joins the pending install batch shipped on the next Rust-to-JS message.
130    pub fn get_next_inbound_js_heap_id(&mut self) -> u64 {
131        self.heap_ids.next_inbound_js_heap_id()
132    }
133
134    /// Get the next borrow ID from the borrow stack (indices 1-127).
135    /// The borrow stack grows downward from JSIDX_OFFSET (128) toward 1.
136    /// Panics if the borrow stack overflows (more than 127 borrowed refs in one operation).
137    pub fn get_next_borrow_id(&mut self) -> u64 {
138        self.borrow_ids.next_borrow_id()
139    }
140
141    /// Push a borrow frame before a nested operation that may use borrowed refs.
142    /// This saves the current borrow stack pointer so we can restore it later.
143    pub fn push_borrow_frame(&mut self) {
144        self.borrow_ids.push_frame();
145    }
146
147    /// Pop a borrow frame after a nested operation completes.
148    /// This restores the borrow stack pointer to where it was before the nested operation.
149    pub fn pop_borrow_frame(&mut self) {
150        self.borrow_ids.pop_frame();
151    }
152
153    /// Track a heap ID as released and queue it for JS drop when appropriate.
154    /// Returns the ID when there is no open operation frame to batch it into,
155    /// signalling the caller to notify JS immediately.
156    pub fn release_heap_id(&mut self, id: u64) -> Option<u64> {
157        self.heap_ids.release_heap_slot(id);
158        match self.op_free_stack.last_mut() {
159            Some(frame) => {
160                frame.heap_ids.push(id);
161                None
162            }
163            None => Some(id),
164        }
165    }
166
167    pub fn recycle_heap_id(&mut self, id: u64) {
168        self.heap_ids.recycle_heap_id(id);
169    }
170
171    pub fn recycle_heap_id_if_released(&mut self, id: u64) -> bool {
172        self.heap_ids.recycle_heap_id_if_released(id)
173    }
174
175    pub fn defer_heap_id_recycle_until_flush(&mut self, id: u64) {
176        self.encoder.defer_heap_id_recycle_until_flush(id);
177    }
178
179    /// Take the message data and reset the batch for reuse.
180    /// Includes ID installation and placeholder reservation metadata at the start of the message.
181    pub(crate) fn take_message(&mut self) -> (OutboundIPCMessage, Vec<u64>) {
182        let reserved_ids = self.take_reserved_placeholder_ids();
183        let mut encoder = self.take_encoder();
184        let heap_ids_to_recycle_after_flush = encoder.take_heap_ids_to_recycle_after_flush();
185        (
186            self.finish_rust_to_js_message(encoder, Some(&reserved_ids)),
187            heap_ids_to_recycle_after_flush,
188        )
189    }
190
191    /// Add Rust-to-JS response metadata and turn the encoder into a response message.
192    pub(crate) fn finish_respond_message(&mut self, encoder: EncodedData) -> OutboundIPCMessage {
193        self.finish_rust_to_js_message(encoder, None)
194    }
195
196    fn finish_rust_to_js_message(
197        &mut self,
198        mut encoder: EncodedData,
199        reserved_ids: Option<&[u64]>,
200    ) -> OutboundIPCMessage {
201        let install_ids = self.take_pending_install_ids();
202        prepend_rust_to_js_prelude(&mut encoder, &install_ids, reserved_ids);
203        let pending_type_ids = encoder.take_pending_type_ids();
204        // Reserved-ids is only passed for outbound Evaluates; Responds pass
205        // None. Evaluates push a type-cache frame that the matching inbound JS
206        // Respond pops and acks. Responds have no such closing message, but JS
207        // processes a Respond synchronously before Rust runs again, so any types
208        // it introduced are already cached — ack them now rather than dropping
209        // them (otherwise they re-ship as TYPE_FULL on every later use).
210        if reserved_ids.is_some() {
211            self.type_cache.push_pending_frame(pending_type_ids);
212        } else {
213            self.type_cache.ack_type_ids(&pending_type_ids);
214        }
215        // Only Evaluates (reserved_ids is Some) can be top-level; they are
216        // top-level exactly when no callback is currently on the stack.
217        let top_level = reserved_ids.is_some() && !self.in_inbound_evaluate();
218        OutboundIPCMessage::new(crate::ipc::IPCMessage::new(encoder.to_bytes()), top_level)
219    }
220
221    pub(crate) fn is_empty(&self) -> bool {
222        // 12 bytes for offsets, 1 byte for message type, and one u32 header word.
223        self.encoder.byte_len() <= 17
224    }
225
226    pub(crate) fn push_operation_frame(&mut self) {
227        self.op_free_stack.push(OperationFreeFrame::default());
228    }
229
230    pub(crate) fn release_object_handle(&mut self, handle: ObjectHandle) -> Option<Box<dyn Any>> {
231        match self.op_free_stack.last_mut() {
232            Some(frame) => {
233                frame.object_handles.push(handle.raw());
234                None
235            }
236            None => self.remove_object_untyped(handle.raw()),
237        }
238    }
239
240    /// Pop the current operation's free frame. If a parent frame exists, the
241    /// released IDs and handles collapse into it (so nested ops flush with
242    /// their parent) and an empty frame is returned; otherwise the frame is
243    /// handed back for the caller to flush.
244    pub(crate) fn pop_operation_frame(&mut self) -> OperationFreeFrame {
245        let frame = self
246            .op_free_stack
247            .pop()
248            .expect("pop_operation_frame called with empty frame stack");
249
250        if let Some(parent) = self.op_free_stack.last_mut() {
251            parent.heap_ids.extend(frame.heap_ids);
252            parent.object_handles.extend(frame.object_handles);
253            OperationFreeFrame::default()
254        } else {
255            frame
256        }
257    }
258
259    pub(crate) fn set_batching(&mut self, batching: bool) {
260        self.is_batching = batching;
261    }
262
263    pub(crate) fn is_batching(&self) -> bool {
264        self.is_batching
265    }
266
267    /// Take the IDs JS should install for objects it sent to Rust.
268    pub(crate) fn take_pending_install_ids(&mut self) -> InstallIdBatch {
269        self.heap_ids.take_pending_install_ids()
270    }
271
272    /// Take IDs JS should reserve for pending Rust-to-JS return values.
273    pub(crate) fn take_reserved_placeholder_ids(&mut self) -> Vec<u64> {
274        self.heap_ids.take_reserved_placeholder_ids()
275    }
276
277    pub(crate) fn take_encoder(&mut self) -> EncodedData {
278        let next = Self::new_encoder_for_evaluate();
279        core::mem::replace(&mut self.encoder, next)
280    }
281
282    pub(crate) fn extend_encoder(&mut self, other: &EncodedData) {
283        // Manually extend to avoid adding an extra message type byte for the
284        // inner encoder.
285        self.encoder.u8_buf.extend_from_slice(&other.u8_buf[1..]);
286        self.encoder.u32_buf.extend_from_slice(&other.u32_buf);
287        self.encoder.u16_buf.extend_from_slice(&other.u16_buf);
288        self.encoder.str_buf.extend_from_slice(&other.str_buf);
289        self.encoder
290            .heap_ids_to_recycle_after_flush
291            .extend_from_slice(&other.heap_ids_to_recycle_after_flush);
292        self.encoder
293            .pending_type_ids
294            .extend_from_slice(&other.pending_type_ids);
295        self.encoder.needs_flush |= other.needs_flush;
296    }
297
298    /// Get or create a type ID for a function-type definition. The second
299    /// element is true if JS has already acked a `TYPE_FULL` for this ID.
300    pub(crate) fn get_or_create_type_id(&mut self, type_bytes: &[u8]) -> (u32, bool) {
301        self.type_cache.get_or_create_type_id(type_bytes)
302    }
303
304    /// Pop the top pending-ack frame and mark its type IDs as acked. Called
305    /// when an inbound JS Respond arrives.
306    pub(crate) fn pop_and_ack_type_cache_frame(&mut self) {
307        self.type_cache.pop_and_ack_pending_frame();
308    }
309
310    /// Insert an exported object and return its handle.
311    pub(crate) fn insert_object<T: 'static>(&mut self, obj: T) -> u32 {
312        let handle = self.object_handles.alloc();
313        self.objects.insert(handle, Box::new(obj));
314        handle
315    }
316
317    /// Get a thread-local variable.
318    pub(crate) fn take_thread_local<T: 'static>(&mut self, key: ThreadLocalKey<'static>) -> T {
319        *self
320            .thread_locals
321            .remove(&key)
322            .expect("thread local not found")
323            .downcast::<T>()
324            .expect("type mismatch")
325    }
326
327    /// Insert a thread-local variable.
328    pub(crate) fn insert_thread_local<T: 'static>(
329        &mut self,
330        key: ThreadLocalKey<'static>,
331        value: T,
332    ) {
333        self.thread_locals.insert(key, Box::new(value));
334    }
335
336    /// Check if a thread-local variable exists.
337    pub(crate) fn has_thread_local(&self, key: ThreadLocalKey<'static>) -> bool {
338        self.thread_locals.contains_key(&key)
339    }
340
341    pub(crate) fn get_object<T: 'static>(&self, handle: u32) -> &T {
342        let boxed = self.objects.get(&handle).expect("invalid handle");
343        boxed.downcast_ref::<T>().expect("type mismatch")
344    }
345
346    pub(crate) fn take_object<T: 'static>(&mut self, handle: u32) -> T {
347        let boxed = self.objects.remove(&handle).expect("invalid handle");
348        *boxed.downcast::<T>().expect("type mismatch")
349    }
350
351    pub(crate) fn reinsert_object<T: 'static>(&mut self, handle: u32, obj: T) {
352        // A drop (e.g. JS GC firing DROP_NATIVE_REF) may have arrived while this
353        // object was checked out. Honor it now instead of resurrecting the
354        // object: free the handle and let `obj` drop.
355        if self.pending_object_drops.remove(&handle) {
356            self.object_handles.free(handle);
357            drop(obj);
358            return;
359        }
360        assert!(
361            self.objects.insert(handle, Box::new(obj)).is_none(),
362            "object handle {handle} was reinserted while occupied"
363        );
364    }
365
366    /// Remove an exported object and return it.
367    pub(crate) fn remove_object<T: 'static>(&mut self, handle: u32) -> T {
368        let boxed = self.objects.remove(&handle).expect("invalid handle");
369        self.object_handles.free(handle);
370        *boxed.downcast::<T>().expect("type mismatch")
371    }
372
373    pub(crate) fn remove_object_untyped(&mut self, handle: u32) -> Option<Box<dyn Any>> {
374        let object = self.objects.remove(&handle);
375        if object.is_some() {
376            self.object_handles.free(handle);
377        } else if self.object_handles.contains(handle) {
378            // The handle is live but absent from the map, so the object is
379            // currently checked out by a `with_object`/`with_object_mut` call
380            // (e.g. a method that triggered this drop through a nested
381            // callback). Defer the drop until the checkout finishes; the
382            // matching `reinsert_object` honors it.
383            self.pending_object_drops.insert(handle);
384        }
385        object
386    }
387
388    /// Get a reference to the IPC layer.
389    pub(crate) fn ipc(&self) -> &WryIPC {
390        &self.ipc
391    }
392
393    /// Get the webview ID associated with this runtime.
394    pub(crate) fn webview_id(&self) -> u64 {
395        self.webview_id
396    }
397}
398
399fn push_id_list(buf: &mut Vec<u32>, ids: &[u64]) {
400    buf.push(ids.len() as u32);
401    for &id in ids {
402        buf.push((id & 0xFFFF_FFFF) as u32);
403        buf.push((id >> 32) as u32);
404    }
405}
406
407fn prepend_rust_to_js_prelude(
408    encoder: &mut EncodedData,
409    install_ids: &[u64],
410    reserved_ids: Option<&[u64]>,
411) {
412    let mut prelude = Vec::new();
413    // A single install id-list: empty (count 0) when the last inbound message
414    // carried no heap refs, otherwise the IDs for that one batch.
415    push_id_list(&mut prelude, install_ids);
416    if let Some(reserved_ids) = reserved_ids {
417        push_id_list(&mut prelude, reserved_ids);
418    }
419    // The message type lives in the u8 buffer, so the u32 buffer starts with
420    // the prelude at index 0 (no request_id word precedes it anymore).
421    encoder.insert_u32s(0, &prelude);
422}
423
424thread_local! {
425    /// Thread-local runtime state - always exists, reset after each flush
426    pub(crate) static RUNTIME: RefCell<Vec<Runtime>> = const { RefCell::new(Vec::new()) };
427}
428
429fn push_runtime(runtime: Runtime) {
430    RUNTIME.with(|state| {
431        state.borrow_mut().push(runtime);
432    });
433}
434
435fn pop_runtime() -> Runtime {
436    RUNTIME.with(|state| {
437        state
438            .borrow_mut()
439            .pop()
440            .expect("No runtime available to pop")
441    })
442}
443
444pub(crate) fn in_runtime<O>(runtime: Runtime, run: impl FnOnce() -> O) -> (Runtime, O) {
445    push_runtime(runtime);
446    let out = run();
447    let runtime = pop_runtime();
448    (runtime, out)
449}
450
451pub(crate) fn with_runtime<R>(f: impl FnOnce(&mut Runtime) -> R) -> R {
452    RUNTIME.with(|state| {
453        let mut state = state.borrow_mut();
454        f(state.last_mut().expect("No runtime available"))
455    })
456}
457
458/// Check if we're currently inside a batch() call
459pub fn is_batching() -> bool {
460    with_runtime(|state| state.is_batching())
461}
462
463/// Whether the runtime is unavailable — already dropped, inaccessible, or
464/// borrowed elsewhere. Callers treat all of these the same: skip the work.
465fn runtime_already_dropped() -> bool {
466    match RUNTIME.try_with(|state| {
467        state
468            .try_borrow()
469            .map(|runtime_stack| runtime_stack.is_empty())
470    }) {
471        Ok(Ok(value)) => value,
472        Ok(Err(_)) | Err(_) => true,
473    }
474}
475
476/// Queue a JS drop operation for a heap ID.
477/// This is called when a JsValue is dropped.
478pub(crate) fn queue_js_drop(id: u64) {
479    debug_assert!(
480        id >= JSIDX_RESERVED,
481        "Attempted to drop reserved JS heap ID {id}"
482    );
483
484    if runtime_already_dropped() {
485        return;
486    }
487
488    let id = match RUNTIME.try_with(|state| {
489        state.try_borrow_mut().ok().and_then(|mut runtime_stack| {
490            runtime_stack
491                .last_mut()
492                .map(|runtime| runtime.release_heap_id(id))
493        })
494    }) {
495        Ok(Some(id)) => id,
496        Ok(None) | Err(_) => return,
497    };
498    if let Some(id) = id {
499        crate::js_helpers::js_drop_heap_ref(id);
500        recycle_heap_id_after_js_drop(id);
501    }
502}
503
504/// Mark the RustFunction wrapper at this heap ID as disposed. The heap-ref
505/// release is the responsibility of the caller — typically `JsValue::drop`
506/// running immediately after this via field-drop glue on `ScopedClosure`.
507pub(crate) fn queue_js_dispose_rust_function(id: u64) {
508    debug_assert!(
509        id >= JSIDX_RESERVED,
510        "Attempted to dispose reserved JS heap ID {id}"
511    );
512
513    if runtime_already_dropped() {
514        return;
515    }
516
517    crate::js_helpers::js_dispose_rust_function(id);
518}
519
520fn recycle_heap_id_after_js_drop(id: u64) {
521    let _ = RUNTIME.try_with(|state| {
522        let Ok(mut runtime_stack) = state.try_borrow_mut() else {
523            return;
524        };
525        let Some(runtime) = runtime_stack.last_mut() else {
526            return;
527        };
528
529        if runtime.is_batching() {
530            runtime.defer_heap_id_recycle_until_flush(id);
531        } else {
532            runtime.recycle_heap_id(id);
533        }
534    });
535}
536
537/// Drop a Rust-owned object now, or after the current encoded JS operation
538/// finishes if that object is being passed to JS.
539pub(crate) fn queue_rust_object_drop(handle: ObjectHandle) {
540    let object = RUNTIME
541        .try_with(|state| {
542            state.try_borrow_mut().ok().and_then(|mut runtime_stack| {
543                runtime_stack
544                    .last_mut()
545                    .and_then(|runtime| runtime.release_object_handle(handle))
546            })
547        })
548        .unwrap_or_default();
549    drop(object);
550}
551
552/// Add an operation to the current batch.
553pub(crate) fn add_operation(
554    encoder: &mut EncodedData,
555    fn_id: u32,
556    add_args: impl FnOnce(&mut EncodedData),
557) {
558    encoder.push_u32(fn_id);
559    add_args(encoder);
560}
561
562/// Core function for executing JavaScript calls.
563///
564/// For each call:
565/// 1. Encode the current evaluate message into the current batch
566/// 2. If the return value is needed immediately, flush the batch and return the result
567/// 3. Otherwise get the pending result from BatchableResult
568pub(crate) fn run_js_sync<R: BatchableResult>(
569    fn_id: u32,
570    add_args: impl FnOnce(&mut EncodedData),
571) -> R {
572    // Step 1: Encode the operation into the batch and get placeholder for non-flush types
573    // We take the current encoder out of the thread-local state to avoid borrowing issues
574    // and then put it back after adding the operation. Drops or other calls may happen while
575    // we are encoding, but they should be queued after this operation.
576    let mut batch = with_runtime(|state| {
577        // Push a new operation into the batch
578        state.push_operation_frame();
579        state.take_encoder()
580    });
581    add_operation(&mut batch, fn_id, add_args);
582
583    // Check if any encoded argument requires immediate flush (e.g., stack-allocated callbacks)
584    let needs_flush = batch.needs_flush;
585
586    with_runtime(|state| {
587        let encoded_during_op = core::mem::replace(&mut state.encoder, batch);
588        state.extend_encoder(&encoded_during_op);
589    });
590
591    // Reserve placeholders before any flush so JS receives exact IDs to fill.
592    let mut placeholder = with_runtime(|state| R::try_placeholder(state));
593
594    // Must flush if: not batching, or if the operation requires immediate execution
595    // (e.g., stack-allocated callbacks that must be invoked before returning)
596    let result = if !is_batching() || needs_flush {
597        flush_and_then(move |mut data| {
598            let response = placeholder
599                .take()
600                .unwrap_or_else(|| R::decode(&mut data).expect("Failed to decode return value"));
601            assert!(
602                data.is_empty(),
603                "Extra data remaining after decoding response"
604            );
605            response
606        })
607    } else {
608        placeholder.unwrap_or_else(|| flush_and_return::<R>())
609    };
610
611    // After running, free any queued IDs and object handles for this operation
612    let frame = with_runtime(|state| state.pop_operation_frame());
613    for id in frame.heap_ids {
614        crate::js_helpers::js_drop_heap_ref(id);
615        recycle_heap_id_after_js_drop(id);
616    }
617    for handle in frame.object_handles {
618        let object = with_runtime(|state| state.remove_object_untyped(handle));
619        drop(object);
620    }
621
622    result
623}
624
625/// Flush the current batch and return the decoded result.
626pub(crate) fn flush_and_return<R: BinaryDecode>() -> R {
627    flush_and_then(|mut data| {
628        let response = R::decode(&mut data).expect("Failed to decode return value");
629        assert!(
630            data.is_empty(),
631            "Extra data remaining after decoding response"
632        );
633        response
634    })
635}
636
637pub(crate) fn flush_and_then<R>(mut then: impl for<'a> FnMut(DecodedData<'a>) -> R) -> R {
638    use crate::runtime::WryBindgenEvent;
639
640    let (batch_msg, heap_ids_to_recycle_after_flush) = with_runtime(|state| state.take_message());
641
642    // Send and wait for the matching Respond. Under strict ping-pong the next
643    // non-Evaluate inbound is necessarily the answer to this outbound.
644    with_runtime(|runtime| {
645        (runtime.ipc().proxy)(WryBindgenEvent::ipc(runtime.webview_id(), batch_msg))
646    });
647    let mut heap_ids_to_recycle_after_flush = Some(heap_ids_to_recycle_after_flush);
648    loop {
649        if let Some(result) = crate::runtime::progress_js_with(&mut then) {
650            recycle_heap_ids_after_flush(
651                heap_ids_to_recycle_after_flush
652                    .take()
653                    .expect("heap IDs should only be recycled once per flush"),
654            );
655            return result;
656        }
657    }
658}
659
660fn recycle_heap_ids_after_flush(ids: Vec<u64>) {
661    for id in ids {
662        with_runtime(|state| {
663            state.recycle_heap_id_if_released(id);
664        });
665    }
666}
667
668/// Execute operations inside a batch. Operations that return opaque types (like JsValue)
669/// will be batched and executed together. Operations that return non-opaque types will
670/// flush the batch to get the actual result.
671pub fn batch<R, F: FnOnce() -> R>(f: F) -> R {
672    let currently_batching = is_batching();
673    // Start batching
674    with_runtime(|state| state.set_batching(true));
675
676    // Execute the closure
677    let result = f();
678
679    if !currently_batching {
680        // Flush any remaining batched operations
681        force_flush();
682    }
683
684    // End batching
685    with_runtime(|state| state.set_batching(currently_batching));
686
687    result
688}
689
690/// Like `batch`, but async.
691pub fn batch_async<'a, R, F: core::future::Future<Output = R> + 'a>(
692    f: F,
693) -> impl core::future::Future<Output = R> + 'a {
694    let mut f = Box::pin(f);
695    std::future::poll_fn(move |ctx| batch(|| f.as_mut().poll(ctx)))
696}
697
698pub fn force_flush() {
699    let has_pending = with_runtime(|state| !state.is_empty());
700    if has_pending {
701        flush_and_return::<()>();
702    }
703}
704
705#[cfg(test)]
706mod take_encoder_tests {
707    use std::sync::Arc;
708
709    use super::*;
710    use crate::ipc::IPCMessage;
711    use crate::runtime::WryIPC;
712
713    fn test_runtime() -> Runtime {
714        let (ipc, _senders) = WryIPC::new(Arc::new(|_| {}));
715        Runtime::new(ipc, 0)
716    }
717
718    #[test]
719    fn take_encoder_yields_an_evaluate_message_with_no_request_id() {
720        let mut runtime = test_runtime();
721        assert!(runtime.is_empty());
722
723        let first = runtime.take_encoder();
724        let bytes = IPCMessage::new(first.to_bytes());
725        assert_eq!(bytes.ty().unwrap(), MessageType::Evaluate);
726        // The encoder holds only the single message-type byte — no per-message
727        // request ID lives on the wire anymore.
728        assert!(first.u32_buf.is_empty());
729    }
730
731    #[test]
732    fn object_drop_during_checkout_is_deferred_then_honored() {
733        use std::cell::Cell;
734        use std::rc::Rc;
735
736        struct DropFlag(Rc<Cell<bool>>);
737        impl Drop for DropFlag {
738            fn drop(&mut self) {
739                self.0.set(true);
740            }
741        }
742
743        let mut runtime = test_runtime();
744        let dropped = Rc::new(Cell::new(false));
745        let handle = runtime.insert_object(DropFlag(dropped.clone()));
746
747        // `with_object`/`with_object_mut` take the object out of the map for the
748        // duration of a method call, leaving the handle live but the slot empty.
749        let checked_out = runtime.take_object::<DropFlag>(handle);
750
751        // A drop arrives mid-call (e.g. JS GC fires DROP_NATIVE_REF during a
752        // nested callback). It must be deferred, not silently lost.
753        assert!(runtime.remove_object_untyped(handle).is_none());
754        assert!(
755            !dropped.get(),
756            "object must not be dropped while it is checked out"
757        );
758
759        // Finishing the checkout honors the deferred drop instead of
760        // resurrecting the object.
761        runtime.reinsert_object(handle, checked_out);
762        assert!(
763            dropped.get(),
764            "deferred drop must run once the checkout completes"
765        );
766
767        // The handle was freed, so the next allocation reuses it (no leak).
768        let reused = runtime.insert_object(DropFlag(Rc::new(Cell::new(false))));
769        assert_eq!(reused, handle);
770    }
771}