Skip to main content

maplibre_native/
runtime.rs

1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3use std::fmt;
4use std::marker::PhantomData;
5use std::rc::{Rc, Weak};
6
7use maplibre_core::AmbientCacheOperation;
8use maplibre_native_core as maplibre_core;
9use maplibre_native_sys as sys;
10
11use crate::events::{
12    MapId, OfflineRegionDownloadState, OfflineRegionStatus, RuntimeEvent, RuntimeEventSource,
13    empty_runtime_event,
14};
15use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle};
16use crate::map::MapState;
17use crate::resource::{ResourceProviderState, ResourceTransformState};
18use crate::{
19    Error, ErrorKind, HandleOperationError, OfflineOperationTakeError, ResourceProviderDecision,
20    Result,
21};
22#[cfg(test)]
23use crate::{Geometry, LatLngBounds, MapHandle, MapOptions};
24
25pub use maplibre_core::runtime::{OfflineRegionDefinition, OfflineRegionInfo, RuntimeOptions};
26pub(crate) use maplibre_core::runtime::{
27    OfflineRegionDefinitionNativeExt, RuntimeOptionsNativeExt,
28};
29
30#[derive(Debug)]
31pub(crate) struct RuntimeState {
32    handle: ThreadAffineNativeHandle<sys::mln_runtime>,
33    next_map_id: Cell<u64>,
34    has_created_map: Cell<bool>,
35    map_ids: RefCell<HashMap<usize, MapId>>,
36    map_states: RefCell<HashMap<usize, Weak<MapState>>>,
37    resource_transform: RefCell<Option<Box<ResourceTransformState>>>,
38    resource_provider: RefCell<Option<Box<ResourceProviderState>>>,
39}
40
41impl RuntimeState {
42    fn new(ptr: std::ptr::NonNull<sys::mln_runtime>) -> Self {
43        // SAFETY: ptr came from successful mln_runtime_create and is paired
44        // with the matching runtime destroy function.
45        let handle = unsafe {
46            ThreadAffineNativeHandle::from_raw(ptr, sys::mln_runtime_destroy, "mln_runtime")
47        };
48        Self {
49            handle,
50            next_map_id: Cell::new(1),
51            has_created_map: Cell::new(false),
52            map_ids: RefCell::new(HashMap::new()),
53            map_states: RefCell::new(HashMap::new()),
54            resource_transform: RefCell::new(None),
55            resource_provider: RefCell::new(None),
56        }
57    }
58
59    pub(crate) fn as_ptr(&self) -> Result<*mut sys::mln_runtime> {
60        let ptr = self.handle.as_ptr();
61        if ptr.is_null() {
62            Err(closed_handle_error("RuntimeHandle"))
63        } else {
64            Ok(ptr)
65        }
66    }
67
68    fn is_closed(&self) -> bool {
69        self.handle.is_closed()
70    }
71
72    fn close(&self) -> Result<()> {
73        self.handle.close()?;
74        self.resource_transform.borrow_mut().take();
75        self.resource_provider.borrow_mut().take();
76        Ok(())
77    }
78
79    fn set_resource_provider<F>(&self, callback: F) -> Result<()>
80    where
81        F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
82            + Send
83            + Sync
84            + 'static,
85    {
86        self.check_resource_callbacks_allowed()?;
87        let runtime = self.as_ptr()?;
88        let replacement = ResourceProviderState::new(callback);
89        let descriptor = replacement.descriptor();
90
91        // SAFETY: runtime is live. descriptor contains a C trampoline and a
92        // user_data pointer to replacement, which remains alive on success. On
93        // failure, native preserves the previous provider and replacement is
94        // dropped below.
95        maplibre_core::check(unsafe {
96            sys::mln_runtime_set_resource_provider(runtime, &descriptor)
97        })?;
98        self.resource_provider.borrow_mut().replace(replacement);
99        Ok(())
100    }
101
102    fn set_resource_transform<F>(&self, callback: F) -> Result<()>
103    where
104        F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
105    {
106        let runtime = self.as_ptr()?;
107        let replacement = ResourceTransformState::new(callback);
108        let descriptor = replacement.descriptor();
109
110        // SAFETY: runtime is live. descriptor contains a C trampoline and a
111        // user_data pointer to replacement, which remains alive on success. On
112        // failure, native preserves the previous transform and replacement is
113        // dropped below.
114        maplibre_core::check(unsafe {
115            sys::mln_runtime_set_resource_transform(runtime, &descriptor)
116        })?;
117        self.resource_transform.borrow_mut().replace(replacement);
118        Ok(())
119    }
120
121    fn clear_resource_transform(&self) -> Result<()> {
122        let runtime = self.as_ptr()?;
123
124        // SAFETY: runtime is live. Native clear waits for in-flight transform
125        // callbacks before returning, so dropping Rust callback state below is safe.
126        maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_transform(runtime) })?;
127        self.resource_transform.borrow_mut().take();
128        Ok(())
129    }
130
131    fn check_resource_callbacks_allowed(&self) -> Result<()> {
132        if self.has_created_map.get() {
133            return Err(Error::new(
134                ErrorKind::InvalidState,
135                None,
136                "resource callbacks must be configured before creating maps from the runtime",
137            ));
138        }
139        Ok(())
140    }
141
142    pub(crate) fn register_map(&self, ptr: *mut sys::mln_map) -> MapId {
143        self.has_created_map.set(true);
144        let id = MapId::new(self.next_map_id.get());
145        self.next_map_id.set(id.get().saturating_add(1));
146        self.map_ids.borrow_mut().insert(ptr as usize, id);
147        id
148    }
149
150    pub(crate) fn register_map_state(&self, ptr: *mut sys::mln_map, state: Weak<MapState>) {
151        if !ptr.is_null() {
152            self.map_states.borrow_mut().insert(ptr as usize, state);
153        }
154    }
155
156    pub(crate) fn unregister_map(&self, ptr: *mut sys::mln_map) {
157        if !ptr.is_null() {
158            self.map_ids.borrow_mut().remove(&(ptr as usize));
159            self.map_states.borrow_mut().remove(&(ptr as usize));
160        }
161    }
162
163    fn apply_event_side_effects(&self, raw: &sys::mln_runtime_event) {
164        if raw.source_type != sys::MLN_RUNTIME_EVENT_SOURCE_MAP {
165            return;
166        }
167        let state = self
168            .map_states
169            .borrow()
170            .get(&(raw.source as usize))
171            .and_then(Weak::upgrade);
172        let Some(state) = state else {
173            return;
174        };
175        if raw.type_ == sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED {
176            state.release_detached_custom_geometry_sources();
177        }
178    }
179
180    #[cfg(test)]
181    pub(crate) fn apply_event_side_effects_for_testing(&self, raw: &sys::mln_runtime_event) {
182        self.apply_event_side_effects(raw);
183    }
184
185    fn source_for_event(&self, raw: &sys::mln_runtime_event) -> RuntimeEventSource {
186        match raw.source_type {
187            sys::MLN_RUNTIME_EVENT_SOURCE_RUNTIME => RuntimeEventSource::Runtime,
188            sys::MLN_RUNTIME_EVENT_SOURCE_MAP => self
189                .map_ids
190                .borrow()
191                .get(&(raw.source as usize))
192                .copied()
193                .map(RuntimeEventSource::Map)
194                .unwrap_or(RuntimeEventSource::UnknownMap),
195            source_type => RuntimeEventSource::Unknown(source_type),
196        }
197    }
198}
199
200/// Owner-thread runtime handle for MapLibre Native work and event polling.
201pub struct RuntimeHandle {
202    pub(crate) inner: Rc<RuntimeState>,
203}
204
205impl fmt::Debug for RuntimeHandle {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        f.debug_struct("RuntimeHandle")
208            .field("closed", &self.inner.is_closed())
209            .finish()
210    }
211}
212
213/// Owner-thread offline database operation token that must be taken or discarded.
214pub struct OfflineOperationHandle<T> {
215    runtime: Rc<RuntimeState>,
216    operation_id: sys::mln_offline_operation_id,
217    operation_kind: maplibre_core::OfflineOperationKind,
218    result_kind: maplibre_core::OfflineOperationResultKind,
219    live: Cell<bool>,
220    _result: PhantomData<fn() -> T>,
221    _thread_affine: PhantomData<Rc<()>>,
222}
223
224impl<T> fmt::Debug for OfflineOperationHandle<T> {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.debug_struct("OfflineOperationHandle")
227            .field("operation_id", &self.operation_id)
228            .field("operation_kind", &self.operation_kind)
229            .field("result_kind", &self.result_kind)
230            .field("live", &self.live.get())
231            .finish()
232    }
233}
234
235impl<T> OfflineOperationHandle<T> {
236    fn new(
237        runtime: Rc<RuntimeState>,
238        operation_id: sys::mln_offline_operation_id,
239        operation_kind: maplibre_core::OfflineOperationKind,
240        result_kind: maplibre_core::OfflineOperationResultKind,
241    ) -> Result<Self> {
242        if operation_id == 0 {
243            return Err(Error::invalid_argument(
244                "offline operation id must not be zero",
245            ));
246        }
247        Ok(Self {
248            runtime,
249            operation_id,
250            operation_kind,
251            result_kind,
252            live: Cell::new(true),
253            _result: PhantomData,
254            _thread_affine: PhantomData,
255        })
256    }
257
258    fn runtime_ptr(&self) -> Result<*mut sys::mln_runtime> {
259        if !self.live.get() {
260            return Err(closed_handle_error("OfflineOperationHandle"));
261        }
262        self.runtime.as_ptr()
263    }
264
265    fn mark_consumed(&self) {
266        self.live.set(false);
267    }
268
269    /// Discards runtime-owned state for this offline operation.
270    #[allow(clippy::result_large_err)]
271    pub fn discard(self) -> std::result::Result<(), HandleOperationError<Self>> {
272        if !self.live.get() {
273            return Ok(());
274        }
275        let runtime = match self.runtime_ptr() {
276            Ok(runtime) => runtime,
277            Err(error) => return Err(HandleOperationError::new(error, self)),
278        };
279        let status =
280            unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
281        if let Err(error) = maplibre_core::check(status) {
282            return Err(HandleOperationError::new(error, self));
283        }
284        self.live.set(false);
285        Ok(())
286    }
287}
288
289impl<T> Drop for OfflineOperationHandle<T> {
290    fn drop(&mut self) {
291        if !self.live.get() {
292            return;
293        }
294        if let Ok(runtime) = self.runtime.as_ptr() {
295            // SAFETY: Safe Rust keeps this !Send/!Sync handle on the runtime owner thread.
296            let status =
297                unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
298            if status == sys::MLN_STATUS_OK {
299                self.live.set(false);
300            }
301        }
302    }
303}
304
305impl OfflineOperationHandle<OfflineRegionInfo> {
306    /// Takes a completed create/update operation result as copied region info.
307    #[allow(clippy::result_large_err)]
308    pub fn take(self) -> std::result::Result<OfflineRegionInfo, OfflineOperationTakeError<Self>> {
309        let runtime = match self.runtime_ptr() {
310            Ok(runtime) => runtime,
311            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
312        };
313        let mut out = maplibre_core::ptr::OutPtr::<sys::mln_offline_region_snapshot>::new();
314        let status = match self.operation_kind {
315            maplibre_core::OfflineOperationKind::RegionCreate => unsafe {
316                sys::mln_runtime_offline_region_create_take_result(
317                    runtime,
318                    self.operation_id,
319                    out.as_mut_ptr(),
320                )
321            },
322            maplibre_core::OfflineOperationKind::RegionUpdateMetadata => unsafe {
323                sys::mln_runtime_offline_region_update_metadata_take_result(
324                    runtime,
325                    self.operation_id,
326                    out.as_mut_ptr(),
327                )
328            },
329            _ => sys::MLN_STATUS_INVALID_STATE,
330        };
331        if let Err(error) = maplibre_core::check(status) {
332            return Err(OfflineOperationTakeError::retryable(error, self));
333        }
334        self.mark_consumed();
335        let snapshot = match out.into_non_null("mln_offline_region_snapshot") {
336            Ok(snapshot) => snapshot,
337            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
338        };
339        // SAFETY: On success, the C API returns an owned snapshot handle;
340        // core copies and releases it.
341        unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
342            .map_err(OfflineOperationTakeError::consumed)
343    }
344}
345
346impl OfflineOperationHandle<Option<OfflineRegionInfo>> {
347    /// Takes a completed get operation result as optional copied region info.
348    #[allow(clippy::result_large_err)]
349    pub fn take(
350        self,
351    ) -> std::result::Result<Option<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
352        let runtime = match self.runtime_ptr() {
353            Ok(runtime) => runtime,
354            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
355        };
356        let mut out = maplibre_core::ptr::OutPtr::<sys::mln_offline_region_snapshot>::new();
357        let mut found = false;
358        let status = unsafe {
359            sys::mln_runtime_offline_region_get_take_result(
360                runtime,
361                self.operation_id,
362                out.as_mut_ptr(),
363                &mut found,
364            )
365        };
366        if let Err(error) = maplibre_core::check(status) {
367            return Err(OfflineOperationTakeError::retryable(error, self));
368        }
369        self.mark_consumed();
370        if !found {
371            return Ok(None);
372        }
373        let snapshot = match out.into_non_null("mln_offline_region_snapshot") {
374            Ok(snapshot) => snapshot,
375            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
376        };
377        // SAFETY: When found is true, the C API returns an owned snapshot
378        // handle; core copies and releases it.
379        Ok(Some(
380            unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
381                .map_err(OfflineOperationTakeError::consumed)?,
382        ))
383    }
384}
385
386impl OfflineOperationHandle<Vec<OfflineRegionInfo>> {
387    /// Takes a completed list/merge operation result as copied region info.
388    #[allow(clippy::result_large_err)]
389    pub fn take(
390        self,
391    ) -> std::result::Result<Vec<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
392        let runtime = match self.runtime_ptr() {
393            Ok(runtime) => runtime,
394            Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
395        };
396        let mut out = maplibre_core::ptr::OutPtr::<sys::mln_offline_region_list>::new();
397        let status = match self.operation_kind {
398            maplibre_core::OfflineOperationKind::RegionsList => unsafe {
399                sys::mln_runtime_offline_regions_list_take_result(
400                    runtime,
401                    self.operation_id,
402                    out.as_mut_ptr(),
403                )
404            },
405            maplibre_core::OfflineOperationKind::RegionsMergeDatabase => unsafe {
406                sys::mln_runtime_offline_regions_merge_database_take_result(
407                    runtime,
408                    self.operation_id,
409                    out.as_mut_ptr(),
410                )
411            },
412            _ => sys::MLN_STATUS_INVALID_STATE,
413        };
414        if let Err(error) = maplibre_core::check(status) {
415            return Err(OfflineOperationTakeError::retryable(error, self));
416        }
417        self.mark_consumed();
418        let list = match out.into_non_null("mln_offline_region_list") {
419            Ok(list) => list,
420            Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
421        };
422        // SAFETY: On success, the C API returns an owned list handle; core
423        // copies and releases it.
424        unsafe { maplibre_core::runtime::copy_offline_region_list(list) }
425            .map_err(OfflineOperationTakeError::consumed)
426    }
427}
428
429impl OfflineOperationHandle<OfflineRegionStatus> {
430    /// Takes a completed status operation result as copied status data.
431    #[allow(clippy::result_large_err)]
432    pub fn take(self) -> std::result::Result<OfflineRegionStatus, HandleOperationError<Self>> {
433        let runtime = match self.runtime_ptr() {
434            Ok(runtime) => runtime,
435            Err(error) => return Err(HandleOperationError::new(error, self)),
436        };
437        let mut raw = maplibre_core::events::empty_offline_region_status_native();
438        let status = unsafe {
439            sys::mln_runtime_offline_region_get_status_take_result(
440                runtime,
441                self.operation_id,
442                &mut raw,
443            )
444        };
445        if let Err(error) = maplibre_core::check(status) {
446            return Err(HandleOperationError::new(error, self));
447        }
448        self.mark_consumed();
449        Ok(maplibre_core::events::offline_region_status_from_native(
450            raw,
451        ))
452    }
453}
454
455impl RuntimeHandle {
456    /// Creates a runtime on the current thread using explicit options.
457    pub fn with_options(options: &RuntimeOptions) -> Result<Self> {
458        maplibre_core::validate_abi_version()?;
459        let native_options = options.to_native()?;
460        let raw_options = native_options.to_raw();
461        Self::create_with_native_options_after_abi_validation(&raw_options)
462    }
463
464    #[cfg(test)]
465    fn create_with_native_options_after_abi_version_check_for_testing(
466        options: *const sys::mln_runtime_options,
467        actual_abi_version: u32,
468    ) -> Result<Self> {
469        maplibre_core::validate_abi_version_value(actual_abi_version)?;
470        Self::create_with_native_options_after_abi_validation(options)
471    }
472
473    fn create_with_native_options_after_abi_validation(
474        options: *const sys::mln_runtime_options,
475    ) -> Result<Self> {
476        let mut out = maplibre_core::ptr::OutPtr::<sys::mln_runtime>::new();
477        // SAFETY: options is either null to request native defaults or points to
478        // a materialized mln_runtime_options value whose backing strings live
479        // for this call. out is a valid null-initialized out-pointer owned by
480        // this call.
481        maplibre_core::check(unsafe { sys::mln_runtime_create(options, out.as_mut_ptr()) })?;
482        let ptr = out_handle(out, "mln_runtime")?;
483
484        Ok(Self {
485            inner: Rc::new(RuntimeState::new(ptr)),
486        })
487    }
488
489    /// Installs or replaces the runtime-scoped network resource provider.
490    ///
491    /// The provider must be installed before creating maps from this runtime.
492    /// Native code may invoke it from worker or network threads, so the closure
493    /// must be thread-safe and `'static`. Keep the closure quick, and do not
494    /// call map or runtime APIs from it. Return `PassThrough` to let native
495    /// networking handle the request. Return `Handle` to complete or release
496    /// the provided `ResourceRequestHandle` inline or later. If the callback
497    /// completes the handle inline, the wrapper returns native `Handle` even
498    /// when the closure returns `PassThrough`, preventing native double
499    /// handling.
500    pub fn set_resource_provider<F>(&self, callback: F) -> Result<()>
501    where
502        F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
503            + Send
504            + Sync
505            + 'static,
506    {
507        self.inner.set_resource_provider(callback)
508    }
509
510    /// Installs or replaces the runtime-scoped network URL transform.
511    ///
512    /// The transform may be installed before or after creating maps from this
513    /// runtime. Native code may invoke it from worker or network threads, so
514    /// the closure must be thread-safe and `'static`. Keep the closure quick,
515    /// and do not call MapLibre Native APIs from it. Returning `Some(url)`
516    /// replaces the request URL; returning `None` or an empty string keeps the
517    /// original URL. Panics are contained and treated by native code as no
518    /// rewrite.
519    pub fn set_resource_transform<F>(&self, callback: F) -> Result<()>
520    where
521        F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
522    {
523        self.inner.set_resource_transform(callback)
524    }
525
526    /// Clears the runtime-scoped network URL transform.
527    ///
528    /// Clearing may happen before or after creating maps from this runtime.
529    /// Native clear waits for in-flight transform callbacks before returning,
530    /// so this method can release Rust callback state after a successful clear.
531    pub fn clear_resource_transform(&self) -> Result<()> {
532        self.inner.clear_resource_transform()
533    }
534
535    fn start_operation<T>(
536        &self,
537        operation_id: sys::mln_offline_operation_id,
538        operation_kind: maplibre_core::OfflineOperationKind,
539        result_kind: maplibre_core::OfflineOperationResultKind,
540    ) -> Result<OfflineOperationHandle<T>> {
541        OfflineOperationHandle::new(
542            Rc::clone(&self.inner),
543            operation_id,
544            operation_kind,
545            result_kind,
546        )
547    }
548
549    /// Starts an ambient cache maintenance operation for this runtime.
550    pub fn start_ambient_cache_operation(
551        &self,
552        operation: AmbientCacheOperation,
553    ) -> Result<OfflineOperationHandle<()>> {
554        let runtime = self.inner.as_ptr()?;
555        let mut operation_id: sys::mln_offline_operation_id = 0;
556        maplibre_core::check(unsafe {
557            sys::mln_runtime_run_ambient_cache_operation_start(
558                runtime,
559                operation.to_native(),
560                &mut operation_id,
561            )
562        })?;
563        self.start_operation(
564            operation_id,
565            maplibre_core::OfflineOperationKind::AmbientCache,
566            maplibre_core::OfflineOperationResultKind::None,
567        )
568    }
569
570    /// Starts creating an offline region.
571    pub fn start_create_offline_region(
572        &self,
573        definition: &OfflineRegionDefinition,
574        metadata: &[u8],
575    ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
576        let runtime = self.inner.as_ptr()?;
577        let definition = definition.to_native()?;
578        let raw_definition = definition.to_raw();
579        let mut operation_id: sys::mln_offline_operation_id = 0;
580        // SAFETY: runtime is live. raw_definition points into definition-owned
581        // string and geometry storage, metadata storage is valid for this call.
582        maplibre_core::check(unsafe {
583            sys::mln_runtime_offline_region_create_start(
584                runtime,
585                &raw_definition,
586                maplibre_core::runtime::metadata_ptr(metadata),
587                metadata.len(),
588                &mut operation_id,
589            )
590        })?;
591        self.start_operation(
592            operation_id,
593            maplibre_core::OfflineOperationKind::RegionCreate,
594            maplibre_core::OfflineOperationResultKind::Region,
595        )
596    }
597
598    /// Starts getting an offline region snapshot by ID.
599    pub fn start_offline_region(
600        &self,
601        region_id: i64,
602    ) -> Result<OfflineOperationHandle<Option<OfflineRegionInfo>>> {
603        let runtime = self.inner.as_ptr()?;
604        let mut operation_id: sys::mln_offline_operation_id = 0;
605        // SAFETY: runtime is live and operation_id points to writable storage.
606        maplibre_core::check(unsafe {
607            sys::mln_runtime_offline_region_get_start(runtime, region_id, &mut operation_id)
608        })?;
609        self.start_operation(
610            operation_id,
611            maplibre_core::OfflineOperationKind::RegionGet,
612            maplibre_core::OfflineOperationResultKind::OptionalRegion,
613        )
614    }
615
616    /// Starts listing offline regions in this runtime's database.
617    pub fn start_offline_regions(&self) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
618        let runtime = self.inner.as_ptr()?;
619        let mut operation_id: sys::mln_offline_operation_id = 0;
620        // SAFETY: runtime is live and operation_id points to writable storage.
621        maplibre_core::check(unsafe {
622            sys::mln_runtime_offline_regions_list_start(runtime, &mut operation_id)
623        })?;
624        self.start_operation(
625            operation_id,
626            maplibre_core::OfflineOperationKind::RegionsList,
627            maplibre_core::OfflineOperationResultKind::RegionList,
628        )
629    }
630
631    /// Starts merging offline regions from another database path.
632    pub fn start_merge_offline_regions_database(
633        &self,
634        path: &str,
635    ) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
636        let runtime = self.inner.as_ptr()?;
637        let path = maplibre_core::string::c_string(path)?;
638        let mut operation_id: sys::mln_offline_operation_id = 0;
639        // SAFETY: runtime is live, path is NUL-terminated and valid for this
640        // call, and operation_id points to writable storage.
641        maplibre_core::check(unsafe {
642            sys::mln_runtime_offline_regions_merge_database_start(
643                runtime,
644                path.as_ptr(),
645                &mut operation_id,
646            )
647        })?;
648        self.start_operation(
649            operation_id,
650            maplibre_core::OfflineOperationKind::RegionsMergeDatabase,
651            maplibre_core::OfflineOperationResultKind::RegionList,
652        )
653    }
654
655    /// Starts updating opaque metadata for an offline region.
656    pub fn start_update_offline_region_metadata(
657        &self,
658        region_id: i64,
659        metadata: &[u8],
660    ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
661        let runtime = self.inner.as_ptr()?;
662        let mut operation_id: sys::mln_offline_operation_id = 0;
663        // SAFETY: runtime is live, metadata storage is valid for this call, and
664        // operation_id points to writable storage.
665        maplibre_core::check(unsafe {
666            sys::mln_runtime_offline_region_update_metadata_start(
667                runtime,
668                region_id,
669                maplibre_core::runtime::metadata_ptr(metadata),
670                metadata.len(),
671                &mut operation_id,
672            )
673        })?;
674        self.start_operation(
675            operation_id,
676            maplibre_core::OfflineOperationKind::RegionUpdateMetadata,
677            maplibre_core::OfflineOperationResultKind::Region,
678        )
679    }
680
681    /// Starts getting the current completed/download status for an offline region.
682    pub fn start_offline_region_status(
683        &self,
684        region_id: i64,
685    ) -> Result<OfflineOperationHandle<OfflineRegionStatus>> {
686        let runtime = self.inner.as_ptr()?;
687        let mut operation_id: sys::mln_offline_operation_id = 0;
688        // SAFETY: runtime is live and operation_id points to writable storage.
689        maplibre_core::check(unsafe {
690            sys::mln_runtime_offline_region_get_status_start(runtime, region_id, &mut operation_id)
691        })?;
692        self.start_operation(
693            operation_id,
694            maplibre_core::OfflineOperationKind::RegionGetStatus,
695            maplibre_core::OfflineOperationResultKind::RegionStatus,
696        )
697    }
698
699    /// Starts enabling or disabling runtime events for an offline region.
700    pub fn start_set_offline_region_observed(
701        &self,
702        region_id: i64,
703        observed: bool,
704    ) -> Result<OfflineOperationHandle<()>> {
705        let runtime = self.inner.as_ptr()?;
706        let mut operation_id: sys::mln_offline_operation_id = 0;
707        maplibre_core::check(unsafe {
708            sys::mln_runtime_offline_region_set_observed_start(
709                runtime,
710                region_id,
711                observed,
712                &mut operation_id,
713            )
714        })?;
715        self.start_operation(
716            operation_id,
717            maplibre_core::OfflineOperationKind::RegionSetObserved,
718            maplibre_core::OfflineOperationResultKind::None,
719        )
720    }
721
722    /// Starts setting an offline region's native download state.
723    pub fn start_set_offline_region_download_state(
724        &self,
725        region_id: i64,
726        state: OfflineRegionDownloadState,
727    ) -> Result<OfflineOperationHandle<()>> {
728        let runtime = self.inner.as_ptr()?;
729        let state = state.raw_for_set()?;
730        let mut operation_id: sys::mln_offline_operation_id = 0;
731        maplibre_core::check(unsafe {
732            sys::mln_runtime_offline_region_set_download_state_start(
733                runtime,
734                region_id,
735                state,
736                &mut operation_id,
737            )
738        })?;
739        self.start_operation(
740            operation_id,
741            maplibre_core::OfflineOperationKind::RegionSetDownloadState,
742            maplibre_core::OfflineOperationResultKind::None,
743        )
744    }
745
746    /// Starts invalidating cached resources for an offline region.
747    pub fn start_invalidate_offline_region(
748        &self,
749        region_id: i64,
750    ) -> Result<OfflineOperationHandle<()>> {
751        let runtime = self.inner.as_ptr()?;
752        let mut operation_id: sys::mln_offline_operation_id = 0;
753        maplibre_core::check(unsafe {
754            sys::mln_runtime_offline_region_invalidate_start(runtime, region_id, &mut operation_id)
755        })?;
756        self.start_operation(
757            operation_id,
758            maplibre_core::OfflineOperationKind::RegionInvalidate,
759            maplibre_core::OfflineOperationResultKind::None,
760        )
761    }
762
763    /// Starts deleting an offline region.
764    pub fn start_delete_offline_region(
765        &self,
766        region_id: i64,
767    ) -> Result<OfflineOperationHandle<()>> {
768        let runtime = self.inner.as_ptr()?;
769        let mut operation_id: sys::mln_offline_operation_id = 0;
770        maplibre_core::check(unsafe {
771            sys::mln_runtime_offline_region_delete_start(runtime, region_id, &mut operation_id)
772        })?;
773        self.start_operation(
774            operation_id,
775            maplibre_core::OfflineOperationKind::RegionDelete,
776            maplibre_core::OfflineOperationResultKind::None,
777        )
778    }
779
780    /// Runs one pending owner-thread task for this runtime.
781    pub fn run_once(&self) -> Result<()> {
782        let runtime = self.inner.as_ptr()?;
783        // SAFETY: runtime is a live runtime handle owned by this wrapper.
784        maplibre_core::check(unsafe { sys::mln_runtime_run_once(runtime) })
785    }
786
787    /// Polls one queued runtime event and copies it into an owned Rust value.
788    pub fn poll_event(&self) -> Result<Option<RuntimeEvent>> {
789        let runtime = self.inner.as_ptr()?;
790        let mut event = empty_runtime_event();
791        let mut has_event = false;
792
793        // SAFETY: runtime is live, event points to initialized writable storage
794        // with a valid size field, and has_event points to writable bool storage.
795        maplibre_core::check(unsafe {
796            sys::mln_runtime_poll_event(runtime, &mut event, &mut has_event)
797        })?;
798        if !has_event {
799            return Ok(None);
800        }
801
802        let raw_event = event;
803        let source = self.inner.source_for_event(&raw_event);
804        let event = RuntimeEvent::from_native(&raw_event, source)?;
805        self.inner.apply_event_side_effects(&raw_event);
806        Ok(Some(event))
807    }
808
809    /// Explicitly destroys the runtime.
810    ///
811    /// Native destruction errors are returned. When destruction fails, the
812    /// underlying native handle remains live in the shared state so child
813    /// handles that retain the runtime can still close safely.
814    pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
815        if self.inner.is_closed() {
816            return Ok(());
817        }
818        if Rc::strong_count(&self.inner) > 1 {
819            return Err(HandleOperationError::new(
820                Error::new(
821                    ErrorKind::InvalidState,
822                    None,
823                    "RuntimeHandle cannot close while child handles are live",
824                ),
825                self,
826            ));
827        }
828        self.inner
829            .close()
830            .map_err(|error| HandleOperationError::new(error, self))
831    }
832}
833
834#[cfg(test)]
835mod tests {
836    use std::io::{Read, Write};
837    use std::net::TcpListener;
838    use std::sync::Arc;
839    use std::sync::atomic::{AtomicUsize, Ordering};
840    use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
841
842    use super::*;
843    use crate::{
844        ErrorKind, OfflineOperationCompletedEvent, ResourceErrorReason, ResourceKind,
845        ResourceProviderDecision, ResourceResponse, RuntimeEventPayload, RuntimeEventSource,
846        RuntimeEventType,
847    };
848    use maplibre_core::{OfflineOperationKind as Op, OfflineOperationResultKind as OpResult};
849
850    const PROVIDER_STYLE_JSON: &str = r#"{"version":8,"sources":{},"layers":[]}"#;
851
852    fn spawn_style_server(
853        request_count: usize,
854    ) -> (
855        String,
856        std::sync::mpsc::Receiver<String>,
857        std::thread::JoinHandle<()>,
858    ) {
859        let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
860        let base_url = format!("http://{}", listener.local_addr().unwrap());
861        let (sender, receiver) = std::sync::mpsc::channel();
862        let handle = std::thread::spawn(move || {
863            for _ in 0..request_count {
864                let (mut stream, _) = listener.accept().unwrap();
865                stream
866                    .set_read_timeout(Some(Duration::from_secs(5)))
867                    .unwrap();
868                let mut request = [0; 4096];
869                let bytes = stream.read(&mut request).unwrap();
870                let request = String::from_utf8_lossy(&request[..bytes]);
871                let path = request
872                    .lines()
873                    .next()
874                    .and_then(|line| line.split_whitespace().nth(1))
875                    .unwrap_or("")
876                    .to_owned();
877                sender.send(path).unwrap();
878
879                let body = PROVIDER_STYLE_JSON.as_bytes();
880                write!(
881                    stream,
882                    "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
883                    body.len()
884                )
885                .unwrap();
886                stream.write_all(body).unwrap();
887            }
888        });
889        (base_url, receiver, handle)
890    }
891
892    fn wait_for_operation<T>(
893        runtime: &RuntimeHandle,
894        operation: &OfflineOperationHandle<T>,
895        operation_kind: Op,
896        result_kind: OpResult,
897    ) -> Result<OfflineOperationCompletedEvent> {
898        let deadline = Instant::now() + Duration::from_secs(30);
899        loop {
900            if Instant::now() >= deadline {
901                return Err(Error::new(
902                    ErrorKind::InvalidState,
903                    None,
904                    format!(
905                        "timed out waiting for offline operation {:?}/{:?} with id {}",
906                        operation_kind, result_kind, operation.operation_id
907                    ),
908                ));
909            }
910            runtime.run_once()?;
911            while let Some(event) = runtime.poll_event()? {
912                let RuntimeEventPayload::OfflineOperationCompleted(completed) = event.payload
913                else {
914                    continue;
915                };
916                if completed.operation_id != operation.operation_id {
917                    continue;
918                }
919                assert_eq!(completed.operation_kind, operation_kind);
920                assert_eq!(completed.raw_operation_kind, operation_kind.raw_value());
921                assert_eq!(completed.result_kind, result_kind);
922                assert_eq!(completed.raw_result_kind, result_kind.raw_value());
923                if completed.result_status != sys::MLN_STATUS_OK {
924                    return Err(Error::from_status_and_diagnostic(
925                        completed.result_status,
926                        event.message.unwrap_or_default(),
927                    ));
928                }
929                return Ok(completed);
930            }
931            std::thread::sleep(Duration::from_millis(1));
932        }
933    }
934
935    #[test]
936    // Spec coverage: BND-084.
937    fn runtime_ambient_cache_operations_use_real_c_abi() {
938        let base = TempDir::new("maplibre-rust-ambient-cache");
939        let cache = base.path().join("ambient.db");
940
941        let mut options = RuntimeOptions::default();
942        options.cache_path = Some(cache.to_string_lossy().into_owned());
943        options.maximum_cache_size = Some(0);
944        let runtime = RuntimeHandle::with_options(&options).unwrap();
945
946        for operation in [
947            AmbientCacheOperation::PackDatabase,
948            AmbientCacheOperation::Invalidate,
949            AmbientCacheOperation::Clear,
950            AmbientCacheOperation::ResetDatabase,
951        ] {
952            let operation = runtime.start_ambient_cache_operation(operation).unwrap();
953            let completed =
954                wait_for_operation(&runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
955            assert_eq!(completed.operation_id, operation.operation_id);
956            operation.discard().unwrap();
957        }
958
959        runtime.close().unwrap();
960    }
961
962    #[test]
963    // Spec coverage: BND-084.
964    fn offline_take_result_failure_returns_live_handle() {
965        let mut options = RuntimeOptions::default();
966        options.cache_path = Some(":memory:".into());
967        let runtime = RuntimeHandle::with_options(&options).unwrap();
968        let ambient = runtime
969            .start_ambient_cache_operation(AmbientCacheOperation::Clear)
970            .unwrap();
971        let region_result = runtime
972            .start_operation::<OfflineRegionInfo>(
973                ambient.operation_id,
974                maplibre_core::OfflineOperationKind::RegionCreate,
975                maplibre_core::OfflineOperationResultKind::Region,
976            )
977            .unwrap();
978
979        let error = region_result.take().unwrap_err();
980
981        assert_eq!(error.kind(), ErrorKind::InvalidState);
982        let region_result = error.into_retryable().unwrap().into_handle();
983        region_result.discard().unwrap();
984        drop(ambient);
985        runtime.close().unwrap();
986    }
987
988    #[test]
989    // Spec coverage: BND-084 and BND-085.
990    fn offline_region_apis_use_real_c_abi() {
991        let mut options = RuntimeOptions::default();
992        options.cache_path = Some(":memory:".into());
993        let runtime = RuntimeHandle::with_options(&options).unwrap();
994        let definition = test_offline_region_definition("custom://offline-style.json");
995
996        let create = runtime
997            .start_create_offline_region(&definition, b"abc")
998            .unwrap();
999        wait_for_operation(&runtime, &create, Op::RegionCreate, OpResult::Region).unwrap();
1000        let created = create.take().unwrap();
1001        assert_eq!(created.definition, definition);
1002        assert_eq!(created.metadata, b"abc");
1003
1004        let geometry_definition = OfflineRegionDefinition::GeometryRegion {
1005            style_url: "custom://offline-geometry-style.json".into(),
1006            geometry: Geometry::Point(crate::LatLng::new(37.5, -122.5)),
1007            min_zoom: 0.0,
1008            max_zoom: 1.0,
1009            pixel_ratio: 1.0,
1010            include_ideographs: false,
1011        };
1012        let create_geometry = runtime
1013            .start_create_offline_region(&geometry_definition, b"geo")
1014            .unwrap();
1015        wait_for_operation(
1016            &runtime,
1017            &create_geometry,
1018            Op::RegionCreate,
1019            OpResult::Region,
1020        )
1021        .unwrap();
1022        let geometry_region = create_geometry.take().unwrap();
1023        assert_eq!(geometry_region.definition, geometry_definition);
1024        assert_eq!(geometry_region.metadata, b"geo");
1025
1026        let get = runtime.start_offline_region(created.id).unwrap();
1027        wait_for_operation(&runtime, &get, Op::RegionGet, OpResult::OptionalRegion).unwrap();
1028        let fetched = get.take().unwrap().unwrap();
1029        assert_eq!(fetched, created);
1030
1031        let list = runtime.start_offline_regions().unwrap();
1032        wait_for_operation(&runtime, &list, Op::RegionsList, OpResult::RegionList).unwrap();
1033        let listed = list.take().unwrap();
1034        assert!(listed.iter().any(|region| region.id == created.id));
1035
1036        let update = runtime
1037            .start_update_offline_region_metadata(created.id, b"")
1038            .unwrap();
1039        wait_for_operation(
1040            &runtime,
1041            &update,
1042            Op::RegionUpdateMetadata,
1043            OpResult::Region,
1044        )
1045        .unwrap();
1046        let updated = update.take().unwrap();
1047        assert_eq!(updated.id, created.id);
1048        assert!(updated.metadata.is_empty());
1049
1050        let status_operation = runtime.start_offline_region_status(created.id).unwrap();
1051        wait_for_operation(
1052            &runtime,
1053            &status_operation,
1054            Op::RegionGetStatus,
1055            OpResult::RegionStatus,
1056        )
1057        .unwrap();
1058        let status = status_operation.take().unwrap();
1059        assert!(matches!(
1060            status.download_state,
1061            OfflineRegionDownloadState::Inactive | OfflineRegionDownloadState::Active
1062        ));
1063
1064        let set_inactive = runtime
1065            .start_set_offline_region_download_state(
1066                created.id,
1067                OfflineRegionDownloadState::Inactive,
1068            )
1069            .unwrap();
1070        wait_for_operation(
1071            &runtime,
1072            &set_inactive,
1073            Op::RegionSetDownloadState,
1074            OpResult::None,
1075        )
1076        .unwrap();
1077        set_inactive.discard().unwrap();
1078        let error = runtime
1079            .start_set_offline_region_download_state(
1080                created.id,
1081                OfflineRegionDownloadState::Unknown(99),
1082            )
1083            .unwrap_err();
1084        assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1085
1086        let observe = runtime
1087            .start_set_offline_region_observed(created.id, true)
1088            .unwrap();
1089        wait_for_operation(&runtime, &observe, Op::RegionSetObserved, OpResult::None).unwrap();
1090        observe.discard().unwrap();
1091        let unobserve = runtime
1092            .start_set_offline_region_observed(created.id, false)
1093            .unwrap();
1094        wait_for_operation(&runtime, &unobserve, Op::RegionSetObserved, OpResult::None).unwrap();
1095        unobserve.discard().unwrap();
1096        let invalidate = runtime.start_invalidate_offline_region(created.id).unwrap();
1097        wait_for_operation(&runtime, &invalidate, Op::RegionInvalidate, OpResult::None).unwrap();
1098        invalidate.discard().unwrap();
1099        let delete = runtime.start_delete_offline_region(created.id).unwrap();
1100        wait_for_operation(&runtime, &delete, Op::RegionDelete, OpResult::None).unwrap();
1101        delete.discard().unwrap();
1102        let delete_geometry = runtime
1103            .start_delete_offline_region(geometry_region.id)
1104            .unwrap();
1105        wait_for_operation(&runtime, &delete_geometry, Op::RegionDelete, OpResult::None).unwrap();
1106        delete_geometry.discard().unwrap();
1107
1108        let missing_created = runtime.start_offline_region(created.id).unwrap();
1109        wait_for_operation(
1110            &runtime,
1111            &missing_created,
1112            Op::RegionGet,
1113            OpResult::OptionalRegion,
1114        )
1115        .unwrap();
1116        assert!(missing_created.take().unwrap().is_none());
1117        let missing_geometry = runtime.start_offline_region(geometry_region.id).unwrap();
1118        wait_for_operation(
1119            &runtime,
1120            &missing_geometry,
1121            Op::RegionGet,
1122            OpResult::OptionalRegion,
1123        )
1124        .unwrap();
1125        assert!(missing_geometry.take().unwrap().is_none());
1126
1127        runtime.close().unwrap();
1128    }
1129
1130    #[test]
1131    // Spec coverage: BND-084.
1132    fn offline_region_merge_database_uses_real_c_abi() {
1133        let base = TempDir::new("maplibre-rust-offline-merge");
1134        let main_cache = base.path().join("main.db");
1135        let side_cache = base.path().join("side.db");
1136
1137        let definition = test_offline_region_definition("custom://merge-style.json");
1138        {
1139            let mut side_options = RuntimeOptions::default();
1140            side_options.cache_path = Some(side_cache.to_string_lossy().into_owned());
1141            let side_runtime = RuntimeHandle::with_options(&side_options).unwrap();
1142            let create = side_runtime
1143                .start_create_offline_region(&definition, b"merge")
1144                .unwrap();
1145            wait_for_operation(&side_runtime, &create, Op::RegionCreate, OpResult::Region).unwrap();
1146            create.take().unwrap();
1147            side_runtime.close().unwrap();
1148        }
1149
1150        let mut main_options = RuntimeOptions::default();
1151        main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1152        let main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1153        let merge = main_runtime
1154            .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1155            .unwrap();
1156        wait_for_operation(
1157            &main_runtime,
1158            &merge,
1159            Op::RegionsMergeDatabase,
1160            OpResult::RegionList,
1161        )
1162        .unwrap();
1163        let merged = merge.take().unwrap();
1164        assert_eq!(merged.len(), 1);
1165        assert_eq!(merged[0].definition, definition);
1166        assert_eq!(merged[0].metadata, b"merge");
1167        main_runtime.close().unwrap();
1168    }
1169
1170    fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1171        OfflineRegionDefinition::TilePyramid {
1172            style_url: style_url.into(),
1173            bounds: LatLngBounds::new(
1174                crate::LatLng::new(37.0, -123.0),
1175                crate::LatLng::new(38.0, -122.0),
1176            ),
1177            min_zoom: 0.0,
1178            max_zoom: 1.0,
1179            pixel_ratio: 1.0,
1180            include_ideographs: false,
1181        }
1182    }
1183
1184    struct TempDir {
1185        path: std::path::PathBuf,
1186    }
1187
1188    impl TempDir {
1189        fn new(prefix: &str) -> Self {
1190            let nanos = SystemTime::now()
1191                .duration_since(UNIX_EPOCH)
1192                .unwrap()
1193                .as_nanos();
1194            let path =
1195                std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1196            std::fs::create_dir_all(&path).unwrap();
1197            Self { path }
1198        }
1199
1200        fn path(&self) -> &std::path::Path {
1201            &self.path
1202        }
1203    }
1204
1205    impl Drop for TempDir {
1206        fn drop(&mut self) {
1207            let _ = std::fs::remove_dir_all(&self.path);
1208        }
1209    }
1210
1211    #[test]
1212    // Spec coverage: BND-040.
1213    fn runtime_create_with_explicit_options_uses_real_c_abi() {
1214        let mut options = RuntimeOptions::default();
1215        options.asset_path = Some(String::new());
1216        options.cache_path = Some(String::new());
1217        options.maximum_cache_size = Some(0);
1218        let runtime = RuntimeHandle::with_options(&options).unwrap();
1219
1220        runtime.run_once().unwrap();
1221        runtime.close().unwrap();
1222    }
1223
1224    #[test]
1225    // Spec coverage: BND-001.
1226    fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1227        let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1228            std::ptr::null(),
1229            maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1230        )
1231        .unwrap_err();
1232
1233        assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1234        assert_eq!(error.raw_status(), None);
1235        assert!(
1236            error
1237                .diagnostic()
1238                .contains("unsupported MapLibre Native C ABI version")
1239        );
1240    }
1241
1242    fn wait_for_runtime_event(runtime: &RuntimeHandle, event_type: RuntimeEventType) -> bool {
1243        for _ in 0..100 {
1244            let _ = runtime.run_once();
1245            while let Ok(Some(event)) = runtime.poll_event() {
1246                if event.event_type == event_type {
1247                    return true;
1248                }
1249            }
1250            std::thread::sleep(Duration::from_millis(10));
1251        }
1252        false
1253    }
1254
1255    #[test]
1256    // Spec coverage: BND-080.
1257    fn runtime_create_run_poll_drain_and_close() {
1258        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1259
1260        runtime.run_once().unwrap();
1261        let _ = runtime.poll_event().unwrap();
1262        let _ = runtime.poll_event().unwrap();
1263        while runtime.poll_event().unwrap().is_some() {}
1264        runtime.close().unwrap();
1265    }
1266
1267    #[test]
1268    // Spec coverage: BND-086.
1269    fn unregistered_map_event_source_becomes_unknown_map() {
1270        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1271        let mut raw = empty_runtime_event();
1272        raw.type_ = sys::MLN_RUNTIME_EVENT_MAP_STYLE_LOADED;
1273        raw.source_type = sys::MLN_RUNTIME_EVENT_SOURCE_MAP;
1274        raw.source = 0x1234usize as *mut std::ffi::c_void;
1275
1276        let source = runtime.inner.source_for_event(&raw);
1277        let event = RuntimeEvent::from_native(&raw, source).unwrap();
1278
1279        assert_eq!(event.source, RuntimeEventSource::UnknownMap);
1280        assert_eq!(event.event_type, RuntimeEventType::MapStyleLoaded);
1281        runtime.close().unwrap();
1282    }
1283
1284    #[test]
1285    // Spec coverage: BND-020, BND-022, BND-190, and BND-191.
1286    fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1287        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1288        let runtime_ptr = runtime.inner.as_ptr().unwrap() as usize;
1289
1290        let error = std::thread::spawn(move || {
1291            // SAFETY: This intentionally exercises the C API's owner-thread
1292            // validation path with a live runtime handle from another thread.
1293            maplibre_core::check(unsafe {
1294                sys::mln_runtime_run_once(runtime_ptr as *mut sys::mln_runtime)
1295            })
1296            .unwrap_err()
1297        })
1298        .join()
1299        .unwrap();
1300
1301        assert_eq!(error.kind(), ErrorKind::WrongThread);
1302        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1303        assert!(!error.diagnostic().is_empty());
1304        runtime.close().unwrap();
1305    }
1306
1307    #[test]
1308    // Spec coverage: BND-123.
1309    fn resource_provider_installs_replaces_and_releases_state() {
1310        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1311        let first = Arc::new(());
1312        let first_callback = Arc::clone(&first);
1313
1314        runtime
1315            .set_resource_provider(move |_, _| {
1316                let _ = &first_callback;
1317                crate::ResourceProviderDecision::PassThrough
1318            })
1319            .unwrap();
1320        assert_eq!(Arc::strong_count(&first), 2);
1321
1322        let second = Arc::new(());
1323        let second_callback = Arc::clone(&second);
1324        runtime
1325            .set_resource_provider(move |_, _| {
1326                let _ = &second_callback;
1327                crate::ResourceProviderDecision::PassThrough
1328            })
1329            .unwrap();
1330        assert_eq!(Arc::strong_count(&first), 1);
1331        assert_eq!(Arc::strong_count(&second), 2);
1332
1333        runtime.close().unwrap();
1334        assert_eq!(Arc::strong_count(&second), 1);
1335    }
1336
1337    #[test]
1338    // Spec coverage: BND-122.
1339    fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1340        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1341        let first = Arc::new(());
1342        let first_callback = Arc::clone(&first);
1343        runtime
1344            .set_resource_provider(move |_, _| {
1345                let _ = &first_callback;
1346                crate::ResourceProviderDecision::PassThrough
1347            })
1348            .unwrap();
1349        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1350
1351        let second = Arc::new(());
1352        let second_callback = Arc::clone(&second);
1353        let error = runtime
1354            .set_resource_provider(move |_, _| {
1355                let _ = &second_callback;
1356                crate::ResourceProviderDecision::PassThrough
1357            })
1358            .unwrap_err();
1359
1360        assert_eq!(error.kind(), ErrorKind::InvalidState);
1361        assert_eq!(Arc::strong_count(&first), 2);
1362        assert_eq!(Arc::strong_count(&second), 1);
1363
1364        map.close().unwrap();
1365        runtime.close().unwrap();
1366        assert_eq!(Arc::strong_count(&first), 1);
1367    }
1368
1369    #[test]
1370    // Rust regression: enforces Rust's callback registration guard after a
1371    // map has made runtime-scoped provider replacement unsafe.
1372    fn resource_provider_rejects_install_after_map_was_closed() {
1373        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1374        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1375        map.close().unwrap();
1376
1377        let error = runtime
1378            .set_resource_provider(|_, _| ResourceProviderDecision::PassThrough)
1379            .unwrap_err();
1380
1381        assert_eq!(error.kind(), ErrorKind::InvalidState);
1382        assert_eq!(error.raw_status(), None);
1383        runtime.close().unwrap();
1384    }
1385
1386    #[test]
1387    // Spec coverage: BND-143 and BND-150.
1388    fn resource_provider_completes_style_request_inline_through_c_abi() {
1389        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1390        let calls = Arc::new(AtomicUsize::new(0));
1391        let callback_calls = Arc::clone(&calls);
1392        runtime
1393            .set_resource_provider(move |request, handle| {
1394                if request.url != "custom://style.json" {
1395                    return ResourceProviderDecision::PassThrough;
1396                }
1397                callback_calls.fetch_add(1, Ordering::SeqCst);
1398                assert_eq!(request.kind, ResourceKind::Style);
1399                handle
1400                    .complete(ResourceResponse::ok(
1401                        PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1402                    ))
1403                    .unwrap();
1404                ResourceProviderDecision::PassThrough
1405            })
1406            .unwrap();
1407
1408        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1409        map.set_style_url("custom://style.json").unwrap();
1410
1411        assert!(wait_for_runtime_event(
1412            &runtime,
1413            RuntimeEventType::MapStyleLoaded
1414        ));
1415        assert_eq!(calls.load(Ordering::SeqCst), 1);
1416        map.close().unwrap();
1417        runtime.close().unwrap();
1418    }
1419
1420    #[test]
1421    // Spec coverage: BND-144 and BND-145.
1422    fn resource_provider_completes_style_request_from_another_thread() {
1423        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1424        let (sender, receiver) = std::sync::mpsc::channel();
1425        runtime
1426            .set_resource_provider(move |request, handle| {
1427                if request.url == "custom://async-style.json" {
1428                    sender.send(handle).unwrap();
1429                    ResourceProviderDecision::Handle
1430                } else {
1431                    ResourceProviderDecision::PassThrough
1432                }
1433            })
1434            .unwrap();
1435
1436        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1437        map.set_style_url("custom://async-style.json").unwrap();
1438        let handle = receiver
1439            .recv_timeout(Duration::from_secs(5))
1440            .expect("provider should send handled request");
1441        assert!(!handle.is_cancelled().unwrap());
1442        std::thread::spawn(move || {
1443            handle
1444                .complete(ResourceResponse::ok(
1445                    PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1446                ))
1447                .unwrap();
1448        })
1449        .join()
1450        .unwrap();
1451
1452        assert!(wait_for_runtime_event(
1453            &runtime,
1454            RuntimeEventType::MapStyleLoaded
1455        ));
1456        map.close().unwrap();
1457        runtime.close().unwrap();
1458    }
1459
1460    #[test]
1461    // Spec coverage: BND-149.
1462    fn resource_provider_error_response_becomes_copied_loading_failure_event() {
1463        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1464        runtime
1465            .set_resource_provider(move |request, handle| {
1466                if request.url == "custom://broken-style.json" {
1467                    handle
1468                        .complete(ResourceResponse::error(
1469                            ResourceErrorReason::Other,
1470                            "provider failed",
1471                        ))
1472                        .unwrap();
1473                    ResourceProviderDecision::Handle
1474                } else {
1475                    ResourceProviderDecision::PassThrough
1476                }
1477            })
1478            .unwrap();
1479
1480        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1481        let map_id = map.id();
1482        map.set_style_url("custom://broken-style.json").unwrap();
1483
1484        let mut loading_failed = None;
1485        for _ in 0..100 {
1486            runtime.run_once().unwrap();
1487            while let Some(event) = runtime.poll_event().unwrap() {
1488                if event.event_type == RuntimeEventType::MapLoadingFailed {
1489                    loading_failed = Some(event);
1490                    break;
1491                }
1492            }
1493            if loading_failed.is_some() {
1494                break;
1495            }
1496            std::thread::sleep(Duration::from_millis(10));
1497        }
1498
1499        let event = loading_failed.expect("resource error should enqueue loading-failed event");
1500        let copied_message = event.message.clone();
1501        let _ = runtime.poll_event().unwrap();
1502
1503        assert_eq!(event.source, RuntimeEventSource::Map(map_id));
1504        assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
1505        assert_eq!(event.message, copied_message);
1506        assert!(
1507            event
1508                .message
1509                .as_deref()
1510                .is_some_and(|message| message.contains("provider failed"))
1511        );
1512
1513        map.close().unwrap();
1514        runtime.close().unwrap();
1515    }
1516
1517    #[test]
1518    // Spec coverage: BND-140 and BND-123.
1519    fn resource_transform_installs_replaces_clears_and_releases_state() {
1520        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1521        let first = Arc::new(());
1522        let first_callback = Arc::clone(&first);
1523
1524        runtime
1525            .set_resource_transform(move |request| {
1526                let _ = &first_callback;
1527                assert!(matches!(
1528                    request.kind,
1529                    ResourceKind::Style | ResourceKind::UnknownRaw(_)
1530                ));
1531                None
1532            })
1533            .unwrap();
1534        assert_eq!(Arc::strong_count(&first), 2);
1535
1536        let second = Arc::new(());
1537        let second_callback = Arc::clone(&second);
1538        runtime
1539            .set_resource_transform(move |_| {
1540                let _ = &second_callback;
1541                Some("https://example.test/replacement".to_owned())
1542            })
1543            .unwrap();
1544        assert_eq!(Arc::strong_count(&first), 1);
1545        assert_eq!(Arc::strong_count(&second), 2);
1546
1547        runtime.clear_resource_transform().unwrap();
1548        assert_eq!(Arc::strong_count(&second), 1);
1549        runtime.close().unwrap();
1550    }
1551
1552    #[test]
1553    // Spec coverage: BND-140.
1554    fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
1555        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1556        let (base_url, requests, server) = spawn_style_server(2);
1557        let transform_base_url = base_url.clone();
1558
1559        runtime
1560            .set_resource_transform(move |request| {
1561                if request.url.ends_with("/original-style.json") {
1562                    Some(format!("{transform_base_url}/rewritten-style.json"))
1563                } else {
1564                    None
1565                }
1566            })
1567            .unwrap();
1568
1569        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1570        map.set_style_url(&format!("{base_url}/original-style.json"))
1571            .unwrap();
1572        assert!(wait_for_runtime_event(
1573            &runtime,
1574            RuntimeEventType::MapStyleLoaded
1575        ));
1576        assert_eq!(
1577            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
1578            "/rewritten-style.json"
1579        );
1580
1581        runtime.clear_resource_transform().unwrap();
1582        map.set_style_url(&format!("{base_url}/original-after-clear.json"))
1583            .unwrap();
1584        assert!(wait_for_runtime_event(
1585            &runtime,
1586            RuntimeEventType::MapStyleLoaded
1587        ));
1588        assert_eq!(
1589            requests.recv_timeout(Duration::from_secs(5)).unwrap(),
1590            "/original-after-clear.json"
1591        );
1592
1593        map.close().unwrap();
1594        runtime.close().unwrap();
1595        server.join().unwrap();
1596    }
1597
1598    #[test]
1599    // Spec coverage: BND-123.
1600    fn resource_transform_replacement_after_map_creation_releases_previous_state() {
1601        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1602        let first = Arc::new(());
1603        let first_callback = Arc::clone(&first);
1604        runtime
1605            .set_resource_transform(move |_| {
1606                let _ = &first_callback;
1607                None
1608            })
1609            .unwrap();
1610        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1611
1612        let second = Arc::new(());
1613        let second_callback = Arc::clone(&second);
1614        runtime
1615            .set_resource_transform(move |_| {
1616                let _ = &second_callback;
1617                None
1618            })
1619            .unwrap();
1620
1621        assert_eq!(Arc::strong_count(&first), 1);
1622        assert_eq!(Arc::strong_count(&second), 2);
1623
1624        map.close().unwrap();
1625        runtime.close().unwrap();
1626        assert_eq!(Arc::strong_count(&second), 1);
1627    }
1628
1629    #[test]
1630    // Spec coverage: BND-123.
1631    fn runtime_teardown_releases_resource_transform_state() {
1632        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1633        let token = Arc::new(());
1634        let callback_token = Arc::clone(&token);
1635        runtime
1636            .set_resource_transform(move |_| {
1637                let _ = &callback_token;
1638                None
1639            })
1640            .unwrap();
1641        assert_eq!(Arc::strong_count(&token), 2);
1642
1643        runtime.close().unwrap();
1644
1645        assert_eq!(Arc::strong_count(&token), 1);
1646    }
1647
1648    #[test]
1649    // Rust regression: documents the Rust binding's late transform-install
1650    // guard after map creation.
1651    fn resource_transform_installs_after_map_creation() {
1652        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1653        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1654
1655        runtime.set_resource_transform(|_| None).unwrap();
1656
1657        map.close().unwrap();
1658        runtime.close().unwrap();
1659    }
1660
1661    #[test]
1662    // Spec coverage: BND-140 and BND-123.
1663    fn resource_transform_clears_after_map_was_closed_and_releases_state() {
1664        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1665        let token = Arc::new(());
1666        let callback_token = Arc::clone(&token);
1667        runtime
1668            .set_resource_transform(move |_| {
1669                let _ = &callback_token;
1670                None
1671            })
1672            .unwrap();
1673        assert_eq!(Arc::strong_count(&token), 2);
1674
1675        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1676        map.close().unwrap();
1677
1678        runtime.clear_resource_transform().unwrap();
1679
1680        assert_eq!(Arc::strong_count(&token), 1);
1681
1682        runtime.close().unwrap();
1683    }
1684
1685    #[test]
1686    // Spec coverage: BND-081 and BND-082.
1687    fn poll_event_returns_owned_map_event_and_source_id() {
1688        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1689        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1690        let map_id = map.id();
1691
1692        let error = map.set_style_json("{").unwrap_err();
1693        assert_eq!(error.kind(), ErrorKind::NativeError);
1694        assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
1695
1696        let mut loading_failed = None;
1697        for _ in 0..8 {
1698            let Some(event) = runtime.poll_event().unwrap() else {
1699                break;
1700            };
1701            if event.event_type == RuntimeEventType::MapLoadingFailed {
1702                loading_failed = Some(event);
1703                break;
1704            }
1705        }
1706        let event = loading_failed.expect("malformed style should enqueue loading-failed event");
1707        let copied_message = event.message.clone();
1708
1709        let _ = runtime.poll_event().unwrap();
1710
1711        assert_eq!(event.source, RuntimeEventSource::Map(map_id));
1712        assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
1713        assert_eq!(event.message, copied_message);
1714        assert!(
1715            event
1716                .message
1717                .as_deref()
1718                .is_some_and(|message| !message.is_empty())
1719        );
1720
1721        map.close().unwrap();
1722        runtime.close().unwrap();
1723    }
1724
1725    #[test]
1726    // Spec coverage: BND-042.
1727    fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
1728        let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1729        let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1730
1731        let error = runtime.close().unwrap_err();
1732        assert_eq!(error.kind(), ErrorKind::InvalidState);
1733        assert_eq!(error.raw_status(), None);
1734        let runtime = error.into_handle();
1735
1736        runtime.run_once().unwrap();
1737        map.close().unwrap();
1738        runtime.close().unwrap();
1739    }
1740}