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 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 #[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 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 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 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 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
227pub 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
240pub 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 #[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 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 #[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 unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
369 .map_err(OfflineOperationTakeError::consumed)
370 }
371}
372
373impl OfflineOperationHandle<Option<OfflineRegionInfo>> {
374 #[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 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 #[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 unsafe { maplibre_core::runtime::copy_offline_region_list(list) }
452 .map_err(OfflineOperationTakeError::consumed)
453 }
454}
455
456impl OfflineOperationHandle<OfflineRegionStatus> {
457 #[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 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 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 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 pub fn clear_resource_provider(&self) -> Result<()> {
553 self.inner.clear_resource_provider()
554 }
555
556 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime, timeout_ms) })
860 }
861
862 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 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 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 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 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#[derive(Debug)]
937pub struct WakeSource {
938 ptr: NonNull<sys::mln_wake_source>,
939}
940
941unsafe impl Send for WakeSource {}
946unsafe impl Sync for WakeSource {}
949
950impl WakeSource {
951 pub fn signal(&self) -> Result<()> {
957 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 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 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 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 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 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 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 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 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 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 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 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 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 map.close().unwrap();
1497 runtime.close().unwrap();
1498 source.signal().unwrap();
1499 }
1500
1501 #[test]
1502 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 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 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 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 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 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 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 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 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 runtime.clear_resource_provider().unwrap();
1705
1706 map.close().unwrap();
1707 runtime.close().unwrap();
1708 }
1709
1710 #[test]
1711 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 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 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 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 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 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 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 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 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 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 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}