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