Skip to main content

maplibre_native_ffi/
runtime.rs

1use std::cell::{Cell, RefCell};
2use std::fmt;
3use std::marker::PhantomData;
4use std::rc::Rc;
5use std::time::Duration;
6
7use maplibre_core::AmbientCacheOperation;
8use maplibre_native_ffi_core as maplibre_core;
9use maplibre_native_ffi_sys as sys;
10
11use crate::events::{OfflineRegionDownloadState, OfflineRegionStatus, RuntimeEventBatch};
12use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle};
13use crate::resource::{HttpHeaderTransformState, ResourceProviderState, ResourceTransformState};
14use crate::{
15    Error, ErrorKind, HandleOperationError, OfflineOperationTakeError, ResourceProviderDecision,
16    Result, RuntimeEventMask,
17};
18#[cfg(test)]
19use crate::{LatLngBounds, MapHandle, MapOptions};
20
21pub use maplibre_core::runtime::{OfflineRegionDefinition, OfflineRegionInfo, RuntimeOptions};
22pub(crate) use maplibre_core::runtime::{
23    OfflineRegionDefinitionNativeExt, RuntimeOptionsNativeExt,
24};
25
26#[derive(Debug)]
27pub(crate) struct RuntimeState {
28    handle: ThreadAffineNativeHandle<sys::mln_runtime>,
29    resource_transform: RefCell<Option<Box<ResourceTransformState>>>,
30    http_header_transform: RefCell<Option<Box<HttpHeaderTransformState>>>,
31    resource_provider: RefCell<Option<Box<ResourceProviderState>>>,
32}
33
34impl RuntimeState {
35    fn new(native: sys::mln_runtime) -> Result<Self> {
36        // SAFETY: native came from successful mln_runtime_create and is paired
37        // with the matching runtime destroy function.
38        let handle = unsafe {
39            ThreadAffineNativeHandle::from_handle(native, sys::mln_runtime_destroy, "mln_runtime")
40        }?;
41        Ok(Self {
42            handle,
43            resource_transform: RefCell::new(None),
44            http_header_transform: RefCell::new(None),
45            resource_provider: RefCell::new(None),
46        })
47    }
48
49    pub(crate) fn native(&self) -> Result<sys::mln_runtime> {
50        self.handle
51            .live_handle()
52            .ok_or_else(|| closed_handle_error("RuntimeHandle"))
53    }
54
55    fn is_closed(&self) -> bool {
56        self.handle.is_closed()
57    }
58
59    fn close(&self) -> Result<()> {
60        self.handle.close()?;
61        self.resource_transform.borrow_mut().take();
62        self.http_header_transform.borrow_mut().take();
63        self.resource_provider.borrow_mut().take();
64        Ok(())
65    }
66
67    fn set_resource_provider<F>(&self, callback: F) -> Result<()>
68    where
69        F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
70            + Send
71            + Sync
72            + 'static,
73    {
74        let replacement = ResourceProviderState::new(callback);
75        let descriptor = replacement.descriptor();
76        self.install_resource_provider(replacement, descriptor)
77    }
78
79    /// Installs a provider descriptor whose callback is null, exercising the
80    /// native install-failure rollback path that public callers cannot reach.
81    #[cfg(test)]
82    fn set_resource_provider_with_rejected_descriptor_for_testing<F>(
83        &self,
84        callback: F,
85    ) -> Result<()>
86    where
87        F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
88            + Send
89            + Sync
90            + 'static,
91    {
92        let replacement = ResourceProviderState::new(callback);
93        let mut descriptor = replacement.descriptor();
94        descriptor.callback = None;
95        self.install_resource_provider(replacement, descriptor)
96    }
97
98    fn install_resource_provider(
99        &self,
100        replacement: Box<ResourceProviderState>,
101        descriptor: sys::mln_resource_provider,
102    ) -> Result<()> {
103        let runtime = self.native()?;
104
105        // SAFETY: runtime is live and descriptor's user_data points to the boxed
106        // replacement, kept alive on success. Native retires the previous
107        // provider before returning, so the state dropped below is unreachable.
108        maplibre_core::check(unsafe {
109            sys::mln_runtime_set_resource_provider(runtime, &descriptor)
110        })?;
111        self.resource_provider.borrow_mut().replace(replacement);
112        Ok(())
113    }
114
115    fn clear_resource_provider(&self) -> Result<()> {
116        let runtime = self.native()?;
117
118        // SAFETY: runtime is live. Native clear waits for in-flight provider
119        // callbacks before returning, so dropping Rust callback state below is safe.
120        maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_provider(runtime) })?;
121        self.resource_provider.borrow_mut().take();
122        Ok(())
123    }
124
125    fn set_resource_transform<F>(&self, callback: F) -> Result<()>
126    where
127        F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
128    {
129        let runtime = self.native()?;
130        let replacement = ResourceTransformState::new(callback);
131        let descriptor = replacement.descriptor();
132
133        // SAFETY: runtime is live and descriptor's user_data points to
134        // replacement, kept alive on success. On failure native preserves the
135        // previous transform and replacement is dropped below.
136        maplibre_core::check(unsafe {
137            sys::mln_runtime_set_resource_transform(runtime, &descriptor)
138        })?;
139        self.resource_transform.borrow_mut().replace(replacement);
140        Ok(())
141    }
142
143    fn clear_resource_transform(&self) -> Result<()> {
144        let runtime = self.native()?;
145
146        // SAFETY: runtime is live. Native clear waits for in-flight transform
147        // callbacks before returning, so dropping Rust callback state below is safe.
148        maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_transform(runtime) })?;
149        self.resource_transform.borrow_mut().take();
150        Ok(())
151    }
152
153    fn set_http_header_transform<F>(&self, callback: F) -> Result<()>
154    where
155        F: Fn(crate::HttpHeaderTransformRequest) -> Vec<crate::HttpHeader> + Send + Sync + 'static,
156    {
157        let runtime = self.native()?;
158        let replacement = HttpHeaderTransformState::new(callback);
159        let descriptor = replacement.descriptor();
160        // SAFETY: descriptor retains replacement through native registration.
161        maplibre_core::check(unsafe {
162            sys::mln_runtime_set_http_header_transform(runtime, &descriptor)
163        })?;
164        self.http_header_transform.borrow_mut().replace(replacement);
165        Ok(())
166    }
167
168    fn clear_http_header_transform(&self) -> Result<()> {
169        let runtime = self.native()?;
170        // SAFETY: native waits for in-flight callbacks before returning.
171        maplibre_core::check(unsafe { sys::mln_runtime_clear_http_header_transform(runtime) })?;
172        self.http_header_transform.borrow_mut().take();
173        Ok(())
174    }
175}
176
177/// Owner-thread runtime handle for MapLibre Native work and event draining.
178pub struct RuntimeHandle {
179    pub(crate) inner: Rc<RuntimeState>,
180}
181
182impl fmt::Debug for RuntimeHandle {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        f.debug_struct("RuntimeHandle")
185            .field("closed", &self.inner.is_closed())
186            .finish()
187    }
188}
189
190/// Owner-thread offline database operation token that must be taken or discarded.
191pub struct OfflineOperationHandle<T> {
192    runtime: Rc<RuntimeState>,
193    operation_id: sys::mln_offline_operation_id,
194    operation_kind: maplibre_core::OfflineOperationKind,
195    result_kind: maplibre_core::OfflineOperationResultKind,
196    live: Cell<bool>,
197    _result: PhantomData<fn() -> T>,
198    _thread_affine: PhantomData<Rc<()>>,
199}
200
201impl<T> fmt::Debug for OfflineOperationHandle<T> {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        f.debug_struct("OfflineOperationHandle")
204            .field("operation_id", &self.operation_id)
205            .field("operation_kind", &self.operation_kind)
206            .field("result_kind", &self.result_kind)
207            .field("live", &self.live.get())
208            .finish()
209    }
210}
211
212impl<T> OfflineOperationHandle<T> {
213    fn new(
214        runtime: Rc<RuntimeState>,
215        operation_id: sys::mln_offline_operation_id,
216        operation_kind: maplibre_core::OfflineOperationKind,
217        result_kind: maplibre_core::OfflineOperationResultKind,
218    ) -> Result<Self> {
219        if operation_id == 0 {
220            return Err(Error::invalid_argument(
221                "offline operation id must not be zero",
222            ));
223        }
224        Ok(Self {
225            runtime,
226            operation_id,
227            operation_kind,
228            result_kind,
229            live: Cell::new(true),
230            _result: PhantomData,
231            _thread_affine: PhantomData,
232        })
233    }
234
235    fn runtime_ptr(&self) -> Result<sys::mln_runtime> {
236        if !self.live.get() {
237            return Err(closed_handle_error("OfflineOperationHandle"));
238        }
239        self.runtime.native()
240    }
241
242    /// Retires this handle after native took or discarded the operation, which
243    /// also drops the operation's undrained completion event from the native
244    /// queue.
245    fn mark_consumed(&self) {
246        self.live.set(false);
247    }
248
249    /// Discards runtime-owned state for this offline operation.
250    #[allow(clippy::result_large_err)]
251    pub fn discard(self) -> std::result::Result<(), HandleOperationError<Self>> {
252        if !self.live.get() {
253            return Ok(());
254        }
255        let runtime = match self.runtime_ptr() {
256            Ok(runtime) => runtime,
257            Err(error) => return Err(HandleOperationError::new(error, self)),
258        };
259        let status =
260            unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
261        if let Err(error) = maplibre_core::check(status) {
262            return Err(HandleOperationError::new(error, self));
263        }
264        self.mark_consumed();
265        Ok(())
266    }
267}
268
269impl<T> Drop for OfflineOperationHandle<T> {
270    fn drop(&mut self) {
271        if !self.live.get() {
272            return;
273        }
274        if let Ok(runtime) = self.runtime.native() {
275            // SAFETY: Safe Rust keeps this !Send/!Sync handle on the runtime owner thread.
276            let status =
277                unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
278            if status == sys::MLN_STATUS_OK {
279                self.mark_consumed();
280            }
281        }
282    }
283}
284
285impl OfflineOperationHandle<OfflineRegionInfo> {
286    /// Takes a completed create/update operation result as copied region info.
287    #[allow(clippy::result_large_err)]
288    pub fn take(self) -> std::result::Result<OfflineRegionInfo, OfflineOperationTakeError<Self>> {
289        let runtime = match self.runtime_ptr() {
290            Ok(runtime) => runtime,
291            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
292        };
293        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_snapshot>::new();
294        let status = match self.operation_kind {
295            maplibre_core::OfflineOperationKind::RegionCreate => unsafe {
296                sys::mln_runtime_offline_region_create_take_result(
297                    runtime,
298                    self.operation_id,
299                    out.as_mut_ptr(),
300                )
301            },
302            maplibre_core::OfflineOperationKind::RegionUpdateMetadata => unsafe {
303                sys::mln_runtime_offline_region_update_metadata_take_result(
304                    runtime,
305                    self.operation_id,
306                    out.as_mut_ptr(),
307                )
308            },
309            _ => sys::MLN_STATUS_INVALID_STATE,
310        };
311        if let Err(error) = maplibre_core::check(status) {
312            return Err(OfflineOperationTakeError::retryable(error, self));
313        }
314        self.mark_consumed();
315        let snapshot = match out.into_live("mln_offline_region_snapshot") {
316            Ok(snapshot) => snapshot,
317            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
318        };
319        // SAFETY: On success, the C API returns an owned snapshot handle;
320        // core copies and releases it.
321        unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
322            .map_err(OfflineOperationTakeError::consumed)
323    }
324}
325
326impl OfflineOperationHandle<Option<OfflineRegionInfo>> {
327    /// Takes a completed get operation result as optional copied region info.
328    #[allow(clippy::result_large_err)]
329    pub fn take(
330        self,
331    ) -> std::result::Result<Option<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
332        let runtime = match self.runtime_ptr() {
333            Ok(runtime) => runtime,
334            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
335        };
336        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_snapshot>::new();
337        let mut found = false;
338        let status = unsafe {
339            sys::mln_runtime_offline_region_get_take_result(
340                runtime,
341                self.operation_id,
342                out.as_mut_ptr(),
343                &mut found,
344            )
345        };
346        if let Err(error) = maplibre_core::check(status) {
347            return Err(OfflineOperationTakeError::retryable(error, self));
348        }
349        self.mark_consumed();
350        if !found {
351            return Ok(None);
352        }
353        let snapshot = match out.into_live("mln_offline_region_snapshot") {
354            Ok(snapshot) => snapshot,
355            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
356        };
357        // SAFETY: When found is true, the C API returns an owned snapshot
358        // handle; core copies and releases it.
359        Ok(Some(
360            unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
361                .map_err(OfflineOperationTakeError::consumed)?,
362        ))
363    }
364}
365
366impl OfflineOperationHandle<Vec<OfflineRegionInfo>> {
367    /// Takes a completed list/merge operation result as copied region info.
368    #[allow(clippy::result_large_err)]
369    pub fn take(
370        self,
371    ) -> std::result::Result<Vec<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
372        let runtime = match self.runtime_ptr() {
373            Ok(runtime) => runtime,
374            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
375        };
376        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_list>::new();
377        let status = match self.operation_kind {
378            maplibre_core::OfflineOperationKind::RegionsList => unsafe {
379                sys::mln_runtime_offline_regions_list_take_result(
380                    runtime,
381                    self.operation_id,
382                    out.as_mut_ptr(),
383                )
384            },
385            maplibre_core::OfflineOperationKind::RegionsMergeDatabase => unsafe {
386                sys::mln_runtime_offline_regions_merge_database_take_result(
387                    runtime,
388                    self.operation_id,
389                    out.as_mut_ptr(),
390                )
391            },
392            _ => sys::MLN_STATUS_INVALID_STATE,
393        };
394        if let Err(error) = maplibre_core::check(status) {
395            return Err(OfflineOperationTakeError::retryable(error, self));
396        }
397        self.mark_consumed();
398        let list = match out.into_live("mln_offline_region_list") {
399            Ok(list) => list,
400            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
401        };
402        // SAFETY: On success, the C API returns an owned list handle; core
403        // copies and releases it.
404        unsafe { maplibre_core::runtime::copy_offline_region_list(list) }
405            .map_err(OfflineOperationTakeError::consumed)
406    }
407}
408
409impl OfflineOperationHandle<OfflineRegionStatus> {
410    /// Takes a completed status operation result as copied status data.
411    #[allow(clippy::result_large_err)]
412    pub fn take(self) -> std::result::Result<OfflineRegionStatus, HandleOperationError<Self>> {
413        let runtime = match self.runtime_ptr() {
414            Ok(runtime) => runtime,
415            Err(error) => return Err(HandleOperationError::new(error, self)),
416        };
417        let mut raw = maplibre_core::events::empty_offline_region_status_native();
418        let status = unsafe {
419            sys::mln_runtime_offline_region_get_status_take_result(
420                runtime,
421                self.operation_id,
422                &mut raw,
423            )
424        };
425        if let Err(error) = maplibre_core::check(status) {
426            return Err(HandleOperationError::new(error, self));
427        }
428        self.mark_consumed();
429        Ok(maplibre_core::events::offline_region_status_from_native(
430            raw,
431        ))
432    }
433}
434
435impl RuntimeHandle {
436    /// Creates a runtime on the current thread using explicit options.
437    pub fn with_options(options: &RuntimeOptions) -> Result<Self> {
438        maplibre_core::validate_abi_version()?;
439        let native_options = options.to_native()?;
440        let raw_options = native_options.to_raw();
441        Self::create_with_native_options_after_abi_validation(&raw_options)
442    }
443
444    #[cfg(test)]
445    fn create_with_native_options_after_abi_version_check_for_testing(
446        options: *const sys::mln_runtime_options,
447        actual_abi_version: u32,
448    ) -> Result<Self> {
449        maplibre_core::validate_abi_version_value(actual_abi_version)?;
450        Self::create_with_native_options_after_abi_validation(options)
451    }
452
453    fn create_with_native_options_after_abi_validation(
454        options: *const sys::mln_runtime_options,
455    ) -> Result<Self> {
456        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_runtime>::new();
457        // SAFETY: options is either null to request native defaults or points to
458        // a materialized mln_runtime_options value whose backing strings live
459        // for this call. out is a valid null-initialized out-pointer owned by
460        // this call.
461        maplibre_core::check(unsafe { sys::mln_runtime_create(options, out.as_mut_ptr()) })?;
462        let ptr = out_handle(out, "mln_runtime")?;
463
464        Ok(Self {
465            inner: Rc::new(RuntimeState::new(ptr)?),
466        })
467    }
468
469    /// Installs or replaces the runtime-scoped network resource provider.
470    ///
471    /// Native may invoke the closure from worker or network threads, so keep it
472    /// quick and call no map or runtime APIs from it. Return `PassThrough` to
473    /// let native networking handle the request, or `Handle` to complete or
474    /// release the provided `ResourceRequestHandle` inline or later.
475    ///
476    /// A successful replacement retires the previous provider before returning
477    /// and then releases its Rust state. Handles the previous provider already
478    /// took stay valid; complete and release each one as usual.
479    pub fn set_resource_provider<F>(&self, callback: F) -> Result<()>
480    where
481        F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
482            + Send
483            + Sync
484            + 'static,
485    {
486        self.inner.set_resource_provider(callback)
487    }
488
489    /// Clears the runtime-scoped network resource provider, sending later
490    /// requests to MapLibre's online file source. The clear waits for in-flight
491    /// provider callbacks before returning and then releases the Rust callback
492    /// state. Handles the provider already took stay valid.
493    pub fn clear_resource_provider(&self) -> Result<()> {
494        self.inner.clear_resource_provider()
495    }
496
497    /// Installs or replaces the runtime-scoped network URL transform.
498    ///
499    /// Native may invoke the closure from worker or network threads, so keep it
500    /// quick and call no MapLibre Native APIs from it. `Some(url)` replaces the
501    /// request URL; `None`, an empty string, or a panic keeps the original.
502    pub fn set_resource_transform<F>(&self, callback: F) -> Result<()>
503    where
504        F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
505    {
506        self.inner.set_resource_transform(callback)
507    }
508
509    /// Clears the runtime-scoped network URL transform. The clear waits for
510    /// in-flight transform callbacks before returning and then releases the
511    /// Rust callback state.
512    pub fn clear_resource_transform(&self) -> Result<()> {
513        self.inner.clear_resource_transform()
514    }
515
516    /// Installs or replaces the runtime-scoped outgoing HTTP header transform.
517    ///
518    /// Native invokes the closure synchronously on worker or network threads
519    /// after URL transformation. Returned headers are copied before the closure
520    /// returns. Panics, duplicate names, and invalid headers leave the request
521    /// unchanged.
522    pub fn set_http_header_transform<F>(&self, callback: F) -> Result<()>
523    where
524        F: Fn(crate::HttpHeaderTransformRequest) -> Vec<crate::HttpHeader> + Send + Sync + 'static,
525    {
526        self.inner.set_http_header_transform(callback)
527    }
528
529    /// Clears the runtime-scoped outgoing HTTP header transform.
530    pub fn clear_http_header_transform(&self) -> Result<()> {
531        self.inner.clear_http_header_transform()
532    }
533
534    fn start_operation<T>(
535        &self,
536        operation_id: sys::mln_offline_operation_id,
537        operation_kind: maplibre_core::OfflineOperationKind,
538        result_kind: maplibre_core::OfflineOperationResultKind,
539    ) -> Result<OfflineOperationHandle<T>> {
540        OfflineOperationHandle::new(
541            Rc::clone(&self.inner),
542            operation_id,
543            operation_kind,
544            result_kind,
545        )
546    }
547
548    /// Starts an ambient cache maintenance operation for this runtime.
549    pub fn start_ambient_cache_operation(
550        &self,
551        operation: AmbientCacheOperation,
552    ) -> Result<OfflineOperationHandle<()>> {
553        let runtime = self.inner.native()?;
554        let mut operation_id: sys::mln_offline_operation_id = 0;
555        maplibre_core::check(unsafe {
556            sys::mln_runtime_run_ambient_cache_operation_start(
557                runtime,
558                operation.to_native(),
559                &mut operation_id,
560            )
561        })?;
562        self.start_operation(
563            operation_id,
564            maplibre_core::OfflineOperationKind::AmbientCache,
565            maplibre_core::OfflineOperationResultKind::None,
566        )
567    }
568
569    /// Starts a change to this runtime's maximum ambient cache size.
570    ///
571    /// MapLibre evicts ambient resources to fit the new budget, so lowering it
572    /// discards cached resources. Offline regions are unaffected.
573    pub fn start_set_maximum_ambient_cache_size(
574        &self,
575        size: u64,
576    ) -> Result<OfflineOperationHandle<()>> {
577        let runtime = self.inner.native()?;
578        let mut operation_id: sys::mln_offline_operation_id = 0;
579        maplibre_core::check(unsafe {
580            sys::mln_runtime_set_maximum_ambient_cache_size_start(runtime, size, &mut operation_id)
581        })?;
582        self.start_operation(
583            operation_id,
584            maplibre_core::OfflineOperationKind::SetMaximumAmbientCacheSize,
585            maplibre_core::OfflineOperationResultKind::None,
586        )
587    }
588
589    /// Starts creating an offline region.
590    pub fn start_create_offline_region(
591        &self,
592        definition: &OfflineRegionDefinition,
593        metadata: &[u8],
594    ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
595        let runtime = self.inner.native()?;
596        let definition = definition.to_native()?;
597        let raw_definition = definition.to_raw();
598        let mut operation_id: sys::mln_offline_operation_id = 0;
599        // SAFETY: runtime is live. raw_definition points into definition-owned
600        // string and geometry storage, metadata storage is valid for this call.
601        maplibre_core::check(unsafe {
602            sys::mln_runtime_offline_region_create_start(
603                runtime,
604                &raw_definition,
605                maplibre_core::runtime::metadata_ptr(metadata),
606                metadata.len(),
607                &mut operation_id,
608            )
609        })?;
610        self.start_operation(
611            operation_id,
612            maplibre_core::OfflineOperationKind::RegionCreate,
613            maplibre_core::OfflineOperationResultKind::Region,
614        )
615    }
616
617    /// Starts getting an offline region snapshot by ID.
618    pub fn start_offline_region(
619        &self,
620        region_id: i64,
621    ) -> Result<OfflineOperationHandle<Option<OfflineRegionInfo>>> {
622        let runtime = self.inner.native()?;
623        let mut operation_id: sys::mln_offline_operation_id = 0;
624        // SAFETY: runtime is live and operation_id points to writable storage.
625        maplibre_core::check(unsafe {
626            sys::mln_runtime_offline_region_get_start(runtime, region_id, &mut operation_id)
627        })?;
628        self.start_operation(
629            operation_id,
630            maplibre_core::OfflineOperationKind::RegionGet,
631            maplibre_core::OfflineOperationResultKind::OptionalRegion,
632        )
633    }
634
635    /// Starts listing offline regions in this runtime's database.
636    pub fn start_offline_regions(&self) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
637        let runtime = self.inner.native()?;
638        let mut operation_id: sys::mln_offline_operation_id = 0;
639        // SAFETY: runtime is live and operation_id points to writable storage.
640        maplibre_core::check(unsafe {
641            sys::mln_runtime_offline_regions_list_start(runtime, &mut operation_id)
642        })?;
643        self.start_operation(
644            operation_id,
645            maplibre_core::OfflineOperationKind::RegionsList,
646            maplibre_core::OfflineOperationResultKind::RegionList,
647        )
648    }
649
650    /// Starts merging offline regions from another database path.
651    pub fn start_merge_offline_regions_database(
652        &self,
653        path: &str,
654    ) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
655        let runtime = self.inner.native()?;
656        let path = maplibre_core::string::c_string(path)?;
657        let mut operation_id: sys::mln_offline_operation_id = 0;
658        // SAFETY: runtime is live, path is NUL-terminated and valid for this
659        // call, and operation_id points to writable storage.
660        maplibre_core::check(unsafe {
661            sys::mln_runtime_offline_regions_merge_database_start(
662                runtime,
663                path.as_ptr(),
664                &mut operation_id,
665            )
666        })?;
667        self.start_operation(
668            operation_id,
669            maplibre_core::OfflineOperationKind::RegionsMergeDatabase,
670            maplibre_core::OfflineOperationResultKind::RegionList,
671        )
672    }
673
674    /// Starts updating opaque metadata for an offline region.
675    pub fn start_update_offline_region_metadata(
676        &self,
677        region_id: i64,
678        metadata: &[u8],
679    ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
680        let runtime = self.inner.native()?;
681        let mut operation_id: sys::mln_offline_operation_id = 0;
682        // SAFETY: runtime is live, metadata storage is valid for this call, and
683        // operation_id points to writable storage.
684        maplibre_core::check(unsafe {
685            sys::mln_runtime_offline_region_update_metadata_start(
686                runtime,
687                region_id,
688                maplibre_core::runtime::metadata_ptr(metadata),
689                metadata.len(),
690                &mut operation_id,
691            )
692        })?;
693        self.start_operation(
694            operation_id,
695            maplibre_core::OfflineOperationKind::RegionUpdateMetadata,
696            maplibre_core::OfflineOperationResultKind::Region,
697        )
698    }
699
700    /// Starts getting the current completed/download status for an offline region.
701    pub fn start_offline_region_status(
702        &self,
703        region_id: i64,
704    ) -> Result<OfflineOperationHandle<OfflineRegionStatus>> {
705        let runtime = self.inner.native()?;
706        let mut operation_id: sys::mln_offline_operation_id = 0;
707        // SAFETY: runtime is live and operation_id points to writable storage.
708        maplibre_core::check(unsafe {
709            sys::mln_runtime_offline_region_get_status_start(runtime, region_id, &mut operation_id)
710        })?;
711        self.start_operation(
712            operation_id,
713            maplibre_core::OfflineOperationKind::RegionGetStatus,
714            maplibre_core::OfflineOperationResultKind::RegionStatus,
715        )
716    }
717
718    /// Starts enabling or disabling runtime events for an offline region.
719    pub fn start_set_offline_region_observed(
720        &self,
721        region_id: i64,
722        observed: bool,
723    ) -> Result<OfflineOperationHandle<()>> {
724        let runtime = self.inner.native()?;
725        let mut operation_id: sys::mln_offline_operation_id = 0;
726        maplibre_core::check(unsafe {
727            sys::mln_runtime_offline_region_set_observed_start(
728                runtime,
729                region_id,
730                observed,
731                &mut operation_id,
732            )
733        })?;
734        self.start_operation(
735            operation_id,
736            maplibre_core::OfflineOperationKind::RegionSetObserved,
737            maplibre_core::OfflineOperationResultKind::None,
738        )
739    }
740
741    /// Starts setting an offline region's native download state.
742    pub fn start_set_offline_region_download_state(
743        &self,
744        region_id: i64,
745        state: OfflineRegionDownloadState,
746    ) -> Result<OfflineOperationHandle<()>> {
747        let runtime = self.inner.native()?;
748        let state = state.raw_for_set()?;
749        let mut operation_id: sys::mln_offline_operation_id = 0;
750        maplibre_core::check(unsafe {
751            sys::mln_runtime_offline_region_set_download_state_start(
752                runtime,
753                region_id,
754                state,
755                &mut operation_id,
756            )
757        })?;
758        self.start_operation(
759            operation_id,
760            maplibre_core::OfflineOperationKind::RegionSetDownloadState,
761            maplibre_core::OfflineOperationResultKind::None,
762        )
763    }
764
765    /// Starts invalidating cached resources for an offline region.
766    pub fn start_invalidate_offline_region(
767        &self,
768        region_id: i64,
769    ) -> Result<OfflineOperationHandle<()>> {
770        let runtime = self.inner.native()?;
771        let mut operation_id: sys::mln_offline_operation_id = 0;
772        maplibre_core::check(unsafe {
773            sys::mln_runtime_offline_region_invalidate_start(runtime, region_id, &mut operation_id)
774        })?;
775        self.start_operation(
776            operation_id,
777            maplibre_core::OfflineOperationKind::RegionInvalidate,
778            maplibre_core::OfflineOperationResultKind::None,
779        )
780    }
781
782    /// Starts deleting an offline region.
783    pub fn start_delete_offline_region(
784        &self,
785        region_id: i64,
786    ) -> Result<OfflineOperationHandle<()>> {
787        let runtime = self.inner.native()?;
788        let mut operation_id: sys::mln_offline_operation_id = 0;
789        maplibre_core::check(unsafe {
790            sys::mln_runtime_offline_region_delete_start(runtime, region_id, &mut operation_id)
791        })?;
792        self.start_operation(
793            operation_id,
794            maplibre_core::OfflineOperationKind::RegionDelete,
795            maplibre_core::OfflineOperationResultKind::None,
796        )
797    }
798
799    /// Advances this runtime: parks the owner thread when `timeout` allows it,
800    /// then drains the owner-thread task queues. Drain the queued runtime
801    /// events with [`Self::drain_events`] afterwards.
802    ///
803    /// `Some(Duration::ZERO)` drains and returns, a longer `Some` parks for up
804    /// to that long, and `None` parks until a wake arrives. The drain runs
805    /// every task queued when it begins plus every task those enqueue, so a
806    /// single call can span a full style parse.
807    ///
808    /// `budget` bounds the drain. `None` drains without a bound; a `Some`
809    /// value stops the drain at the first task boundary after that long,
810    /// measured from the start of the drain. The first queued task always
811    /// runs, so a bounded pump always makes progress, and tasks left behind
812    /// set the wake flag so the next pump returns without parking and
813    /// continues them. The budget bounds the task queues alone: expired timers
814    /// and ready file descriptors are serviced regardless, and a single task
815    /// runs to completion once started, so one long task can overrun the
816    /// budget.
817    ///
818    /// A parking call returns as soon as the runtime's wake flag is set, and
819    /// returns without parking while unread runtime events are queued. Timers
820    /// and ready file descriptors set the flag only when they queue
821    /// owner-thread work, so pass a bounded timeout to cap how long a call
822    /// waits.
823    ///
824    /// A non-zero timeout blocks the calling thread. Call it outside any lock
825    /// that a thread signalling a [`WakeSource`] takes, and outside native
826    /// callbacks.
827    pub fn pump(&self, timeout: Option<Duration>, budget: Option<Duration>) -> Result<()> {
828        let runtime = self.inner.native()?;
829        let timeout_ms = timeout.map_or(-1, |timeout| {
830            i64::try_from(timeout.as_millis()).unwrap_or(i64::MAX)
831        });
832        let budget_ms = budget.map_or(-1, |budget| {
833            i64::try_from(budget.as_millis()).unwrap_or(i64::MAX)
834        });
835        // SAFETY: runtime is a live runtime handle owned by this wrapper.
836        maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime, timeout_ms, budget_ms) })
837    }
838
839    /// Acquires a [`WakeSource`] that releases this runtime's parked owner
840    /// thread. The returned source is usable from any thread.
841    pub fn wake_source(&self) -> Result<WakeSource> {
842        let runtime = self.inner.native()?;
843        let mut out = maplibre_core::ptr::OutHandle::<sys::mln_wake_source>::new();
844        // SAFETY: runtime is a live runtime handle owned by this wrapper, and
845        // out is a valid null-initialized out-pointer owned by this call.
846        maplibre_core::check(unsafe {
847            sys::mln_runtime_wake_source_acquire(runtime, out.as_mut_ptr())
848        })?;
849        Ok(WakeSource {
850            handle: out_handle(out, "mln_wake_source")?,
851        })
852    }
853
854    /// Drains this runtime's queued events into one batch borrowed from
855    /// runtime-owned storage.
856    ///
857    /// `max_events` bounds the drain. Zero drains every queued event, and a
858    /// positive value drains at most that many and reports the rest through
859    /// [`RuntimeEventBatch::remaining`].
860    ///
861    /// The returned batch borrows this handle, so the next drain waits until
862    /// the batch is dropped, and an event read out of the batch borrows the
863    /// batch. Take [`crate::RuntimeEventRef::to_owned`] for a value that
864    /// outlives either.
865    ///
866    /// A drain invalidates the batch before it, which the mutable borrow turns
867    /// into a compile error:
868    ///
869    /// ```compile_fail,E0499
870    /// # use maplibre_native_ffi::{RuntimeHandle, RuntimeOptions};
871    /// let mut runtime = RuntimeHandle::with_options(&RuntimeOptions::default()).unwrap();
872    /// let batch = runtime.drain_events(0).unwrap();
873    /// let next = runtime.drain_events(0).unwrap();
874    /// let _ = batch.len();
875    /// ```
876    pub fn drain_events(&mut self, max_events: usize) -> Result<RuntimeEventBatch<'_>> {
877        let runtime = self.inner.native()?;
878        // SAFETY: The default constructor takes no arguments and fills the size
879        // field for this C ABI version.
880        let mut raw = unsafe { sys::mln_runtime_event_batch_default() };
881        // SAFETY: runtime is live, and raw points to writable batch storage
882        // whose size field this call reads.
883        maplibre_core::check(unsafe {
884            sys::mln_runtime_drain_events(runtime, max_events, &mut raw)
885        })?;
886        // SAFETY: raw came from a successful drain on this runtime, so its
887        // storage stays readable until the next drain, which the returned
888        // batch's borrow of this handle keeps out of the way.
889        Ok(unsafe { RuntimeEventBatch::new(raw) })
890    }
891
892    /// Selects which runtime-originated event types this runtime queues.
893    ///
894    /// A runtime reads the bits in
895    /// [`RuntimeEventMask::ALL_RUNTIME_EVENTS`](crate::RuntimeEventMask::ALL_RUNTIME_EVENTS),
896    /// so [`RuntimeEventMask::ALL`](crate::RuntimeEventMask::ALL) selects every
897    /// runtime-originated type. Narrowing gates later events and keeps queued
898    /// ones. A bit outside `ALL` is an invalid-argument error.
899    pub fn set_event_mask(&self, mask: RuntimeEventMask) -> Result<()> {
900        let runtime = self.inner.native()?;
901        // SAFETY: runtime is live. The C API validates unknown mask bits.
902        maplibre_core::check(unsafe { sys::mln_runtime_set_event_mask(runtime, mask.bits()) })
903    }
904
905    /// Reports which runtime-originated event types this runtime queues,
906    /// starting from the mask its creation options selected.
907    pub fn event_mask(&self) -> Result<RuntimeEventMask> {
908        let runtime = self.inner.native()?;
909        let mut raw = 0;
910        // SAFETY: runtime is live and out_mask points to writable u64 storage.
911        maplibre_core::check(unsafe { sys::mln_runtime_get_event_mask(runtime, &mut raw) })?;
912        Ok(RuntimeEventMask::from_bits_retain(raw))
913    }
914
915    /// Explicitly destroys the runtime. A failed destroy leaves the native
916    /// handle live so child handles can still close safely.
917    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
918        if self.inner.is_closed() {
919            return Ok(());
920        }
921        if Rc::strong_count(&self.inner) > 1 {
922            return Err(HandleOperationError::new(
923                Error::new(
924                    ErrorKind::InvalidState,
925                    None,
926                    "RuntimeHandle cannot close while child handles are live",
927                ),
928                self,
929            ));
930        }
931        self.inner
932            .close()
933            .map_err(|error| HandleOperationError::new(error, self))
934    }
935}
936
937/// Releases a runtime owner thread parked in [`RuntimeHandle::pump`].
938///
939/// Signalling and destruction are callable from any thread. Each source holds
940/// its own reference to the runtime's wake state, so it stays usable after the
941/// runtime closes, where signalling does nothing.
942#[derive(Debug)]
943pub struct WakeSource {
944    handle: sys::mln_wake_source,
945}
946
947impl WakeSource {
948    /// Sets the runtime's wake flag and releases the parked owner thread.
949    ///
950    /// A signal raised while the owner thread is running sets the wake flag,
951    /// so the next [`RuntimeHandle::pump`] returns without parking. Signalling
952    /// after the runtime closes succeeds and does nothing.
953    pub fn signal(&self) -> Result<()> {
954        // SAFETY: handle is a live wake source owned by this wrapper, and
955        // native accepts signals from any thread.
956        maplibre_core::check(unsafe { sys::mln_wake_source_signal(self.handle) })
957    }
958}
959
960impl Drop for WakeSource {
961    fn drop(&mut self) {
962        // SAFETY: handle is a live wake source this wrapper owns and destroys
963        // exactly once, and native accepts destruction from any thread.
964        unsafe { sys::mln_wake_source_destroy(self.handle) };
965    }
966}
967
968#[cfg(test)]
969mod tests {
970    // The fixture HTTP servers below are native-only; a browser build fetches
971    // from the servers the test runner hosts instead.
972    #[cfg(not(target_os = "emscripten"))]
973    use std::io::{Read, Write};
974    #[cfg(not(target_os = "emscripten"))]
975    use std::net::TcpListener;
976    use std::sync::atomic::{AtomicUsize, Ordering};
977    use std::sync::{Arc, Mutex};
978    use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
979
980    use super::*;
981    use crate::{
982        ErrorKind, OfflineOperationCompletedEvent, ResourceErrorReason, ResourceKind,
983        ResourceProviderDecision, ResourceResponse, RuntimeEvent, RuntimeEventPayload,
984        RuntimeEventSource, RuntimeEventType,
985    };
986    use maplibre_core::{OfflineOperationKind as Op, OfflineOperationResultKind as OpResult};
987
988    const PROVIDER_STYLE_JSON: &str = r#"{"version":8,"sources":{},"layers":[]}"#;
989
990    #[cfg(not(target_os = "emscripten"))]
991    fn spawn_style_server(
992        request_count: usize,
993    ) -> (
994        String,
995        std::sync::mpsc::Receiver<String>,
996        std::thread::JoinHandle<()>,
997    ) {
998        let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
999        let base_url = format!("http://{}", listener.local_addr().unwrap());
1000        let (sender, receiver) = std::sync::mpsc::channel();
1001        let handle = std::thread::spawn(move || {
1002            for _ in 0..request_count {
1003                let (mut stream, _) = listener.accept().unwrap();
1004                stream
1005                    .set_read_timeout(Some(Duration::from_secs(5)))
1006                    .unwrap();
1007                let mut request = [0; 4096];
1008                let bytes = stream.read(&mut request).unwrap();
1009                let request = String::from_utf8_lossy(&request[..bytes]);
1010                let path = request
1011                    .lines()
1012                    .next()
1013                    .and_then(|line| line.split_whitespace().nth(1))
1014                    .unwrap_or("")
1015                    .to_owned();
1016                sender.send(path).unwrap();
1017
1018                let body = PROVIDER_STYLE_JSON.as_bytes();
1019                write!(
1020                    stream,
1021                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1022                    body.len()
1023                )
1024                .unwrap();
1025                stream.write_all(body).unwrap();
1026            }
1027        });
1028        (base_url, receiver, handle)
1029    }
1030
1031    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
1032    fn spawn_recording_style_server(
1033        request_count: usize,
1034    ) -> (
1035        String,
1036        std::sync::mpsc::Receiver<String>,
1037        std::thread::JoinHandle<()>,
1038    ) {
1039        let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1040        let base_url = format!("http://{}", listener.local_addr().unwrap());
1041        let (sender, receiver) = std::sync::mpsc::channel();
1042        let handle = std::thread::spawn(move || {
1043            for _ in 0..request_count {
1044                let (mut stream, _) = listener.accept().unwrap();
1045                stream
1046                    .set_read_timeout(Some(Duration::from_secs(5)))
1047                    .unwrap();
1048                let mut bytes = [0; 4096];
1049                let count = stream.read(&mut bytes).unwrap();
1050                sender
1051                    .send(String::from_utf8_lossy(&bytes[..count]).into_owned())
1052                    .unwrap();
1053                let body = PROVIDER_STYLE_JSON.as_bytes();
1054                write!(
1055                    stream,
1056                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1057                    body.len()
1058                )
1059                .unwrap();
1060                stream.write_all(body).unwrap();
1061            }
1062        });
1063        (base_url, receiver, handle)
1064    }
1065
1066    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
1067    fn spawn_redirect_style_servers() -> (
1068        String,
1069        std::sync::mpsc::Receiver<(String, bool)>,
1070        Vec<std::thread::JoinHandle<()>>,
1071    ) {
1072        let origin = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1073        let destination = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1074        let origin_url = format!("http://{}", origin.local_addr().unwrap());
1075        let destination_url = format!("http://{}", destination.local_addr().unwrap());
1076        let (sender, receiver) = std::sync::mpsc::channel();
1077
1078        let origin_sender = sender.clone();
1079        let origin_destination_url = destination_url.clone();
1080        let origin_server = std::thread::spawn(move || {
1081            for _ in 0..3 {
1082                let (mut stream, _) = origin.accept().unwrap();
1083                stream
1084                    .set_read_timeout(Some(Duration::from_secs(5)))
1085                    .unwrap();
1086                let mut bytes = [0; 4096];
1087                let count = stream.read(&mut bytes).unwrap();
1088                let request = String::from_utf8_lossy(&bytes[..count]);
1089                let path = request
1090                    .lines()
1091                    .next()
1092                    .and_then(|line| line.split_whitespace().nth(1))
1093                    .unwrap_or("")
1094                    .to_owned();
1095                let has_header = request
1096                    .lines()
1097                    .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"));
1098                origin_sender.send((path.clone(), has_header)).unwrap();
1099
1100                if path == "/same-start.json" {
1101                    write!(
1102                        stream,
1103                        "HTTP/1.1 302 Found\r\nLocation: /same-final.json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1104                    )
1105                    .unwrap();
1106                } else if path == "/cross-start.json" {
1107                    write!(
1108                        stream,
1109                        "HTTP/1.1 302 Found\r\nLocation: {origin_destination_url}/cross-final.json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1110                    )
1111                    .unwrap();
1112                } else {
1113                    let body = PROVIDER_STYLE_JSON.as_bytes();
1114                    write!(
1115                        stream,
1116                        "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1117                        body.len()
1118                    )
1119                    .unwrap();
1120                    stream.write_all(body).unwrap();
1121                }
1122            }
1123        });
1124
1125        let destination_server = std::thread::spawn(move || {
1126            let (mut stream, _) = destination.accept().unwrap();
1127            stream
1128                .set_read_timeout(Some(Duration::from_secs(5)))
1129                .unwrap();
1130            let mut bytes = [0; 4096];
1131            let count = stream.read(&mut bytes).unwrap();
1132            let request = String::from_utf8_lossy(&bytes[..count]);
1133            let path = request
1134                .lines()
1135                .next()
1136                .and_then(|line| line.split_whitespace().nth(1))
1137                .unwrap_or("")
1138                .to_owned();
1139            let has_header = request
1140                .lines()
1141                .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"));
1142            sender.send((path, has_header)).unwrap();
1143            let body = PROVIDER_STYLE_JSON.as_bytes();
1144            write!(
1145                stream,
1146                "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1147                body.len()
1148            )
1149            .unwrap();
1150            stream.write_all(body).unwrap();
1151        });
1152
1153        (
1154            origin_url,
1155            receiver,
1156            vec![origin_server, destination_server],
1157        )
1158    }
1159
1160    fn wait_for_operation<T>(
1161        runtime: &mut RuntimeHandle,
1162        operation: &OfflineOperationHandle<T>,
1163        operation_kind: Op,
1164        result_kind: OpResult,
1165    ) -> Result<OfflineOperationCompletedEvent> {
1166        let deadline = Instant::now() + Duration::from_secs(30);
1167        loop {
1168            if Instant::now() >= deadline {
1169                return Err(Error::new(
1170                    ErrorKind::InvalidState,
1171                    None,
1172                    format!(
1173                        "timed out waiting for offline operation {:?}/{:?} with id {}",
1174                        operation_kind, result_kind, operation.operation_id
1175                    ),
1176                ));
1177            }
1178            runtime.pump(Some(Duration::ZERO), None)?;
1179            let mut outcome = None;
1180            for event in runtime.drain_events(0)?.iter() {
1181                let RuntimeEventPayload::OfflineOperationCompleted(completed) = event.payload()
1182                else {
1183                    continue;
1184                };
1185                if completed.operation_id != operation.operation_id {
1186                    continue;
1187                }
1188                assert_eq!(completed.operation_kind, operation_kind);
1189                assert_eq!(completed.raw_operation_kind, operation_kind.raw_value());
1190                assert_eq!(completed.result_kind, result_kind);
1191                assert_eq!(completed.raw_result_kind, result_kind.raw_value());
1192                outcome = Some((completed, event.message()?.unwrap_or_default().to_owned()));
1193                break;
1194            }
1195            if let Some((completed, message)) = outcome {
1196                if completed.result_status != sys::MLN_STATUS_OK {
1197                    return Err(Error::from_status_and_diagnostic(
1198                        completed.result_status,
1199                        message,
1200                    ));
1201                }
1202                return Ok(completed);
1203            }
1204            std::thread::sleep(Duration::from_millis(1));
1205        }
1206    }
1207
1208    #[test]
1209    // Spec coverage: BND-084.
1210    fn runtime_ambient_cache_operations_use_real_c_abi() {
1211        let base = TempDir::new("maplibre-rust-ambient-cache");
1212        let cache = base.path().join("ambient.db");
1213
1214        let mut options = RuntimeOptions::default();
1215        options.cache_path = Some(cache.to_string_lossy().into_owned());
1216        let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1217
1218        for operation in [
1219            AmbientCacheOperation::PackDatabase,
1220            AmbientCacheOperation::Invalidate,
1221            AmbientCacheOperation::Clear,
1222            AmbientCacheOperation::ResetDatabase,
1223        ] {
1224            let operation = runtime.start_ambient_cache_operation(operation).unwrap();
1225            let completed =
1226                wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None)
1227                    .unwrap();
1228            assert_eq!(completed.operation_id, operation.operation_id);
1229            operation.discard().unwrap();
1230        }
1231
1232        runtime.close().unwrap();
1233    }
1234
1235    #[test]
1236    // Spec coverage: BND-084.
1237    fn runtime_set_maximum_ambient_cache_size_reports_completion() {
1238        let base = TempDir::new("maplibre-rust-cache-size");
1239        let cache = base.path().join("ambient-size.db");
1240
1241        let mut options = RuntimeOptions::default();
1242        options.cache_path = Some(cache.to_string_lossy().into_owned());
1243        let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1244
1245        // Raising then lowering the budget both report through the same event.
1246        for size in [8 * 1024 * 1024, 0] {
1247            let operation = runtime.start_set_maximum_ambient_cache_size(size).unwrap();
1248            let completed = wait_for_operation(
1249                &mut runtime,
1250                &operation,
1251                Op::SetMaximumAmbientCacheSize,
1252                OpResult::None,
1253            )
1254            .unwrap();
1255            assert_eq!(completed.operation_id, operation.operation_id);
1256            operation.discard().unwrap();
1257        }
1258
1259        runtime.close().unwrap();
1260    }
1261
1262    #[test]
1263    // Spec coverage: BND-084.
1264    fn offline_take_result_failure_returns_live_handle() {
1265        let mut options = RuntimeOptions::default();
1266        options.cache_path = Some(":memory:".into());
1267        let runtime = RuntimeHandle::with_options(&options).unwrap();
1268        let ambient = runtime
1269            .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1270            .unwrap();
1271        let region_result = runtime
1272            .start_operation::<OfflineRegionInfo>(
1273                ambient.operation_id,
1274                maplibre_core::OfflineOperationKind::RegionCreate,
1275                maplibre_core::OfflineOperationResultKind::Region,
1276            )
1277            .unwrap();
1278
1279        let error = region_result.take().unwrap_err();
1280
1281        assert_eq!(error.kind(), ErrorKind::InvalidState);
1282        let region_result = error.into_retryable().unwrap().into_handle();
1283        region_result.discard().unwrap();
1284        drop(ambient);
1285        runtime.close().unwrap();
1286    }
1287
1288    #[test]
1289    // Spec coverage: BND-084 and BND-085.
1290    fn offline_region_apis_use_real_c_abi() {
1291        let mut options = RuntimeOptions::default();
1292        options.cache_path = Some(":memory:".into());
1293        let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1294        let definition = test_offline_region_definition("custom://offline-style.json");
1295
1296        let create = runtime
1297            .start_create_offline_region(&definition, b"abc")
1298            .unwrap();
1299        wait_for_operation(&mut runtime, &create, Op::RegionCreate, OpResult::Region).unwrap();
1300        let created = create.take().unwrap();
1301        assert_eq!(created.definition, definition);
1302        assert_eq!(created.metadata, b"abc");
1303
1304        let geometry_definition = OfflineRegionDefinition::GeometryRegion {
1305            style_url: "custom://offline-geometry-style.json".into(),
1306            geometry: br#"{"type":"Point","coordinates":[-122.5,37.5]}"#.to_vec(),
1307            min_zoom: 0.0,
1308            max_zoom: 1.0,
1309            pixel_ratio: 1.0,
1310            include_ideographs: false,
1311        };
1312        let create_geometry = runtime
1313            .start_create_offline_region(&geometry_definition, b"geo")
1314            .unwrap();
1315        wait_for_operation(
1316            &mut runtime,
1317            &create_geometry,
1318            Op::RegionCreate,
1319            OpResult::Region,
1320        )
1321        .unwrap();
1322        let geometry_region = create_geometry.take().unwrap();
1323        assert_eq!(geometry_region.definition, geometry_definition);
1324        assert_eq!(geometry_region.metadata, b"geo");
1325
1326        let get = runtime.start_offline_region(created.id).unwrap();
1327        wait_for_operation(&mut runtime, &get, Op::RegionGet, OpResult::OptionalRegion).unwrap();
1328        let fetched = get.take().unwrap().unwrap();
1329        assert_eq!(fetched, created);
1330
1331        let list = runtime.start_offline_regions().unwrap();
1332        wait_for_operation(&mut runtime, &list, Op::RegionsList, OpResult::RegionList).unwrap();
1333        let listed = list.take().unwrap();
1334        assert!(listed.iter().any(|region| region.id == created.id));
1335
1336        let update = runtime
1337            .start_update_offline_region_metadata(created.id, b"")
1338            .unwrap();
1339        wait_for_operation(
1340            &mut runtime,
1341            &update,
1342            Op::RegionUpdateMetadata,
1343            OpResult::Region,
1344        )
1345        .unwrap();
1346        let updated = update.take().unwrap();
1347        assert_eq!(updated.id, created.id);
1348        assert!(updated.metadata.is_empty());
1349
1350        let status_operation = runtime.start_offline_region_status(created.id).unwrap();
1351        wait_for_operation(
1352            &mut runtime,
1353            &status_operation,
1354            Op::RegionGetStatus,
1355            OpResult::RegionStatus,
1356        )
1357        .unwrap();
1358        let status = status_operation.take().unwrap();
1359        assert!(matches!(
1360            status.download_state,
1361            OfflineRegionDownloadState::Inactive | OfflineRegionDownloadState::Active
1362        ));
1363
1364        let set_inactive = runtime
1365            .start_set_offline_region_download_state(
1366                created.id,
1367                OfflineRegionDownloadState::Inactive,
1368            )
1369            .unwrap();
1370        wait_for_operation(
1371            &mut runtime,
1372            &set_inactive,
1373            Op::RegionSetDownloadState,
1374            OpResult::None,
1375        )
1376        .unwrap();
1377        set_inactive.discard().unwrap();
1378        let error = runtime
1379            .start_set_offline_region_download_state(
1380                created.id,
1381                OfflineRegionDownloadState::Unknown(99),
1382            )
1383            .unwrap_err();
1384        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1385
1386        let observe = runtime
1387            .start_set_offline_region_observed(created.id, true)
1388            .unwrap();
1389        wait_for_operation(
1390            &mut runtime,
1391            &observe,
1392            Op::RegionSetObserved,
1393            OpResult::None,
1394        )
1395        .unwrap();
1396        observe.discard().unwrap();
1397        let unobserve = runtime
1398            .start_set_offline_region_observed(created.id, false)
1399            .unwrap();
1400        wait_for_operation(
1401            &mut runtime,
1402            &unobserve,
1403            Op::RegionSetObserved,
1404            OpResult::None,
1405        )
1406        .unwrap();
1407        unobserve.discard().unwrap();
1408        let invalidate = runtime.start_invalidate_offline_region(created.id).unwrap();
1409        wait_for_operation(
1410            &mut runtime,
1411            &invalidate,
1412            Op::RegionInvalidate,
1413            OpResult::None,
1414        )
1415        .unwrap();
1416        invalidate.discard().unwrap();
1417        let delete = runtime.start_delete_offline_region(created.id).unwrap();
1418        wait_for_operation(&mut runtime, &delete, Op::RegionDelete, OpResult::None).unwrap();
1419        delete.discard().unwrap();
1420        let delete_geometry = runtime
1421            .start_delete_offline_region(geometry_region.id)
1422            .unwrap();
1423        wait_for_operation(
1424            &mut runtime,
1425            &delete_geometry,
1426            Op::RegionDelete,
1427            OpResult::None,
1428        )
1429        .unwrap();
1430        delete_geometry.discard().unwrap();
1431
1432        let missing_created = runtime.start_offline_region(created.id).unwrap();
1433        wait_for_operation(
1434            &mut runtime,
1435            &missing_created,
1436            Op::RegionGet,
1437            OpResult::OptionalRegion,
1438        )
1439        .unwrap();
1440        assert!(missing_created.take().unwrap().is_none());
1441        let missing_geometry = runtime.start_offline_region(geometry_region.id).unwrap();
1442        wait_for_operation(
1443            &mut runtime,
1444            &missing_geometry,
1445            Op::RegionGet,
1446            OpResult::OptionalRegion,
1447        )
1448        .unwrap();
1449        assert!(missing_geometry.take().unwrap().is_none());
1450
1451        runtime.close().unwrap();
1452    }
1453
1454    #[test]
1455    // Spec coverage: BND-084.
1456    fn offline_region_merge_database_uses_real_c_abi() {
1457        let base = TempDir::new("maplibre-rust-offline-merge");
1458        let main_cache = base.path().join("main.db");
1459        let side_cache = base.path().join("side.db");
1460
1461        let definition = test_offline_region_definition("custom://merge-style.json");
1462        {
1463            let mut side_options = RuntimeOptions::default();
1464            side_options.cache_path = Some(side_cache.to_string_lossy().into_owned());
1465            let mut side_runtime = RuntimeHandle::with_options(&side_options).unwrap();
1466            let create = side_runtime
1467                .start_create_offline_region(&definition, b"merge")
1468                .unwrap();
1469            wait_for_operation(
1470                &mut side_runtime,
1471                &create,
1472                Op::RegionCreate,
1473                OpResult::Region,
1474            )
1475            .unwrap();
1476            create.take().unwrap();
1477            side_runtime.close().unwrap();
1478        }
1479
1480        let mut main_options = RuntimeOptions::default();
1481        main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1482        let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1483        let merge = main_runtime
1484            .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1485            .unwrap();
1486        wait_for_operation(
1487            &mut main_runtime,
1488            &merge,
1489            Op::RegionsMergeDatabase,
1490            OpResult::RegionList,
1491        )
1492        .unwrap();
1493        let merged = merge.take().unwrap();
1494        assert_eq!(merged.len(), 1);
1495        assert_eq!(merged[0].definition, definition);
1496        assert_eq!(merged[0].metadata, b"merge");
1497        main_runtime.close().unwrap();
1498    }
1499
1500    fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1501        OfflineRegionDefinition::TilePyramid {
1502            style_url: style_url.into(),
1503            bounds: LatLngBounds::new(
1504                crate::LatLng::new(37.0, -123.0),
1505                crate::LatLng::new(38.0, -122.0),
1506            ),
1507            min_zoom: 0.0,
1508            max_zoom: 1.0,
1509            pixel_ratio: 1.0,
1510            include_ideographs: false,
1511        }
1512    }
1513
1514    struct TempDir {
1515        path: std::path::PathBuf,
1516    }
1517
1518    impl TempDir {
1519        fn new(prefix: &str) -> Self {
1520            let nanos = SystemTime::now()
1521                .duration_since(UNIX_EPOCH)
1522                .unwrap()
1523                .as_nanos();
1524            let path =
1525                std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1526            std::fs::create_dir_all(&path).unwrap();
1527            Self { path }
1528        }
1529
1530        fn path(&self) -> &std::path::Path {
1531            &self.path
1532        }
1533    }
1534
1535    impl Drop for TempDir {
1536        fn drop(&mut self) {
1537            let _ = std::fs::remove_dir_all(&self.path);
1538        }
1539    }
1540
1541    #[test]
1542    // Spec coverage: BND-040.
1543    fn runtime_create_with_explicit_options_uses_real_c_abi() {
1544        let mut options = RuntimeOptions::default();
1545        options.asset_path = Some(String::new());
1546        options.cache_path = Some(String::new());
1547        let runtime = RuntimeHandle::with_options(&options).unwrap();
1548
1549        runtime.pump(Some(Duration::ZERO), None).unwrap();
1550        runtime.close().unwrap();
1551    }
1552
1553    #[test]
1554    // Spec coverage: BND-001.
1555    fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1556        let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1557            std::ptr::null(),
1558            maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1559        )
1560        .unwrap_err();
1561
1562        assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1563        assert_eq!(error.raw_status(), None);
1564        assert!(
1565            error
1566                .diagnostic()
1567                .contains("unsupported MapLibre Native C ABI version")
1568        );
1569    }
1570
1571    fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1572        runtime
1573            .drain_events(0)
1574            .unwrap()
1575            .iter()
1576            .any(|event| event.event_type() == event_type)
1577    }
1578
1579    fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1580        for _ in 0..100 {
1581            let _ = runtime.pump(Some(Duration::ZERO), None);
1582            if drain_holds_event_type(runtime, event_type) {
1583                return true;
1584            }
1585            std::thread::sleep(Duration::from_millis(10));
1586        }
1587        false
1588    }
1589
1590    fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent {
1591        for _ in 0..100 {
1592            runtime.pump(Some(Duration::ZERO), None).unwrap();
1593            let mut failure = None;
1594            for event in runtime.drain_events(0).unwrap().iter() {
1595                if event.event_type() == RuntimeEventType::MapLoadingFailed {
1596                    failure = Some(event.to_owned().unwrap());
1597                    break;
1598                }
1599            }
1600            if let Some(failure) = failure {
1601                return failure;
1602            }
1603            std::thread::sleep(Duration::from_millis(10));
1604        }
1605        panic!("expected a map loading-failure event");
1606    }
1607
1608    #[test]
1609    // Spec coverage: BND-080.
1610    fn runtime_create_run_drain_and_close() {
1611        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1612
1613        runtime.pump(Some(Duration::ZERO), None).unwrap();
1614        // A fresh runtime with no map queues nothing.
1615        let batch = runtime.drain_events(0).unwrap();
1616        assert!(batch.is_empty());
1617        assert_eq!(batch.len(), 0);
1618        assert_eq!(batch.remaining(), 0);
1619        assert_eq!(batch.iter().count(), 0);
1620        // A second drain of an empty queue reports the same empty batch.
1621        assert!(runtime.drain_events(0).unwrap().is_empty());
1622        runtime.close().unwrap();
1623    }
1624
1625    // Pumps until the runtime is idle, so a park that follows is released by
1626    // the signal the test raises.
1627    fn quiesce(runtime: &mut RuntimeHandle) {
1628        for _ in 0..100 {
1629            runtime.pump(Some(Duration::ZERO), None).unwrap();
1630            if runtime.drain_events(0).unwrap().is_empty() {
1631                return;
1632            }
1633        }
1634        panic!("the runtime kept producing events while idle");
1635    }
1636
1637    // Drives the runtime the way a parked host does, so a missing wake shows up
1638    // as an expired timeout.
1639    fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1640        let started = Instant::now();
1641        for _ in 0..20 {
1642            runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1643            assert!(
1644                started.elapsed() < Duration::from_secs(5),
1645                "parks sat out their timeouts instead of taking wakes"
1646            );
1647            if drain_holds_event_type(runtime, event_type) {
1648                return true;
1649            }
1650        }
1651        false
1652    }
1653
1654    #[test]
1655    // Spec coverage: BND-088.
1656    fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() {
1657        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1658        runtime
1659            .set_resource_provider(move |request, handle| {
1660                if request.requested_url != "custom://style.json" {
1661                    return ResourceProviderDecision::PassThrough;
1662                }
1663                handle
1664                    .complete(ResourceResponse::ok(
1665                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1666                    ))
1667                    .unwrap();
1668                ResourceProviderDecision::PassThrough
1669            })
1670            .unwrap();
1671
1672        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1673        map.set_style_url("custom://style.json").unwrap();
1674        assert!(park_for_runtime_event(
1675            &mut runtime,
1676            RuntimeEventType::MapStyleLoaded
1677        ));
1678
1679        // A source moved to another thread matches a host's submission path,
1680        // and the park it releases has no other work to end it.
1681        let source = runtime.wake_source().unwrap();
1682        quiesce(&mut runtime);
1683        let signaller = std::thread::spawn(move || {
1684            std::thread::sleep(Duration::from_millis(20));
1685            source.signal().unwrap();
1686            source
1687        });
1688        let started = Instant::now();
1689        runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1690        assert!(
1691            started.elapsed() < Duration::from_secs(5),
1692            "the parked owner thread timed out instead of taking the signal"
1693        );
1694        let source = signaller.join().unwrap();
1695
1696        // A wake source stays usable after its runtime closes, so hosts tear
1697        // the two down in either order.
1698        map.close().unwrap();
1699        runtime.close().unwrap();
1700        source.signal().unwrap();
1701    }
1702
1703    #[test]
1704    // Spec coverage: BND-089.
1705    fn a_pump_clears_the_wake_flag_it_returns_on() {
1706        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1707        let source = runtime.wake_source().unwrap();
1708        quiesce(&mut runtime);
1709
1710        source.signal().unwrap();
1711        let started = Instant::now();
1712        runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1713        assert!(
1714            started.elapsed() < Duration::from_secs(5),
1715            "a pump waited even though the wake flag was set"
1716        );
1717
1718        // The pump above cleared the wake flag, so this one waits its full timeout.
1719        let started = Instant::now();
1720        runtime
1721            .pump(Some(Duration::from_millis(200)), None)
1722            .unwrap();
1723        assert!(
1724            started.elapsed() >= Duration::from_millis(100),
1725            "the first pump left the wake flag set"
1726        );
1727
1728        runtime.close().unwrap();
1729    }
1730
1731    #[test]
1732    // Spec coverage: BND-090.
1733    fn a_bounded_drain_reports_the_events_it_left_queued() {
1734        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1735        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1736        map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap();
1737        runtime.pump(Some(Duration::ZERO), None).unwrap();
1738
1739        let bounded = runtime.drain_events(1).unwrap();
1740        assert_eq!(bounded.len(), 1);
1741        assert!(
1742            bounded.remaining() > 0,
1743            "a style load should queue more than one event"
1744        );
1745        let rest = runtime.drain_events(0).unwrap();
1746        assert!(rest.len() > 1, "one drain should report the whole queue");
1747        assert_eq!(rest.remaining(), 0);
1748        let first = rest.iter().next().unwrap();
1749        assert_eq!(first.source(), RuntimeEventSource::Map(map.id()));
1750
1751        map.close().unwrap();
1752        runtime.close().unwrap();
1753    }
1754
1755    #[test]
1756    // Spec coverage: BND-060 and BND-091.
1757    fn a_creation_mask_narrows_a_runtime_before_its_first_operation() {
1758        let mut options = crate::RuntimeOptions::default();
1759        options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED;
1760        let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1761
1762        assert_eq!(
1763            runtime.event_mask().unwrap(),
1764            RuntimeEventMask::OFFLINE_OPERATION_COMPLETED
1765        );
1766
1767        // The narrowed runtime still reports the completion of the operation
1768        // this test starts, and the ambient cache operation needs no database.
1769        let operation = runtime
1770            .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1771            .unwrap();
1772        let completed =
1773            wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
1774        assert_eq!(completed.operation_id, operation.operation_id);
1775        operation.discard().unwrap();
1776
1777        runtime.close().unwrap();
1778
1779        // A creation mask carrying an undefined bit fails before the runtime
1780        // exists.
1781        options.event_mask = RuntimeEventMask::from_bits_retain(1 << 63);
1782        let error = RuntimeHandle::with_options(&options).unwrap_err();
1783        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1784    }
1785
1786    #[test]
1787    // Spec coverage: BND-091.
1788    fn a_runtime_mask_round_trips_and_rejects_undefined_bits() {
1789        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1790
1791        // The default options mask selects every type.
1792        assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1793
1794        runtime.set_event_mask(RuntimeEventMask::ALL).unwrap();
1795        assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1796
1797        // Read, clear one bit, write back: every other bit survives.
1798        let mut mask = runtime.event_mask().unwrap();
1799        mask.remove(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED);
1800        runtime.set_event_mask(mask).unwrap();
1801        let read_back = runtime.event_mask().unwrap();
1802        assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED));
1803        assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED));
1804        assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED));
1805
1806        let undefined = RuntimeEventMask::from_bits_retain(1 << 63);
1807        let error = runtime.set_event_mask(undefined).unwrap_err();
1808        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1809        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1810        assert_eq!(runtime.event_mask().unwrap(), read_back);
1811
1812        runtime.close().unwrap();
1813    }
1814
1815    #[test]
1816    // Spec coverage: BND-020, BND-022, BND-190, and BND-191.
1817    fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1818        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1819        let runtime_handle = runtime.inner.native().unwrap();
1820
1821        let error = std::thread::spawn(move || {
1822            // SAFETY: This intentionally exercises the C API's owner-thread
1823            // validation path with a live runtime handle from another thread.
1824            maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) })
1825                .unwrap_err()
1826        })
1827        .join()
1828        .unwrap();
1829
1830        assert_eq!(error.kind(), ErrorKind::WrongThread);
1831        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1832        assert!(!error.diagnostic().is_empty());
1833        runtime.close().unwrap();
1834    }
1835
1836    #[test]
1837    // Spec coverage: BND-123.
1838    fn resource_provider_installs_replaces_clears_and_releases_state() {
1839        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1840        let first = Arc::new(());
1841        let first_callback = Arc::clone(&first);
1842
1843        runtime
1844            .set_resource_provider(move |_, _| {
1845                let _ = &first_callback;
1846                crate::ResourceProviderDecision::PassThrough
1847            })
1848            .unwrap();
1849        assert_eq!(Arc::strong_count(&first), 2);
1850
1851        let second = Arc::new(());
1852        let second_callback = Arc::clone(&second);
1853        runtime
1854            .set_resource_provider(move |_, _| {
1855                let _ = &second_callback;
1856                crate::ResourceProviderDecision::PassThrough
1857            })
1858            .unwrap();
1859        assert_eq!(Arc::strong_count(&first), 1);
1860        assert_eq!(Arc::strong_count(&second), 2);
1861
1862        runtime.clear_resource_provider().unwrap();
1863        assert_eq!(Arc::strong_count(&second), 1);
1864
1865        let third = Arc::new(());
1866        let third_callback = Arc::clone(&third);
1867        runtime
1868            .set_resource_provider(move |_, _| {
1869                let _ = &third_callback;
1870                crate::ResourceProviderDecision::PassThrough
1871            })
1872            .unwrap();
1873        assert_eq!(Arc::strong_count(&third), 2);
1874
1875        runtime.close().unwrap();
1876        assert_eq!(Arc::strong_count(&third), 1);
1877    }
1878
1879    #[test]
1880    // Spec coverage: BND-122.
1881    fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1882        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1883        let first = Arc::new(());
1884        let first_callback = Arc::clone(&first);
1885        runtime
1886            .set_resource_provider(move |_, _| {
1887                let _ = &first_callback;
1888                crate::ResourceProviderDecision::PassThrough
1889            })
1890            .unwrap();
1891
1892        let second = Arc::new(());
1893        let second_callback = Arc::clone(&second);
1894        let error = runtime
1895            .inner
1896            .set_resource_provider_with_rejected_descriptor_for_testing(move |_, _| {
1897                let _ = &second_callback;
1898                crate::ResourceProviderDecision::PassThrough
1899            })
1900            .unwrap_err();
1901
1902        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1903        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1904        assert_eq!(Arc::strong_count(&first), 2);
1905        assert_eq!(Arc::strong_count(&second), 1);
1906
1907        runtime.close().unwrap();
1908        assert_eq!(Arc::strong_count(&first), 1);
1909    }
1910
1911    // Requests a style no file source serves, so the loading-failure event that
1912    // follows proves the request reached the network file source where the
1913    // runtime-scoped provider sits.
1914    fn load_probe_style(runtime: &mut RuntimeHandle, map: &MapHandle, style_url: &str) {
1915        map.set_style_url(style_url).unwrap();
1916        let event = wait_for_map_loading_failure(runtime);
1917        assert!(
1918            event
1919                .message
1920                .as_deref()
1921                .is_some_and(|message| message.contains("\"jar\""))
1922        );
1923    }
1924
1925    #[test]
1926    // Spec coverage: BND-142 and BND-154.
1927    fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() {
1928        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1929        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1930
1931        let first_calls = Arc::new(AtomicUsize::new(0));
1932        let first_callback_calls = Arc::clone(&first_calls);
1933        runtime
1934            .set_resource_provider(move |_, _| {
1935                first_callback_calls.fetch_add(1, Ordering::SeqCst);
1936                ResourceProviderDecision::PassThrough
1937            })
1938            .unwrap();
1939        load_probe_style(&mut runtime, &map, "jar:file:/packaged/first.json");
1940        assert!(first_calls.load(Ordering::SeqCst) > 0);
1941
1942        let second_calls = Arc::new(AtomicUsize::new(0));
1943        let second_callback_calls = Arc::clone(&second_calls);
1944        runtime
1945            .set_resource_provider(move |_, _| {
1946                second_callback_calls.fetch_add(1, Ordering::SeqCst);
1947                ResourceProviderDecision::PassThrough
1948            })
1949            .unwrap();
1950        let first_calls_after_replace = first_calls.load(Ordering::SeqCst);
1951        load_probe_style(&mut runtime, &map, "jar:file:/packaged/second.json");
1952        assert!(second_calls.load(Ordering::SeqCst) > 0);
1953        assert_eq!(
1954            first_calls.load(Ordering::SeqCst),
1955            first_calls_after_replace
1956        );
1957
1958        runtime.clear_resource_provider().unwrap();
1959        let second_calls_after_clear = second_calls.load(Ordering::SeqCst);
1960        load_probe_style(&mut runtime, &map, "jar:file:/packaged/third.json");
1961        assert_eq!(
1962            first_calls.load(Ordering::SeqCst),
1963            first_calls_after_replace
1964        );
1965        assert_eq!(
1966            second_calls.load(Ordering::SeqCst),
1967            second_calls_after_clear
1968        );
1969
1970        // Clearing an already cleared provider stays a successful no-op.
1971        runtime.clear_resource_provider().unwrap();
1972
1973        map.close().unwrap();
1974        runtime.close().unwrap();
1975    }
1976
1977    #[test]
1978    // Spec coverage: BND-143 and BND-150.
1979    fn resource_provider_completes_style_request_inline_through_c_abi() {
1980        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1981        let calls = Arc::new(AtomicUsize::new(0));
1982        let callback_calls = Arc::clone(&calls);
1983        runtime
1984            .set_resource_provider(move |request, handle| {
1985                if request.requested_url != "custom://style.json" {
1986                    return ResourceProviderDecision::PassThrough;
1987                }
1988                callback_calls.fetch_add(1, Ordering::SeqCst);
1989                assert_eq!(request.kind, ResourceKind::Style);
1990                handle
1991                    .complete(ResourceResponse::ok(
1992                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1993                    ))
1994                    .unwrap();
1995                ResourceProviderDecision::PassThrough
1996            })
1997            .unwrap();
1998
1999        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2000        map.set_style_url("custom://style.json").unwrap();
2001
2002        assert!(wait_for_runtime_event(
2003            &mut runtime,
2004            RuntimeEventType::MapStyleLoaded
2005        ));
2006        assert_eq!(calls.load(Ordering::SeqCst), 1);
2007        map.close().unwrap();
2008        runtime.close().unwrap();
2009    }
2010
2011    #[test]
2012    // Spec coverage: BND-155.
2013    fn resource_provider_sees_scheme_alias_and_its_resolved_url() {
2014        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2015        let resolved = Arc::new(Mutex::new(None));
2016        let callback_resolved = Arc::clone(&resolved);
2017        runtime
2018            .set_resource_provider(move |request, handle| {
2019                if request.requested_url != "maplibre://maps/style" {
2020                    return ResourceProviderDecision::PassThrough;
2021                }
2022                *callback_resolved.lock().unwrap() = Some(request.resolved_url.clone());
2023                handle
2024                    .complete(ResourceResponse::ok(
2025                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2026                    ))
2027                    .unwrap();
2028                ResourceProviderDecision::Handle
2029            })
2030            .unwrap();
2031
2032        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2033        map.set_style_url("maplibre://maps/style").unwrap();
2034
2035        assert!(wait_for_runtime_event(
2036            &mut runtime,
2037            RuntimeEventType::MapStyleLoaded
2038        ));
2039        assert_eq!(
2040            resolved.lock().unwrap().as_deref(),
2041            Some("https://demotiles.maplibre.org/style.json")
2042        );
2043        map.close().unwrap();
2044        runtime.close().unwrap();
2045    }
2046
2047    #[test]
2048    // Spec coverage: BND-144 and BND-145.
2049    fn resource_provider_completes_style_request_from_another_thread() {
2050        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2051        let (sender, receiver) = std::sync::mpsc::channel();
2052        runtime
2053            .set_resource_provider(move |request, handle| {
2054                if request.requested_url == "custom://async-style.json" {
2055                    sender.send(handle).unwrap();
2056                    ResourceProviderDecision::Handle
2057                } else {
2058                    ResourceProviderDecision::PassThrough
2059                }
2060            })
2061            .unwrap();
2062
2063        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2064        map.set_style_url("custom://async-style.json").unwrap();
2065        let handle = receiver
2066            .recv_timeout(Duration::from_secs(5))
2067            .expect("provider should send handled request");
2068        assert!(!handle.is_cancelled().unwrap());
2069        std::thread::spawn(move || {
2070            handle
2071                .complete(ResourceResponse::ok(
2072                    PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2073                ))
2074                .unwrap();
2075        })
2076        .join()
2077        .unwrap();
2078
2079        assert!(wait_for_runtime_event(
2080            &mut runtime,
2081            RuntimeEventType::MapStyleLoaded
2082        ));
2083        map.close().unwrap();
2084        runtime.close().unwrap();
2085    }
2086
2087    #[test]
2088    // Spec coverage: BND-149.
2089    fn resource_provider_error_response_becomes_copied_loading_failure_event() {
2090        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2091        runtime
2092            .set_resource_provider(move |request, handle| {
2093                if request.requested_url == "custom://broken-style.json" {
2094                    handle
2095                        .complete(ResourceResponse::error(
2096                            ResourceErrorReason::Other,
2097                            "provider failed",
2098                        ))
2099                        .unwrap();
2100                    ResourceProviderDecision::Handle
2101                } else {
2102                    ResourceProviderDecision::PassThrough
2103                }
2104            })
2105            .unwrap();
2106
2107        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2108        let map_id = map.id();
2109        map.set_style_url("custom://broken-style.json").unwrap();
2110
2111        let event = wait_for_map_loading_failure(&mut runtime);
2112        let copied_message = event.message.clone();
2113        // The copy stays intact after the drain that ends the batch's window.
2114        let _ = runtime.drain_events(0).unwrap();
2115
2116        assert_eq!(event.source, RuntimeEventSource::Map(map_id));
2117        assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
2118        assert_eq!(event.message, copied_message);
2119        assert!(
2120            event
2121                .message
2122                .as_deref()
2123                .is_some_and(|message| message.contains("provider failed"))
2124        );
2125
2126        map.close().unwrap();
2127        runtime.close().unwrap();
2128    }
2129
2130    #[test]
2131    // Spec coverage: BND-140 and BND-123.
2132    fn resource_transform_installs_replaces_clears_and_releases_state() {
2133        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2134        let first = Arc::new(());
2135        let first_callback = Arc::clone(&first);
2136
2137        runtime
2138            .set_resource_transform(move |request| {
2139                let _ = &first_callback;
2140                assert!(matches!(
2141                    request.kind,
2142                    ResourceKind::Style | ResourceKind::UnknownRaw(_)
2143                ));
2144                None
2145            })
2146            .unwrap();
2147        assert_eq!(Arc::strong_count(&first), 2);
2148
2149        let second = Arc::new(());
2150        let second_callback = Arc::clone(&second);
2151        runtime
2152            .set_resource_transform(move |_| {
2153                let _ = &second_callback;
2154                Some("https://example.test/replacement".to_owned())
2155            })
2156            .unwrap();
2157        assert_eq!(Arc::strong_count(&first), 1);
2158        assert_eq!(Arc::strong_count(&second), 2);
2159
2160        runtime.clear_resource_transform().unwrap();
2161        assert_eq!(Arc::strong_count(&second), 1);
2162        runtime.close().unwrap();
2163    }
2164
2165    /// The browser has no in-process TCP server, so the runner serves the two
2166    /// style documents and answers 404 for everything else. That makes the
2167    /// server the oracle: a rewrite that did not happen fails the style load
2168    /// rather than fetching an identical document, and the layer each document
2169    /// carries names which one was used. See scripts/run-browser-test.mjs.
2170    #[cfg(target_os = "emscripten")]
2171    #[test]
2172    // Spec coverage: BND-140.
2173    fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2174        let origin = std::env::var("MLN_FFI_TEST_FIXTURE_ORIGIN").expect(
2175            "MLN_FFI_TEST_FIXTURE_ORIGIN is unset; run the suite through \
2176             `mise run //bindings/rust:test emscripten-wasm32-webgl`",
2177        );
2178        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2179        let transform_url = format!("{origin}/__fixture/rewritten-style.json");
2180        // Matches the URL loaded after the clear as well, so a transform that
2181        // outlived the clear rewrites that request too and the layer id says so.
2182        runtime
2183            .set_resource_transform(move |request| {
2184                (request.url.ends_with("/original-style.json")
2185                    || request.url.ends_with("/original-after-clear.json"))
2186                .then(|| transform_url.clone())
2187            })
2188            .unwrap();
2189
2190        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2191        map.set_style_url(&format!("{origin}/__fixture/original-style.json"))
2192            .unwrap();
2193        assert!(wait_for_runtime_event(
2194            &mut runtime,
2195            RuntimeEventType::MapStyleLoaded
2196        ));
2197        // Contains rather than equals: MapLibre adds its own annotation layer to
2198        // every style it loads.
2199        assert!(
2200            map.style_layer_ids()
2201                .unwrap()
2202                .iter()
2203                .any(|id| id == "rewritten")
2204        );
2205
2206        runtime.clear_resource_transform().unwrap();
2207        map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json"))
2208            .unwrap();
2209        assert!(wait_for_runtime_event(
2210            &mut runtime,
2211            RuntimeEventType::MapStyleLoaded
2212        ));
2213        assert!(
2214            map.style_layer_ids()
2215                .unwrap()
2216                .iter()
2217                .any(|id| id == "original-after-clear")
2218        );
2219
2220        map.close().unwrap();
2221        runtime.close().unwrap();
2222    }
2223
2224    #[cfg(not(target_os = "emscripten"))]
2225    #[test]
2226    // Spec coverage: BND-140.
2227    fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2228        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2229        let (base_url, requests, server) = spawn_style_server(2);
2230        let transform_base_url = base_url.clone();
2231
2232        // Matches the URL loaded after the clear as well, so a transform that
2233        // outlived the clear rewrites that request too and the recorded path
2234        // says so.
2235        runtime
2236            .set_resource_transform(move |request| {
2237                if request.url.ends_with("/original-style.json")
2238                    || request.url.ends_with("/original-after-clear.json")
2239                {
2240                    Some(format!("{transform_base_url}/rewritten-style.json"))
2241                } else {
2242                    None
2243                }
2244            })
2245            .unwrap();
2246
2247        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2248        map.set_style_url(&format!("{base_url}/original-style.json"))
2249            .unwrap();
2250        assert!(wait_for_runtime_event(
2251            &mut runtime,
2252            RuntimeEventType::MapStyleLoaded
2253        ));
2254        assert_eq!(
2255            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2256            "/rewritten-style.json"
2257        );
2258
2259        runtime.clear_resource_transform().unwrap();
2260        map.set_style_url(&format!("{base_url}/original-after-clear.json"))
2261            .unwrap();
2262        assert!(wait_for_runtime_event(
2263            &mut runtime,
2264            RuntimeEventType::MapStyleLoaded
2265        ));
2266        assert_eq!(
2267            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2268            "/original-after-clear.json"
2269        );
2270
2271        map.close().unwrap();
2272        runtime.close().unwrap();
2273        server.join().unwrap();
2274    }
2275
2276    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2277    #[test]
2278    // Spec coverage: BND-158 and BND-159.
2279    fn http_header_transform_reaches_requests_and_clear_stops_it() {
2280        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2281        let (base_url, requests, server) = spawn_recording_style_server(2);
2282        runtime
2283            .set_http_header_transform(|request| {
2284                assert_eq!(request.kind, ResourceKind::Style);
2285                vec![crate::HttpHeader::new("X-Map-Token", "secret")]
2286            })
2287            .unwrap();
2288
2289        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2290        map.set_style_url(&format!("{base_url}/with-header.json"))
2291            .unwrap();
2292        assert!(wait_for_runtime_event(
2293            &mut runtime,
2294            RuntimeEventType::MapStyleLoaded
2295        ));
2296        let first = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2297        assert!(
2298            first
2299                .lines()
2300                .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"))
2301        );
2302
2303        runtime.clear_http_header_transform().unwrap();
2304        map.set_style_url(&format!("{base_url}/after-clear.json"))
2305            .unwrap();
2306        assert!(wait_for_runtime_event(
2307            &mut runtime,
2308            RuntimeEventType::MapStyleLoaded
2309        ));
2310        let second = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2311        assert!(
2312            !second
2313                .lines()
2314                .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:"))
2315        );
2316
2317        map.close().unwrap();
2318        runtime.close().unwrap();
2319        server.join().unwrap();
2320    }
2321
2322    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2323    #[test]
2324    // Spec coverage: BND-158.
2325    fn http_header_transform_skips_non_http_urls() {
2326        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2327        runtime.set_resource_transform(|_| None).unwrap();
2328        let calls = Arc::new(AtomicUsize::new(0));
2329        let callback_calls = Arc::clone(&calls);
2330        runtime
2331            .set_http_header_transform(move |_| {
2332                callback_calls.fetch_add(1, Ordering::SeqCst);
2333                Vec::new()
2334            })
2335            .unwrap();
2336        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2337
2338        map.set_style_url("jar:file:/packaged/style.json").unwrap();
2339        let _ = wait_for_map_loading_failure(&mut runtime);
2340        assert_eq!(calls.load(Ordering::SeqCst), 0);
2341
2342        map.close().unwrap();
2343        runtime.close().unwrap();
2344    }
2345
2346    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2347    #[test]
2348    // Spec coverage: BND-159.
2349    fn http_header_transform_preserves_same_origin_and_strips_cross_origin_redirects() {
2350        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2351        let (origin_url, requests, servers) = spawn_redirect_style_servers();
2352        runtime
2353            .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")])
2354            .unwrap();
2355        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2356
2357        map.set_style_url(&format!("{origin_url}/same-start.json"))
2358            .unwrap();
2359        assert!(wait_for_runtime_event(
2360            &mut runtime,
2361            RuntimeEventType::MapStyleLoaded
2362        ));
2363        assert_eq!(
2364            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2365            ("/same-start.json".to_owned(), true)
2366        );
2367        assert_eq!(
2368            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2369            ("/same-final.json".to_owned(), true)
2370        );
2371
2372        map.set_style_url(&format!("{origin_url}/cross-start.json"))
2373            .unwrap();
2374        assert!(wait_for_runtime_event(
2375            &mut runtime,
2376            RuntimeEventType::MapStyleLoaded
2377        ));
2378        assert_eq!(
2379            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2380            ("/cross-start.json".to_owned(), true)
2381        );
2382        assert_eq!(
2383            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2384            ("/cross-final.json".to_owned(), false)
2385        );
2386
2387        map.close().unwrap();
2388        runtime.close().unwrap();
2389        for server in servers {
2390            server.join().unwrap();
2391        }
2392    }
2393
2394    // OpenHarmony's platform HTTP client has no redirect-decision hook, and the
2395    // browser's fetch transport follows redirects itself, so both report header
2396    // transforms unsupported rather than enabling a transport that cannot
2397    // satisfy the redirect contract.
2398    #[cfg(any(target_env = "ohos", target_os = "emscripten"))]
2399    #[test]
2400    fn http_header_transform_reports_unsupported() {
2401        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2402        let error = runtime
2403            .set_http_header_transform(|_| Vec::new())
2404            .unwrap_err();
2405        assert_eq!(error.kind(), ErrorKind::Unsupported);
2406        runtime.close().unwrap();
2407    }
2408
2409    #[test]
2410    // Spec coverage: BND-123.
2411    fn resource_transform_replacement_after_map_creation_releases_previous_state() {
2412        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2413        let first = Arc::new(());
2414        let first_callback = Arc::clone(&first);
2415        runtime
2416            .set_resource_transform(move |_| {
2417                let _ = &first_callback;
2418                None
2419            })
2420            .unwrap();
2421        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2422
2423        let second = Arc::new(());
2424        let second_callback = Arc::clone(&second);
2425        runtime
2426            .set_resource_transform(move |_| {
2427                let _ = &second_callback;
2428                None
2429            })
2430            .unwrap();
2431
2432        assert_eq!(Arc::strong_count(&first), 1);
2433        assert_eq!(Arc::strong_count(&second), 2);
2434
2435        map.close().unwrap();
2436        runtime.close().unwrap();
2437        assert_eq!(Arc::strong_count(&second), 1);
2438    }
2439
2440    #[test]
2441    // Spec coverage: BND-123.
2442    fn runtime_teardown_releases_resource_transform_state() {
2443        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2444        let token = Arc::new(());
2445        let callback_token = Arc::clone(&token);
2446        runtime
2447            .set_resource_transform(move |_| {
2448                let _ = &callback_token;
2449                None
2450            })
2451            .unwrap();
2452        assert_eq!(Arc::strong_count(&token), 2);
2453
2454        runtime.close().unwrap();
2455
2456        assert_eq!(Arc::strong_count(&token), 1);
2457    }
2458
2459    #[test]
2460    // Rust regression: documents the Rust binding's late transform-install
2461    // guard after map creation.
2462    fn resource_transform_installs_after_map_creation() {
2463        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2464        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2465
2466        runtime.set_resource_transform(|_| None).unwrap();
2467
2468        map.close().unwrap();
2469        runtime.close().unwrap();
2470    }
2471
2472    #[test]
2473    // Spec coverage: BND-140 and BND-123.
2474    fn resource_transform_clears_after_map_was_closed_and_releases_state() {
2475        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2476        let token = Arc::new(());
2477        let callback_token = Arc::clone(&token);
2478        runtime
2479            .set_resource_transform(move |_| {
2480                let _ = &callback_token;
2481                None
2482            })
2483            .unwrap();
2484        assert_eq!(Arc::strong_count(&token), 2);
2485
2486        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2487        map.close().unwrap();
2488
2489        runtime.clear_resource_transform().unwrap();
2490
2491        assert_eq!(Arc::strong_count(&token), 1);
2492
2493        runtime.close().unwrap();
2494    }
2495
2496    #[test]
2497    // Spec coverage: BND-081, BND-090, and BND-092.
2498    fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() {
2499        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2500        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2501        let map_id = map.id();
2502
2503        let error = map.set_style_json(b"{").unwrap_err();
2504        assert_eq!(error.kind(), ErrorKind::NativeError);
2505        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
2506
2507        let batch = runtime.drain_events(0).unwrap();
2508        let types = batch
2509            .iter()
2510            .map(|event| event.event_type())
2511            .collect::<Vec<_>>();
2512        assert!(
2513            types.len() > 1,
2514            "a failed style load should queue more than one event, got {types:?}"
2515        );
2516        let loading_failed = batch
2517            .iter()
2518            .find(|event| event.event_type() == RuntimeEventType::MapLoadingFailed)
2519            .expect("a malformed style should queue a loading-failed event");
2520        assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id));
2521        assert!(
2522            loading_failed
2523                .message()
2524                .unwrap()
2525                .is_some_and(|message| !message.is_empty())
2526        );
2527        let owned = loading_failed.to_owned().unwrap();
2528
2529        // The next drain ends the previous batch's window; the copy is
2530        // untouched.
2531        assert!(runtime.drain_events(0).unwrap().is_empty());
2532        assert_eq!(owned.source, RuntimeEventSource::Map(map_id));
2533        assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed);
2534        assert!(
2535            owned
2536                .message
2537                .as_deref()
2538                .is_some_and(|message| !message.is_empty())
2539        );
2540
2541        map.close().unwrap();
2542        runtime.close().unwrap();
2543    }
2544
2545    #[test]
2546    // Spec coverage: BND-042.
2547    fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
2548        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2549        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2550
2551        let error = runtime.close().unwrap_err();
2552        assert_eq!(error.kind(), ErrorKind::InvalidState);
2553        assert_eq!(error.raw_status(), None);
2554        let runtime = error.into_handle();
2555
2556        runtime.pump(Some(Duration::ZERO), None).unwrap();
2557        map.close().unwrap();
2558        runtime.close().unwrap();
2559    }
2560}