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