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