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