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_accepts_read_only_source_through_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        let side_database_before = std::fs::read(&side_cache).unwrap();
1480
1481        #[cfg(unix)]
1482        {
1483            use std::os::unix::fs::PermissionsExt;
1484
1485            let mut permissions = std::fs::metadata(&side_cache).unwrap().permissions();
1486            permissions.set_mode(0o444);
1487            std::fs::set_permissions(&side_cache, permissions).unwrap();
1488        }
1489
1490        let mut main_options = RuntimeOptions::default();
1491        main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1492        let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1493        let merge = main_runtime
1494            .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1495            .unwrap();
1496        wait_for_operation(
1497            &mut main_runtime,
1498            &merge,
1499            Op::RegionsMergeDatabase,
1500            OpResult::RegionList,
1501        )
1502        .unwrap();
1503        let merged = merge.take().unwrap();
1504        assert_eq!(merged.len(), 1);
1505        assert_eq!(merged[0].definition, definition);
1506        assert_eq!(merged[0].metadata, b"merge");
1507        assert_eq!(std::fs::read(&side_cache).unwrap(), side_database_before);
1508        main_runtime.close().unwrap();
1509    }
1510
1511    fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1512        OfflineRegionDefinition::TilePyramid {
1513            style_url: style_url.into(),
1514            bounds: LatLngBounds::new(
1515                crate::LatLng::new(37.0, -123.0),
1516                crate::LatLng::new(38.0, -122.0),
1517            ),
1518            min_zoom: 0.0,
1519            max_zoom: 1.0,
1520            pixel_ratio: 1.0,
1521            include_ideographs: false,
1522        }
1523    }
1524
1525    struct TempDir {
1526        path: std::path::PathBuf,
1527    }
1528
1529    impl TempDir {
1530        fn new(prefix: &str) -> Self {
1531            let nanos = SystemTime::now()
1532                .duration_since(UNIX_EPOCH)
1533                .unwrap()
1534                .as_nanos();
1535            let path =
1536                std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1537            std::fs::create_dir_all(&path).unwrap();
1538            Self { path }
1539        }
1540
1541        fn path(&self) -> &std::path::Path {
1542            &self.path
1543        }
1544    }
1545
1546    impl Drop for TempDir {
1547        fn drop(&mut self) {
1548            let _ = std::fs::remove_dir_all(&self.path);
1549        }
1550    }
1551
1552    #[test]
1553    // Spec coverage: BND-040.
1554    fn runtime_create_with_explicit_options_uses_real_c_abi() {
1555        let mut options = RuntimeOptions::default();
1556        options.asset_path = Some(String::new());
1557        options.cache_path = Some(String::new());
1558        let runtime = RuntimeHandle::with_options(&options).unwrap();
1559
1560        runtime.pump(Some(Duration::ZERO), None).unwrap();
1561        runtime.close().unwrap();
1562    }
1563
1564    #[test]
1565    // Spec coverage: BND-001.
1566    fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1567        let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1568            std::ptr::null(),
1569            maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1570        )
1571        .unwrap_err();
1572
1573        assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1574        assert_eq!(error.raw_status(), None);
1575        assert!(
1576            error
1577                .diagnostic()
1578                .contains("unsupported MapLibre Native C ABI version")
1579        );
1580    }
1581
1582    fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1583        runtime
1584            .drain_events(0)
1585            .unwrap()
1586            .iter()
1587            .any(|event| event.event_type() == event_type)
1588    }
1589
1590    fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1591        for _ in 0..100 {
1592            let _ = runtime.pump(Some(Duration::ZERO), None);
1593            if drain_holds_event_type(runtime, event_type) {
1594                return true;
1595            }
1596            std::thread::sleep(Duration::from_millis(10));
1597        }
1598        false
1599    }
1600
1601    fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent {
1602        for _ in 0..100 {
1603            runtime.pump(Some(Duration::ZERO), None).unwrap();
1604            let mut failure = None;
1605            for event in runtime.drain_events(0).unwrap().iter() {
1606                if event.event_type() == RuntimeEventType::MapLoadingFailed {
1607                    failure = Some(event.to_owned().unwrap());
1608                    break;
1609                }
1610            }
1611            if let Some(failure) = failure {
1612                return failure;
1613            }
1614            std::thread::sleep(Duration::from_millis(10));
1615        }
1616        panic!("expected a map loading-failure event");
1617    }
1618
1619    #[test]
1620    // Spec coverage: BND-080.
1621    fn runtime_create_run_drain_and_close() {
1622        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1623
1624        runtime.pump(Some(Duration::ZERO), None).unwrap();
1625        // A fresh runtime with no map queues nothing.
1626        let batch = runtime.drain_events(0).unwrap();
1627        assert!(batch.is_empty());
1628        assert_eq!(batch.len(), 0);
1629        assert_eq!(batch.remaining(), 0);
1630        assert_eq!(batch.iter().count(), 0);
1631        // A second drain of an empty queue reports the same empty batch.
1632        assert!(runtime.drain_events(0).unwrap().is_empty());
1633        runtime.close().unwrap();
1634    }
1635
1636    // Pumps until the runtime is idle, so a park that follows is released by
1637    // the signal the test raises.
1638    fn quiesce(runtime: &mut RuntimeHandle) {
1639        for _ in 0..100 {
1640            runtime.pump(Some(Duration::ZERO), None).unwrap();
1641            if runtime.drain_events(0).unwrap().is_empty() {
1642                return;
1643            }
1644        }
1645        panic!("the runtime kept producing events while idle");
1646    }
1647
1648    // Drives the runtime the way a parked host does, so a missing wake shows up
1649    // as an expired timeout.
1650    fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1651        let started = Instant::now();
1652        for _ in 0..20 {
1653            runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1654            assert!(
1655                started.elapsed() < Duration::from_secs(5),
1656                "parks sat out their timeouts instead of taking wakes"
1657            );
1658            if drain_holds_event_type(runtime, event_type) {
1659                return true;
1660            }
1661        }
1662        false
1663    }
1664
1665    #[test]
1666    // Spec coverage: BND-088.
1667    fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() {
1668        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1669        runtime
1670            .set_resource_provider(move |request, handle| {
1671                if request.requested_url != "custom://style.json" {
1672                    return ResourceProviderDecision::PassThrough;
1673                }
1674                handle
1675                    .complete(ResourceResponse::ok(
1676                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1677                    ))
1678                    .unwrap();
1679                ResourceProviderDecision::PassThrough
1680            })
1681            .unwrap();
1682
1683        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1684        map.set_style_url("custom://style.json").unwrap();
1685        assert!(park_for_runtime_event(
1686            &mut runtime,
1687            RuntimeEventType::MapStyleLoaded
1688        ));
1689
1690        // A source moved to another thread matches a host's submission path,
1691        // and the park it releases has no other work to end it.
1692        let source = runtime.wake_source().unwrap();
1693        quiesce(&mut runtime);
1694        let signaller = std::thread::spawn(move || {
1695            std::thread::sleep(Duration::from_millis(20));
1696            source.signal().unwrap();
1697            source
1698        });
1699        let started = Instant::now();
1700        runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1701        assert!(
1702            started.elapsed() < Duration::from_secs(5),
1703            "the parked owner thread timed out instead of taking the signal"
1704        );
1705        let source = signaller.join().unwrap();
1706
1707        // A wake source stays usable after its runtime closes, so hosts tear
1708        // the two down in either order.
1709        map.close().unwrap();
1710        runtime.close().unwrap();
1711        source.signal().unwrap();
1712    }
1713
1714    #[test]
1715    // Spec coverage: BND-089.
1716    fn a_pump_clears_the_wake_flag_it_returns_on() {
1717        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1718        let source = runtime.wake_source().unwrap();
1719        quiesce(&mut runtime);
1720
1721        source.signal().unwrap();
1722        let started = Instant::now();
1723        runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1724        assert!(
1725            started.elapsed() < Duration::from_secs(5),
1726            "a pump waited even though the wake flag was set"
1727        );
1728
1729        // The pump above cleared the wake flag, so this one waits its full timeout.
1730        let started = Instant::now();
1731        runtime
1732            .pump(Some(Duration::from_millis(200)), None)
1733            .unwrap();
1734        assert!(
1735            started.elapsed() >= Duration::from_millis(100),
1736            "the first pump left the wake flag set"
1737        );
1738
1739        runtime.close().unwrap();
1740    }
1741
1742    #[test]
1743    // Spec coverage: BND-090.
1744    fn a_bounded_drain_reports_the_events_it_left_queued() {
1745        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1746        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1747        map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap();
1748        runtime.pump(Some(Duration::ZERO), None).unwrap();
1749
1750        let bounded = runtime.drain_events(1).unwrap();
1751        assert_eq!(bounded.len(), 1);
1752        assert!(
1753            bounded.remaining() > 0,
1754            "a style load should queue more than one event"
1755        );
1756        let rest = runtime.drain_events(0).unwrap();
1757        assert!(rest.len() > 1, "one drain should report the whole queue");
1758        assert_eq!(rest.remaining(), 0);
1759        let first = rest.iter().next().unwrap();
1760        assert_eq!(first.source(), RuntimeEventSource::Map(map.id()));
1761
1762        map.close().unwrap();
1763        runtime.close().unwrap();
1764    }
1765
1766    #[test]
1767    // Spec coverage: BND-060 and BND-091.
1768    fn a_creation_mask_narrows_a_runtime_before_its_first_operation() {
1769        let mut options = crate::RuntimeOptions::default();
1770        options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED;
1771        let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1772
1773        assert_eq!(
1774            runtime.event_mask().unwrap(),
1775            RuntimeEventMask::OFFLINE_OPERATION_COMPLETED
1776        );
1777
1778        // The narrowed runtime still reports the completion of the operation
1779        // this test starts, and the ambient cache operation needs no database.
1780        let operation = runtime
1781            .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1782            .unwrap();
1783        let completed =
1784            wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
1785        assert_eq!(completed.operation_id, operation.operation_id);
1786        operation.discard().unwrap();
1787
1788        runtime.close().unwrap();
1789
1790        // A creation mask carrying an undefined bit fails before the runtime
1791        // exists.
1792        options.event_mask = RuntimeEventMask::from_bits_retain(1 << 63);
1793        let error = RuntimeHandle::with_options(&options).unwrap_err();
1794        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1795    }
1796
1797    #[test]
1798    // Spec coverage: BND-091.
1799    fn a_runtime_mask_round_trips_and_rejects_undefined_bits() {
1800        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1801
1802        // The default options mask selects every type.
1803        assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1804
1805        runtime.set_event_mask(RuntimeEventMask::ALL).unwrap();
1806        assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1807
1808        // Read, clear one bit, write back: every other bit survives.
1809        let mut mask = runtime.event_mask().unwrap();
1810        mask.remove(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED);
1811        runtime.set_event_mask(mask).unwrap();
1812        let read_back = runtime.event_mask().unwrap();
1813        assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED));
1814        assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED));
1815        assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED));
1816
1817        let undefined = RuntimeEventMask::from_bits_retain(1 << 63);
1818        let error = runtime.set_event_mask(undefined).unwrap_err();
1819        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1820        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1821        assert_eq!(runtime.event_mask().unwrap(), read_back);
1822
1823        runtime.close().unwrap();
1824    }
1825
1826    #[test]
1827    // Spec coverage: BND-020, BND-022, BND-190, and BND-191.
1828    fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1829        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1830        let runtime_handle = runtime.inner.native().unwrap();
1831
1832        let error = std::thread::spawn(move || {
1833            // SAFETY: This intentionally exercises the C API's owner-thread
1834            // validation path with a live runtime handle from another thread.
1835            maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) })
1836                .unwrap_err()
1837        })
1838        .join()
1839        .unwrap();
1840
1841        assert_eq!(error.kind(), ErrorKind::WrongThread);
1842        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1843        assert!(!error.diagnostic().is_empty());
1844        runtime.close().unwrap();
1845    }
1846
1847    #[test]
1848    // Spec coverage: BND-123.
1849    fn resource_provider_installs_replaces_clears_and_releases_state() {
1850        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1851        let first = Arc::new(());
1852        let first_callback = Arc::clone(&first);
1853
1854        runtime
1855            .set_resource_provider(move |_, _| {
1856                let _ = &first_callback;
1857                crate::ResourceProviderDecision::PassThrough
1858            })
1859            .unwrap();
1860        assert_eq!(Arc::strong_count(&first), 2);
1861
1862        let second = Arc::new(());
1863        let second_callback = Arc::clone(&second);
1864        runtime
1865            .set_resource_provider(move |_, _| {
1866                let _ = &second_callback;
1867                crate::ResourceProviderDecision::PassThrough
1868            })
1869            .unwrap();
1870        assert_eq!(Arc::strong_count(&first), 1);
1871        assert_eq!(Arc::strong_count(&second), 2);
1872
1873        runtime.clear_resource_provider().unwrap();
1874        assert_eq!(Arc::strong_count(&second), 1);
1875
1876        let third = Arc::new(());
1877        let third_callback = Arc::clone(&third);
1878        runtime
1879            .set_resource_provider(move |_, _| {
1880                let _ = &third_callback;
1881                crate::ResourceProviderDecision::PassThrough
1882            })
1883            .unwrap();
1884        assert_eq!(Arc::strong_count(&third), 2);
1885
1886        runtime.close().unwrap();
1887        assert_eq!(Arc::strong_count(&third), 1);
1888    }
1889
1890    #[test]
1891    // Spec coverage: BND-122.
1892    fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1893        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1894        let first = Arc::new(());
1895        let first_callback = Arc::clone(&first);
1896        runtime
1897            .set_resource_provider(move |_, _| {
1898                let _ = &first_callback;
1899                crate::ResourceProviderDecision::PassThrough
1900            })
1901            .unwrap();
1902
1903        let second = Arc::new(());
1904        let second_callback = Arc::clone(&second);
1905        let error = runtime
1906            .inner
1907            .set_resource_provider_with_rejected_descriptor_for_testing(move |_, _| {
1908                let _ = &second_callback;
1909                crate::ResourceProviderDecision::PassThrough
1910            })
1911            .unwrap_err();
1912
1913        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1914        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1915        assert_eq!(Arc::strong_count(&first), 2);
1916        assert_eq!(Arc::strong_count(&second), 1);
1917
1918        runtime.close().unwrap();
1919        assert_eq!(Arc::strong_count(&first), 1);
1920    }
1921
1922    // Requests a style no file source serves, so the loading-failure event that
1923    // follows proves the request reached the network file source where the
1924    // runtime-scoped provider sits.
1925    fn load_probe_style(runtime: &mut RuntimeHandle, map: &MapHandle, style_url: &str) {
1926        map.set_style_url(style_url).unwrap();
1927        let event = wait_for_map_loading_failure(runtime);
1928        assert!(
1929            event
1930                .message
1931                .as_deref()
1932                .is_some_and(|message| message.contains("\"jar\""))
1933        );
1934    }
1935
1936    #[test]
1937    // Spec coverage: BND-142 and BND-154.
1938    fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() {
1939        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1940        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1941
1942        let first_calls = Arc::new(AtomicUsize::new(0));
1943        let first_callback_calls = Arc::clone(&first_calls);
1944        runtime
1945            .set_resource_provider(move |_, _| {
1946                first_callback_calls.fetch_add(1, Ordering::SeqCst);
1947                ResourceProviderDecision::PassThrough
1948            })
1949            .unwrap();
1950        load_probe_style(&mut runtime, &map, "jar:file:/packaged/first.json");
1951        assert!(first_calls.load(Ordering::SeqCst) > 0);
1952
1953        let second_calls = Arc::new(AtomicUsize::new(0));
1954        let second_callback_calls = Arc::clone(&second_calls);
1955        runtime
1956            .set_resource_provider(move |_, _| {
1957                second_callback_calls.fetch_add(1, Ordering::SeqCst);
1958                ResourceProviderDecision::PassThrough
1959            })
1960            .unwrap();
1961        let first_calls_after_replace = first_calls.load(Ordering::SeqCst);
1962        load_probe_style(&mut runtime, &map, "jar:file:/packaged/second.json");
1963        assert!(second_calls.load(Ordering::SeqCst) > 0);
1964        assert_eq!(
1965            first_calls.load(Ordering::SeqCst),
1966            first_calls_after_replace
1967        );
1968
1969        runtime.clear_resource_provider().unwrap();
1970        let second_calls_after_clear = second_calls.load(Ordering::SeqCst);
1971        load_probe_style(&mut runtime, &map, "jar:file:/packaged/third.json");
1972        assert_eq!(
1973            first_calls.load(Ordering::SeqCst),
1974            first_calls_after_replace
1975        );
1976        assert_eq!(
1977            second_calls.load(Ordering::SeqCst),
1978            second_calls_after_clear
1979        );
1980
1981        // Clearing an already cleared provider stays a successful no-op.
1982        runtime.clear_resource_provider().unwrap();
1983
1984        map.close().unwrap();
1985        runtime.close().unwrap();
1986    }
1987
1988    #[test]
1989    // Spec coverage: BND-143 and BND-150.
1990    fn resource_provider_completes_style_request_inline_through_c_abi() {
1991        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1992        let calls = Arc::new(AtomicUsize::new(0));
1993        let callback_calls = Arc::clone(&calls);
1994        runtime
1995            .set_resource_provider(move |request, handle| {
1996                if request.requested_url != "custom://style.json" {
1997                    return ResourceProviderDecision::PassThrough;
1998                }
1999                callback_calls.fetch_add(1, Ordering::SeqCst);
2000                assert_eq!(request.kind, ResourceKind::Style);
2001                handle
2002                    .complete(ResourceResponse::ok(
2003                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2004                    ))
2005                    .unwrap();
2006                ResourceProviderDecision::PassThrough
2007            })
2008            .unwrap();
2009
2010        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2011        map.set_style_url("custom://style.json").unwrap();
2012
2013        assert!(wait_for_runtime_event(
2014            &mut runtime,
2015            RuntimeEventType::MapStyleLoaded
2016        ));
2017        assert_eq!(calls.load(Ordering::SeqCst), 1);
2018        map.close().unwrap();
2019        runtime.close().unwrap();
2020    }
2021
2022    #[test]
2023    // Spec coverage: BND-155.
2024    fn resource_provider_sees_scheme_alias_and_its_resolved_url() {
2025        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2026        let resolved = Arc::new(Mutex::new(None));
2027        let callback_resolved = Arc::clone(&resolved);
2028        runtime
2029            .set_resource_provider(move |request, handle| {
2030                if request.requested_url != "maplibre://maps/style" {
2031                    return ResourceProviderDecision::PassThrough;
2032                }
2033                *callback_resolved.lock().unwrap() = Some(request.resolved_url.clone());
2034                handle
2035                    .complete(ResourceResponse::ok(
2036                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2037                    ))
2038                    .unwrap();
2039                ResourceProviderDecision::Handle
2040            })
2041            .unwrap();
2042
2043        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2044        map.set_style_url("maplibre://maps/style").unwrap();
2045
2046        assert!(wait_for_runtime_event(
2047            &mut runtime,
2048            RuntimeEventType::MapStyleLoaded
2049        ));
2050        assert_eq!(
2051            resolved.lock().unwrap().as_deref(),
2052            Some("https://demotiles.maplibre.org/style.json")
2053        );
2054        map.close().unwrap();
2055        runtime.close().unwrap();
2056    }
2057
2058    #[test]
2059    // Spec coverage: BND-144 and BND-145.
2060    fn resource_provider_completes_style_request_from_another_thread() {
2061        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2062        let (sender, receiver) = std::sync::mpsc::channel();
2063        runtime
2064            .set_resource_provider(move |request, handle| {
2065                if request.requested_url == "custom://async-style.json" {
2066                    sender.send(handle).unwrap();
2067                    ResourceProviderDecision::Handle
2068                } else {
2069                    ResourceProviderDecision::PassThrough
2070                }
2071            })
2072            .unwrap();
2073
2074        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2075        map.set_style_url("custom://async-style.json").unwrap();
2076        let handle = receiver
2077            .recv_timeout(Duration::from_secs(5))
2078            .expect("provider should send handled request");
2079        assert!(!handle.is_cancelled().unwrap());
2080        std::thread::spawn(move || {
2081            handle
2082                .complete(ResourceResponse::ok(
2083                    PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2084                ))
2085                .unwrap();
2086        })
2087        .join()
2088        .unwrap();
2089
2090        assert!(wait_for_runtime_event(
2091            &mut runtime,
2092            RuntimeEventType::MapStyleLoaded
2093        ));
2094        map.close().unwrap();
2095        runtime.close().unwrap();
2096    }
2097
2098    /// Pumps the runtime until the condition holds, returning whether it did.
2099    fn pump_until(runtime: &mut RuntimeHandle, condition: impl Fn() -> bool) -> bool {
2100        for _ in 0..100 {
2101            if condition() {
2102                return true;
2103            }
2104            runtime.pump(Some(Duration::ZERO), None).unwrap();
2105            std::thread::sleep(Duration::from_millis(10));
2106        }
2107        condition()
2108    }
2109
2110    #[test]
2111    // Spec coverage: BND-198.
2112    fn cancel_callback_runs_once_when_the_map_discards_the_request() {
2113        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2114        let cancels = Arc::new(AtomicUsize::new(0));
2115        let callback_cancels = Arc::clone(&cancels);
2116        let (sender, receiver) = std::sync::mpsc::channel();
2117        runtime
2118            .set_resource_provider(move |request, handle| {
2119                if request.requested_url != "custom://cancel-style.json" {
2120                    return ResourceProviderDecision::PassThrough;
2121                }
2122                let cancels = Arc::clone(&callback_cancels);
2123                handle
2124                    .set_cancel_callback(move || {
2125                        cancels.fetch_add(1, Ordering::SeqCst);
2126                    })
2127                    .unwrap();
2128                sender.send(handle).unwrap();
2129                ResourceProviderDecision::Handle
2130            })
2131            .unwrap();
2132
2133        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2134        map.set_style_url("custom://cancel-style.json").unwrap();
2135        let handle = receiver
2136            .recv_timeout(Duration::from_secs(5))
2137            .expect("provider should send handled request");
2138        assert_eq!(cancels.load(Ordering::SeqCst), 0);
2139
2140        map.close().unwrap();
2141
2142        assert!(pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) == 1));
2143        assert!(handle.is_cancelled().unwrap());
2144        // The cancelled request rejects a late completion and stays at one call.
2145        assert_eq!(
2146            handle
2147                .complete(ResourceResponse::no_content())
2148                .unwrap_err()
2149                .kind(),
2150            ErrorKind::InvalidState
2151        );
2152        assert_eq!(cancels.load(Ordering::SeqCst), 1);
2153        runtime.close().unwrap();
2154    }
2155
2156    #[test]
2157    // Spec coverage: BND-198.
2158    fn cancel_callback_may_close_its_own_request() {
2159        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2160        let cancelled_request: Arc<Mutex<Option<crate::ResourceRequestHandle>>> =
2161            Arc::new(Mutex::new(None));
2162        let provider_request = Arc::clone(&cancelled_request);
2163        let closed = Arc::new(AtomicUsize::new(0));
2164        let callback_closed = Arc::clone(&closed);
2165        runtime
2166            .set_resource_provider(move |request, handle| {
2167                if request.requested_url != "custom://cancel-style.json" {
2168                    return ResourceProviderDecision::PassThrough;
2169                }
2170                let callback_request = Arc::clone(&provider_request);
2171                let closed = Arc::clone(&callback_closed);
2172                handle
2173                    .set_cancel_callback(move || {
2174                        let request = callback_request.lock().unwrap().take();
2175                        request.expect("the cancelled request").close();
2176                        closed.fetch_add(1, Ordering::SeqCst);
2177                    })
2178                    .unwrap();
2179                *provider_request.lock().unwrap() = Some(handle);
2180                ResourceProviderDecision::Handle
2181            })
2182            .unwrap();
2183
2184        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2185        map.set_style_url("custom://cancel-style.json").unwrap();
2186        assert!(pump_until(&mut runtime, || cancelled_request
2187            .lock()
2188            .unwrap()
2189            .is_some()));
2190
2191        map.close().unwrap();
2192
2193        assert!(pump_until(&mut runtime, || closed.load(Ordering::SeqCst) == 1));
2194        assert!(cancelled_request.lock().unwrap().is_none());
2195        runtime.close().unwrap();
2196    }
2197
2198    #[test]
2199    // Spec coverage: BND-198.
2200    fn cancel_callback_skips_a_completed_request() {
2201        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2202        let cancels = Arc::new(AtomicUsize::new(0));
2203        let callback_cancels = Arc::clone(&cancels);
2204        runtime
2205            .set_resource_provider(move |request, handle| {
2206                if request.requested_url != "custom://cancel-style.json" {
2207                    return ResourceProviderDecision::PassThrough;
2208                }
2209                let cancels = Arc::clone(&callback_cancels);
2210                handle
2211                    .set_cancel_callback(move || {
2212                        cancels.fetch_add(1, Ordering::SeqCst);
2213                    })
2214                    .unwrap();
2215                handle
2216                    .complete(ResourceResponse::ok(
2217                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2218                    ))
2219                    .unwrap();
2220                ResourceProviderDecision::Handle
2221            })
2222            .unwrap();
2223
2224        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2225        map.set_style_url("custom://cancel-style.json").unwrap();
2226        assert!(wait_for_runtime_event(
2227            &mut runtime,
2228            RuntimeEventType::MapStyleLoaded
2229        ));
2230
2231        map.close().unwrap();
2232
2233        // MapLibre runs its cancel hook on every request teardown, so this
2234        // proves the completed request reports no cancellation.
2235        assert!(!pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) > 0));
2236        runtime.close().unwrap();
2237    }
2238
2239    #[test]
2240    // Spec coverage: BND-198.
2241    fn cancel_registration_on_a_cancelled_request_runs_before_returning() {
2242        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2243        let (sender, receiver) = std::sync::mpsc::channel();
2244        runtime
2245            .set_resource_provider(move |request, handle| {
2246                if request.requested_url != "custom://cancel-style.json" {
2247                    return ResourceProviderDecision::PassThrough;
2248                }
2249                sender.send(handle).unwrap();
2250                ResourceProviderDecision::Handle
2251            })
2252            .unwrap();
2253
2254        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2255        map.set_style_url("custom://cancel-style.json").unwrap();
2256        let handle = receiver
2257            .recv_timeout(Duration::from_secs(5))
2258            .expect("provider should send handled request");
2259        map.close().unwrap();
2260        assert!(pump_until(&mut runtime, || handle.is_cancelled().unwrap()));
2261
2262        let cancels = Arc::new(AtomicUsize::new(0));
2263        let callback_cancels = Arc::clone(&cancels);
2264        handle
2265            .set_cancel_callback(move || {
2266                callback_cancels.fetch_add(1, Ordering::SeqCst);
2267            })
2268            .unwrap();
2269        assert_eq!(cancels.load(Ordering::SeqCst), 1);
2270
2271        // The request keeps its one registration after the callback ran.
2272        assert_eq!(
2273            handle.set_cancel_callback(|| {}).unwrap_err().kind(),
2274            ErrorKind::InvalidState
2275        );
2276        runtime.close().unwrap();
2277    }
2278
2279    #[test]
2280    // Spec coverage: BND-149.
2281    fn resource_provider_error_response_becomes_copied_loading_failure_event() {
2282        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2283        runtime
2284            .set_resource_provider(move |request, handle| {
2285                if request.requested_url == "custom://broken-style.json" {
2286                    handle
2287                        .complete(ResourceResponse::error(
2288                            ResourceErrorReason::Other,
2289                            "provider failed",
2290                        ))
2291                        .unwrap();
2292                    ResourceProviderDecision::Handle
2293                } else {
2294                    ResourceProviderDecision::PassThrough
2295                }
2296            })
2297            .unwrap();
2298
2299        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2300        let map_id = map.id();
2301        map.set_style_url("custom://broken-style.json").unwrap();
2302
2303        let event = wait_for_map_loading_failure(&mut runtime);
2304        let copied_message = event.message.clone();
2305        // The copy stays intact after the drain that ends the batch's window.
2306        let _ = runtime.drain_events(0).unwrap();
2307
2308        assert_eq!(event.source, RuntimeEventSource::Map(map_id));
2309        assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
2310        assert_eq!(event.message, copied_message);
2311        assert!(
2312            event
2313                .message
2314                .as_deref()
2315                .is_some_and(|message| message.contains("provider failed"))
2316        );
2317
2318        map.close().unwrap();
2319        runtime.close().unwrap();
2320    }
2321
2322    #[test]
2323    // Spec coverage: BND-140 and BND-123.
2324    fn resource_transform_installs_replaces_clears_and_releases_state() {
2325        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2326        let first = Arc::new(());
2327        let first_callback = Arc::clone(&first);
2328
2329        runtime
2330            .set_resource_transform(move |request| {
2331                let _ = &first_callback;
2332                assert!(matches!(
2333                    request.kind,
2334                    ResourceKind::Style | ResourceKind::UnknownRaw(_)
2335                ));
2336                None
2337            })
2338            .unwrap();
2339        assert_eq!(Arc::strong_count(&first), 2);
2340
2341        let second = Arc::new(());
2342        let second_callback = Arc::clone(&second);
2343        runtime
2344            .set_resource_transform(move |_| {
2345                let _ = &second_callback;
2346                Some("https://example.test/replacement".to_owned())
2347            })
2348            .unwrap();
2349        assert_eq!(Arc::strong_count(&first), 1);
2350        assert_eq!(Arc::strong_count(&second), 2);
2351
2352        runtime.clear_resource_transform().unwrap();
2353        assert_eq!(Arc::strong_count(&second), 1);
2354        runtime.close().unwrap();
2355    }
2356
2357    /// The browser has no in-process TCP server, so the runner serves the two
2358    /// style documents and answers 404 for everything else. That makes the
2359    /// server the oracle: a rewrite that did not happen fails the style load
2360    /// rather than fetching an identical document, and the layer each document
2361    /// carries names which one was used. See scripts/run-browser-test.mjs.
2362    #[cfg(target_os = "emscripten")]
2363    #[test]
2364    // Spec coverage: BND-140.
2365    fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2366        let origin = std::env::var("MLN_FFI_TEST_FIXTURE_ORIGIN").expect(
2367            "MLN_FFI_TEST_FIXTURE_ORIGIN is unset; run the suite through \
2368             `mise run //bindings/rust:test emscripten-wasm32-webgl`",
2369        );
2370        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2371        let transform_url = format!("{origin}/__fixture/rewritten-style.json");
2372        // Matches the URL loaded after the clear as well, so a transform that
2373        // outlived the clear rewrites that request too and the layer id says so.
2374        runtime
2375            .set_resource_transform(move |request| {
2376                (request.url.ends_with("/original-style.json")
2377                    || request.url.ends_with("/original-after-clear.json"))
2378                .then(|| transform_url.clone())
2379            })
2380            .unwrap();
2381
2382        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2383        map.set_style_url(&format!("{origin}/__fixture/original-style.json"))
2384            .unwrap();
2385        assert!(wait_for_runtime_event(
2386            &mut runtime,
2387            RuntimeEventType::MapStyleLoaded
2388        ));
2389        // Contains rather than equals: MapLibre adds its own annotation layer to
2390        // every style it loads.
2391        assert!(
2392            map.style_layer_ids()
2393                .unwrap()
2394                .iter()
2395                .any(|id| id == "rewritten")
2396        );
2397
2398        runtime.clear_resource_transform().unwrap();
2399        map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json"))
2400            .unwrap();
2401        assert!(wait_for_runtime_event(
2402            &mut runtime,
2403            RuntimeEventType::MapStyleLoaded
2404        ));
2405        assert!(
2406            map.style_layer_ids()
2407                .unwrap()
2408                .iter()
2409                .any(|id| id == "original-after-clear")
2410        );
2411
2412        map.close().unwrap();
2413        runtime.close().unwrap();
2414    }
2415
2416    #[cfg(not(target_os = "emscripten"))]
2417    #[test]
2418    // Spec coverage: BND-140.
2419    fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2420        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2421        let (base_url, requests, server) = spawn_style_server(2);
2422        let transform_base_url = base_url.clone();
2423
2424        // Matches the URL loaded after the clear as well, so a transform that
2425        // outlived the clear rewrites that request too and the recorded path
2426        // says so.
2427        runtime
2428            .set_resource_transform(move |request| {
2429                if request.url.ends_with("/original-style.json")
2430                    || request.url.ends_with("/original-after-clear.json")
2431                {
2432                    Some(format!("{transform_base_url}/rewritten-style.json"))
2433                } else {
2434                    None
2435                }
2436            })
2437            .unwrap();
2438
2439        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2440        map.set_style_url(&format!("{base_url}/original-style.json"))
2441            .unwrap();
2442        assert!(wait_for_runtime_event(
2443            &mut runtime,
2444            RuntimeEventType::MapStyleLoaded
2445        ));
2446        assert_eq!(
2447            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2448            "/rewritten-style.json"
2449        );
2450
2451        runtime.clear_resource_transform().unwrap();
2452        map.set_style_url(&format!("{base_url}/original-after-clear.json"))
2453            .unwrap();
2454        assert!(wait_for_runtime_event(
2455            &mut runtime,
2456            RuntimeEventType::MapStyleLoaded
2457        ));
2458        assert_eq!(
2459            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2460            "/original-after-clear.json"
2461        );
2462
2463        map.close().unwrap();
2464        runtime.close().unwrap();
2465        server.join().unwrap();
2466    }
2467
2468    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2469    #[test]
2470    // Spec coverage: BND-158 and BND-159.
2471    fn http_header_transform_reaches_requests_and_clear_stops_it() {
2472        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2473        let (base_url, requests, server) = spawn_recording_style_server(2);
2474        runtime
2475            .set_http_header_transform(|request| {
2476                assert_eq!(request.kind, ResourceKind::Style);
2477                vec![crate::HttpHeader::new("X-Map-Token", "secret")]
2478            })
2479            .unwrap();
2480
2481        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2482        map.set_style_url(&format!("{base_url}/with-header.json"))
2483            .unwrap();
2484        assert!(wait_for_runtime_event(
2485            &mut runtime,
2486            RuntimeEventType::MapStyleLoaded
2487        ));
2488        let first = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2489        assert!(
2490            first
2491                .lines()
2492                .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"))
2493        );
2494
2495        runtime.clear_http_header_transform().unwrap();
2496        map.set_style_url(&format!("{base_url}/after-clear.json"))
2497            .unwrap();
2498        assert!(wait_for_runtime_event(
2499            &mut runtime,
2500            RuntimeEventType::MapStyleLoaded
2501        ));
2502        let second = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2503        assert!(
2504            !second
2505                .lines()
2506                .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:"))
2507        );
2508
2509        map.close().unwrap();
2510        runtime.close().unwrap();
2511        server.join().unwrap();
2512    }
2513
2514    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2515    #[test]
2516    // Spec coverage: BND-158.
2517    fn http_header_transform_skips_non_http_urls() {
2518        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2519        runtime.set_resource_transform(|_| None).unwrap();
2520        let calls = Arc::new(AtomicUsize::new(0));
2521        let callback_calls = Arc::clone(&calls);
2522        runtime
2523            .set_http_header_transform(move |_| {
2524                callback_calls.fetch_add(1, Ordering::SeqCst);
2525                Vec::new()
2526            })
2527            .unwrap();
2528        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2529
2530        map.set_style_url("jar:file:/packaged/style.json").unwrap();
2531        let _ = wait_for_map_loading_failure(&mut runtime);
2532        assert_eq!(calls.load(Ordering::SeqCst), 0);
2533
2534        map.close().unwrap();
2535        runtime.close().unwrap();
2536    }
2537
2538    #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2539    #[test]
2540    // Spec coverage: BND-159.
2541    fn http_header_transform_preserves_same_origin_and_strips_cross_origin_redirects() {
2542        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2543        let (origin_url, requests, servers) = spawn_redirect_style_servers();
2544        runtime
2545            .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")])
2546            .unwrap();
2547        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2548
2549        map.set_style_url(&format!("{origin_url}/same-start.json"))
2550            .unwrap();
2551        assert!(wait_for_runtime_event(
2552            &mut runtime,
2553            RuntimeEventType::MapStyleLoaded
2554        ));
2555        assert_eq!(
2556            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2557            ("/same-start.json".to_owned(), true)
2558        );
2559        assert_eq!(
2560            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2561            ("/same-final.json".to_owned(), true)
2562        );
2563
2564        map.set_style_url(&format!("{origin_url}/cross-start.json"))
2565            .unwrap();
2566        assert!(wait_for_runtime_event(
2567            &mut runtime,
2568            RuntimeEventType::MapStyleLoaded
2569        ));
2570        assert_eq!(
2571            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2572            ("/cross-start.json".to_owned(), true)
2573        );
2574        assert_eq!(
2575            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2576            ("/cross-final.json".to_owned(), false)
2577        );
2578
2579        map.close().unwrap();
2580        runtime.close().unwrap();
2581        for server in servers {
2582            server.join().unwrap();
2583        }
2584    }
2585
2586    // OpenHarmony's platform HTTP client has no redirect-decision hook, and the
2587    // browser's fetch transport follows redirects itself, so both report header
2588    // transforms unsupported rather than enabling a transport that cannot
2589    // satisfy the redirect contract.
2590    #[cfg(any(target_env = "ohos", target_os = "emscripten"))]
2591    #[test]
2592    fn http_header_transform_reports_unsupported() {
2593        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2594        let error = runtime
2595            .set_http_header_transform(|_| Vec::new())
2596            .unwrap_err();
2597        assert_eq!(error.kind(), ErrorKind::Unsupported);
2598        runtime.close().unwrap();
2599    }
2600
2601    #[test]
2602    // Spec coverage: BND-123.
2603    fn resource_transform_replacement_after_map_creation_releases_previous_state() {
2604        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2605        let first = Arc::new(());
2606        let first_callback = Arc::clone(&first);
2607        runtime
2608            .set_resource_transform(move |_| {
2609                let _ = &first_callback;
2610                None
2611            })
2612            .unwrap();
2613        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2614
2615        let second = Arc::new(());
2616        let second_callback = Arc::clone(&second);
2617        runtime
2618            .set_resource_transform(move |_| {
2619                let _ = &second_callback;
2620                None
2621            })
2622            .unwrap();
2623
2624        assert_eq!(Arc::strong_count(&first), 1);
2625        assert_eq!(Arc::strong_count(&second), 2);
2626
2627        map.close().unwrap();
2628        runtime.close().unwrap();
2629        assert_eq!(Arc::strong_count(&second), 1);
2630    }
2631
2632    #[test]
2633    // Spec coverage: BND-123.
2634    fn runtime_teardown_releases_resource_transform_state() {
2635        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2636        let token = Arc::new(());
2637        let callback_token = Arc::clone(&token);
2638        runtime
2639            .set_resource_transform(move |_| {
2640                let _ = &callback_token;
2641                None
2642            })
2643            .unwrap();
2644        assert_eq!(Arc::strong_count(&token), 2);
2645
2646        runtime.close().unwrap();
2647
2648        assert_eq!(Arc::strong_count(&token), 1);
2649    }
2650
2651    #[test]
2652    // Rust regression: documents the Rust binding's late transform-install
2653    // guard after map creation.
2654    fn resource_transform_installs_after_map_creation() {
2655        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2656        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2657
2658        runtime.set_resource_transform(|_| None).unwrap();
2659
2660        map.close().unwrap();
2661        runtime.close().unwrap();
2662    }
2663
2664    #[test]
2665    // Spec coverage: BND-140 and BND-123.
2666    fn resource_transform_clears_after_map_was_closed_and_releases_state() {
2667        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2668        let token = Arc::new(());
2669        let callback_token = Arc::clone(&token);
2670        runtime
2671            .set_resource_transform(move |_| {
2672                let _ = &callback_token;
2673                None
2674            })
2675            .unwrap();
2676        assert_eq!(Arc::strong_count(&token), 2);
2677
2678        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2679        map.close().unwrap();
2680
2681        runtime.clear_resource_transform().unwrap();
2682
2683        assert_eq!(Arc::strong_count(&token), 1);
2684
2685        runtime.close().unwrap();
2686    }
2687
2688    #[test]
2689    // Spec coverage: BND-081, BND-090, and BND-092.
2690    fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() {
2691        let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2692        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2693        let map_id = map.id();
2694
2695        let error = map.set_style_json(b"{").unwrap_err();
2696        assert_eq!(error.kind(), ErrorKind::NativeError);
2697        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
2698
2699        let batch = runtime.drain_events(0).unwrap();
2700        let types = batch
2701            .iter()
2702            .map(|event| event.event_type())
2703            .collect::<Vec<_>>();
2704        assert!(
2705            types.len() > 1,
2706            "a failed style load should queue more than one event, got {types:?}"
2707        );
2708        let loading_failed = batch
2709            .iter()
2710            .find(|event| event.event_type() == RuntimeEventType::MapLoadingFailed)
2711            .expect("a malformed style should queue a loading-failed event");
2712        assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id));
2713        assert!(
2714            loading_failed
2715                .message()
2716                .unwrap()
2717                .is_some_and(|message| !message.is_empty())
2718        );
2719        let owned = loading_failed.to_owned().unwrap();
2720
2721        // The next drain ends the previous batch's window; the copy is
2722        // untouched.
2723        assert!(runtime.drain_events(0).unwrap().is_empty());
2724        assert_eq!(owned.source, RuntimeEventSource::Map(map_id));
2725        assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed);
2726        assert!(
2727            owned
2728                .message
2729                .as_deref()
2730                .is_some_and(|message| !message.is_empty())
2731        );
2732
2733        map.close().unwrap();
2734        runtime.close().unwrap();
2735    }
2736
2737    #[test]
2738    // Spec coverage: BND-042.
2739    fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
2740        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2741        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2742
2743        let error = runtime.close().unwrap_err();
2744        assert_eq!(error.kind(), ErrorKind::InvalidState);
2745        assert_eq!(error.raw_status(), None);
2746        let runtime = error.into_handle();
2747
2748        runtime.pump(Some(Duration::ZERO), None).unwrap();
2749        map.close().unwrap();
2750        runtime.close().unwrap();
2751    }
2752}