1use std::cell::{Cell, RefCell};
2use std::fmt;
3use std::marker::PhantomData;
4use std::rc::Rc;
5use std::time::Duration;
6
7use maplibre_core::AmbientCacheOperation;
8use maplibre_native_ffi_core as maplibre_core;
9use maplibre_native_ffi_sys as sys;
10
11use crate::events::{OfflineRegionDownloadState, OfflineRegionStatus, RuntimeEventBatch};
12use crate::handle::{ThreadAffineNativeHandle, closed_handle_error, out_handle};
13use crate::resource::{HttpHeaderTransformState, ResourceProviderState, ResourceTransformState};
14use crate::{
15 Error, ErrorKind, HandleOperationError, OfflineOperationTakeError, ResourceProviderDecision,
16 Result, RuntimeEventMask,
17};
18#[cfg(test)]
19use crate::{LatLngBounds, MapHandle, MapOptions};
20
21pub use maplibre_core::runtime::{OfflineRegionDefinition, OfflineRegionInfo, RuntimeOptions};
22pub(crate) use maplibre_core::runtime::{
23 OfflineRegionDefinitionNativeExt, RuntimeOptionsNativeExt,
24};
25
26#[derive(Debug)]
27pub(crate) struct RuntimeState {
28 handle: ThreadAffineNativeHandle<sys::mln_runtime>,
29 resource_transform: RefCell<Option<Box<ResourceTransformState>>>,
30 http_header_transform: RefCell<Option<Box<HttpHeaderTransformState>>>,
31 resource_provider: RefCell<Option<Box<ResourceProviderState>>>,
32}
33
34impl RuntimeState {
35 fn new(native: sys::mln_runtime) -> Result<Self> {
36 let handle = unsafe {
39 ThreadAffineNativeHandle::from_handle(native, sys::mln_runtime_destroy, "mln_runtime")
40 }?;
41 Ok(Self {
42 handle,
43 resource_transform: RefCell::new(None),
44 http_header_transform: RefCell::new(None),
45 resource_provider: RefCell::new(None),
46 })
47 }
48
49 pub(crate) fn native(&self) -> Result<sys::mln_runtime> {
50 self.handle
51 .live_handle()
52 .ok_or_else(|| closed_handle_error("RuntimeHandle"))
53 }
54
55 fn is_closed(&self) -> bool {
56 self.handle.is_closed()
57 }
58
59 fn close(&self) -> Result<()> {
60 self.handle.close()?;
61 self.resource_transform.borrow_mut().take();
62 self.http_header_transform.borrow_mut().take();
63 self.resource_provider.borrow_mut().take();
64 Ok(())
65 }
66
67 fn set_resource_provider<F>(&self, callback: F) -> Result<()>
68 where
69 F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
70 + Send
71 + Sync
72 + 'static,
73 {
74 let replacement = ResourceProviderState::new(callback);
75 let descriptor = replacement.descriptor();
76 self.install_resource_provider(replacement, descriptor)
77 }
78
79 #[cfg(test)]
82 fn set_resource_provider_with_rejected_descriptor_for_testing<F>(
83 &self,
84 callback: F,
85 ) -> Result<()>
86 where
87 F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
88 + Send
89 + Sync
90 + 'static,
91 {
92 let replacement = ResourceProviderState::new(callback);
93 let mut descriptor = replacement.descriptor();
94 descriptor.callback = None;
95 self.install_resource_provider(replacement, descriptor)
96 }
97
98 fn install_resource_provider(
99 &self,
100 replacement: Box<ResourceProviderState>,
101 descriptor: sys::mln_resource_provider,
102 ) -> Result<()> {
103 let runtime = self.native()?;
104
105 maplibre_core::check(unsafe {
109 sys::mln_runtime_set_resource_provider(runtime, &descriptor)
110 })?;
111 self.resource_provider.borrow_mut().replace(replacement);
112 Ok(())
113 }
114
115 fn clear_resource_provider(&self) -> Result<()> {
116 let runtime = self.native()?;
117
118 maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_provider(runtime) })?;
121 self.resource_provider.borrow_mut().take();
122 Ok(())
123 }
124
125 fn set_resource_transform<F>(&self, callback: F) -> Result<()>
126 where
127 F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
128 {
129 let runtime = self.native()?;
130 let replacement = ResourceTransformState::new(callback);
131 let descriptor = replacement.descriptor();
132
133 maplibre_core::check(unsafe {
137 sys::mln_runtime_set_resource_transform(runtime, &descriptor)
138 })?;
139 self.resource_transform.borrow_mut().replace(replacement);
140 Ok(())
141 }
142
143 fn clear_resource_transform(&self) -> Result<()> {
144 let runtime = self.native()?;
145
146 maplibre_core::check(unsafe { sys::mln_runtime_clear_resource_transform(runtime) })?;
149 self.resource_transform.borrow_mut().take();
150 Ok(())
151 }
152
153 fn set_http_header_transform<F>(&self, callback: F) -> Result<()>
154 where
155 F: Fn(crate::HttpHeaderTransformRequest) -> Vec<crate::HttpHeader> + Send + Sync + 'static,
156 {
157 let runtime = self.native()?;
158 let replacement = HttpHeaderTransformState::new(callback);
159 let descriptor = replacement.descriptor();
160 maplibre_core::check(unsafe {
162 sys::mln_runtime_set_http_header_transform(runtime, &descriptor)
163 })?;
164 self.http_header_transform.borrow_mut().replace(replacement);
165 Ok(())
166 }
167
168 fn clear_http_header_transform(&self) -> Result<()> {
169 let runtime = self.native()?;
170 maplibre_core::check(unsafe { sys::mln_runtime_clear_http_header_transform(runtime) })?;
172 self.http_header_transform.borrow_mut().take();
173 Ok(())
174 }
175}
176
177pub struct RuntimeHandle {
179 pub(crate) inner: Rc<RuntimeState>,
180}
181
182impl fmt::Debug for RuntimeHandle {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 f.debug_struct("RuntimeHandle")
185 .field("closed", &self.inner.is_closed())
186 .finish()
187 }
188}
189
190pub struct OfflineOperationHandle<T> {
192 runtime: Rc<RuntimeState>,
193 operation_id: sys::mln_offline_operation_id,
194 operation_kind: maplibre_core::OfflineOperationKind,
195 result_kind: maplibre_core::OfflineOperationResultKind,
196 live: Cell<bool>,
197 _result: PhantomData<fn() -> T>,
198 _thread_affine: PhantomData<Rc<()>>,
199}
200
201impl<T> fmt::Debug for OfflineOperationHandle<T> {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.debug_struct("OfflineOperationHandle")
204 .field("operation_id", &self.operation_id)
205 .field("operation_kind", &self.operation_kind)
206 .field("result_kind", &self.result_kind)
207 .field("live", &self.live.get())
208 .finish()
209 }
210}
211
212impl<T> OfflineOperationHandle<T> {
213 fn new(
214 runtime: Rc<RuntimeState>,
215 operation_id: sys::mln_offline_operation_id,
216 operation_kind: maplibre_core::OfflineOperationKind,
217 result_kind: maplibre_core::OfflineOperationResultKind,
218 ) -> Result<Self> {
219 if operation_id == 0 {
220 return Err(Error::invalid_argument(
221 "offline operation id must not be zero",
222 ));
223 }
224 Ok(Self {
225 runtime,
226 operation_id,
227 operation_kind,
228 result_kind,
229 live: Cell::new(true),
230 _result: PhantomData,
231 _thread_affine: PhantomData,
232 })
233 }
234
235 fn runtime_ptr(&self) -> Result<sys::mln_runtime> {
236 if !self.live.get() {
237 return Err(closed_handle_error("OfflineOperationHandle"));
238 }
239 self.runtime.native()
240 }
241
242 fn mark_consumed(&self) {
246 self.live.set(false);
247 }
248
249 #[allow(clippy::result_large_err)]
251 pub fn discard(self) -> std::result::Result<(), HandleOperationError<Self>> {
252 if !self.live.get() {
253 return Ok(());
254 }
255 let runtime = match self.runtime_ptr() {
256 Ok(runtime) => runtime,
257 Err(error) => return Err(HandleOperationError::new(error, self)),
258 };
259 let status =
260 unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
261 if let Err(error) = maplibre_core::check(status) {
262 return Err(HandleOperationError::new(error, self));
263 }
264 self.mark_consumed();
265 Ok(())
266 }
267}
268
269impl<T> Drop for OfflineOperationHandle<T> {
270 fn drop(&mut self) {
271 if !self.live.get() {
272 return;
273 }
274 if let Ok(runtime) = self.runtime.native() {
275 let status =
277 unsafe { sys::mln_runtime_offline_operation_discard(runtime, self.operation_id) };
278 if status == sys::MLN_STATUS_OK {
279 self.mark_consumed();
280 }
281 }
282 }
283}
284
285impl OfflineOperationHandle<OfflineRegionInfo> {
286 #[allow(clippy::result_large_err)]
288 pub fn take(self) -> std::result::Result<OfflineRegionInfo, OfflineOperationTakeError<Self>> {
289 let runtime = match self.runtime_ptr() {
290 Ok(runtime) => runtime,
291 Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
292 };
293 let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_snapshot>::new();
294 let status = match self.operation_kind {
295 maplibre_core::OfflineOperationKind::RegionCreate => unsafe {
296 sys::mln_runtime_offline_region_create_take_result(
297 runtime,
298 self.operation_id,
299 out.as_mut_ptr(),
300 )
301 },
302 maplibre_core::OfflineOperationKind::RegionUpdateMetadata => unsafe {
303 sys::mln_runtime_offline_region_update_metadata_take_result(
304 runtime,
305 self.operation_id,
306 out.as_mut_ptr(),
307 )
308 },
309 _ => sys::MLN_STATUS_INVALID_STATE,
310 };
311 if let Err(error) = maplibre_core::check(status) {
312 return Err(OfflineOperationTakeError::retryable(error, self));
313 }
314 self.mark_consumed();
315 let snapshot = match out.into_live("mln_offline_region_snapshot") {
316 Ok(snapshot) => snapshot,
317 Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
318 };
319 unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
322 .map_err(OfflineOperationTakeError::consumed)
323 }
324}
325
326impl OfflineOperationHandle<Option<OfflineRegionInfo>> {
327 #[allow(clippy::result_large_err)]
329 pub fn take(
330 self,
331 ) -> std::result::Result<Option<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
332 let runtime = match self.runtime_ptr() {
333 Ok(runtime) => runtime,
334 Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
335 };
336 let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_snapshot>::new();
337 let mut found = false;
338 let status = unsafe {
339 sys::mln_runtime_offline_region_get_take_result(
340 runtime,
341 self.operation_id,
342 out.as_mut_ptr(),
343 &mut found,
344 )
345 };
346 if let Err(error) = maplibre_core::check(status) {
347 return Err(OfflineOperationTakeError::retryable(error, self));
348 }
349 self.mark_consumed();
350 if !found {
351 return Ok(None);
352 }
353 let snapshot = match out.into_live("mln_offline_region_snapshot") {
354 Ok(snapshot) => snapshot,
355 Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
356 };
357 Ok(Some(
360 unsafe { maplibre_core::runtime::copy_offline_region_snapshot(snapshot) }
361 .map_err(OfflineOperationTakeError::consumed)?,
362 ))
363 }
364}
365
366impl OfflineOperationHandle<Vec<OfflineRegionInfo>> {
367 #[allow(clippy::result_large_err)]
369 pub fn take(
370 self,
371 ) -> std::result::Result<Vec<OfflineRegionInfo>, OfflineOperationTakeError<Self>> {
372 let runtime = match self.runtime_ptr() {
373 Ok(runtime) => runtime,
374 Err(error) => return Err(OfflineOperationTakeError::retryable(error, self)),
375 };
376 let mut out = maplibre_core::ptr::OutHandle::<sys::mln_offline_region_list>::new();
377 let status = match self.operation_kind {
378 maplibre_core::OfflineOperationKind::RegionsList => unsafe {
379 sys::mln_runtime_offline_regions_list_take_result(
380 runtime,
381 self.operation_id,
382 out.as_mut_ptr(),
383 )
384 },
385 maplibre_core::OfflineOperationKind::RegionsMergeDatabase => unsafe {
386 sys::mln_runtime_offline_regions_merge_database_take_result(
387 runtime,
388 self.operation_id,
389 out.as_mut_ptr(),
390 )
391 },
392 _ => sys::MLN_STATUS_INVALID_STATE,
393 };
394 if let Err(error) = maplibre_core::check(status) {
395 return Err(OfflineOperationTakeError::retryable(error, self));
396 }
397 self.mark_consumed();
398 let list = match out.into_live("mln_offline_region_list") {
399 Ok(list) => list,
400 Err(error) => return Err(OfflineOperationTakeError::consumed(error)),
401 };
402 unsafe { maplibre_core::runtime::copy_offline_region_list(list) }
405 .map_err(OfflineOperationTakeError::consumed)
406 }
407}
408
409impl OfflineOperationHandle<OfflineRegionStatus> {
410 #[allow(clippy::result_large_err)]
412 pub fn take(self) -> std::result::Result<OfflineRegionStatus, HandleOperationError<Self>> {
413 let runtime = match self.runtime_ptr() {
414 Ok(runtime) => runtime,
415 Err(error) => return Err(HandleOperationError::new(error, self)),
416 };
417 let mut raw = maplibre_core::events::empty_offline_region_status_native();
418 let status = unsafe {
419 sys::mln_runtime_offline_region_get_status_take_result(
420 runtime,
421 self.operation_id,
422 &mut raw,
423 )
424 };
425 if let Err(error) = maplibre_core::check(status) {
426 return Err(HandleOperationError::new(error, self));
427 }
428 self.mark_consumed();
429 Ok(maplibre_core::events::offline_region_status_from_native(
430 raw,
431 ))
432 }
433}
434
435impl RuntimeHandle {
436 pub fn with_options(options: &RuntimeOptions) -> Result<Self> {
438 maplibre_core::validate_abi_version()?;
439 let native_options = options.to_native()?;
440 let raw_options = native_options.to_raw();
441 Self::create_with_native_options_after_abi_validation(&raw_options)
442 }
443
444 #[cfg(test)]
445 fn create_with_native_options_after_abi_version_check_for_testing(
446 options: *const sys::mln_runtime_options,
447 actual_abi_version: u32,
448 ) -> Result<Self> {
449 maplibre_core::validate_abi_version_value(actual_abi_version)?;
450 Self::create_with_native_options_after_abi_validation(options)
451 }
452
453 fn create_with_native_options_after_abi_validation(
454 options: *const sys::mln_runtime_options,
455 ) -> Result<Self> {
456 let mut out = maplibre_core::ptr::OutHandle::<sys::mln_runtime>::new();
457 maplibre_core::check(unsafe { sys::mln_runtime_create(options, out.as_mut_ptr()) })?;
462 let ptr = out_handle(out, "mln_runtime")?;
463
464 Ok(Self {
465 inner: Rc::new(RuntimeState::new(ptr)?),
466 })
467 }
468
469 pub fn set_resource_provider<F>(&self, callback: F) -> Result<()>
480 where
481 F: Fn(crate::ResourceRequest, crate::ResourceRequestHandle) -> ResourceProviderDecision
482 + Send
483 + Sync
484 + 'static,
485 {
486 self.inner.set_resource_provider(callback)
487 }
488
489 pub fn clear_resource_provider(&self) -> Result<()> {
494 self.inner.clear_resource_provider()
495 }
496
497 pub fn set_resource_transform<F>(&self, callback: F) -> Result<()>
503 where
504 F: Fn(crate::ResourceTransformRequest) -> Option<String> + Send + Sync + 'static,
505 {
506 self.inner.set_resource_transform(callback)
507 }
508
509 pub fn clear_resource_transform(&self) -> Result<()> {
513 self.inner.clear_resource_transform()
514 }
515
516 pub fn set_http_header_transform<F>(&self, callback: F) -> Result<()>
523 where
524 F: Fn(crate::HttpHeaderTransformRequest) -> Vec<crate::HttpHeader> + Send + Sync + 'static,
525 {
526 self.inner.set_http_header_transform(callback)
527 }
528
529 pub fn clear_http_header_transform(&self) -> Result<()> {
531 self.inner.clear_http_header_transform()
532 }
533
534 fn start_operation<T>(
535 &self,
536 operation_id: sys::mln_offline_operation_id,
537 operation_kind: maplibre_core::OfflineOperationKind,
538 result_kind: maplibre_core::OfflineOperationResultKind,
539 ) -> Result<OfflineOperationHandle<T>> {
540 OfflineOperationHandle::new(
541 Rc::clone(&self.inner),
542 operation_id,
543 operation_kind,
544 result_kind,
545 )
546 }
547
548 pub fn start_ambient_cache_operation(
550 &self,
551 operation: AmbientCacheOperation,
552 ) -> Result<OfflineOperationHandle<()>> {
553 let runtime = self.inner.native()?;
554 let mut operation_id: sys::mln_offline_operation_id = 0;
555 maplibre_core::check(unsafe {
556 sys::mln_runtime_run_ambient_cache_operation_start(
557 runtime,
558 operation.to_native(),
559 &mut operation_id,
560 )
561 })?;
562 self.start_operation(
563 operation_id,
564 maplibre_core::OfflineOperationKind::AmbientCache,
565 maplibre_core::OfflineOperationResultKind::None,
566 )
567 }
568
569 pub fn start_set_maximum_ambient_cache_size(
574 &self,
575 size: u64,
576 ) -> Result<OfflineOperationHandle<()>> {
577 let runtime = self.inner.native()?;
578 let mut operation_id: sys::mln_offline_operation_id = 0;
579 maplibre_core::check(unsafe {
580 sys::mln_runtime_set_maximum_ambient_cache_size_start(runtime, size, &mut operation_id)
581 })?;
582 self.start_operation(
583 operation_id,
584 maplibre_core::OfflineOperationKind::SetMaximumAmbientCacheSize,
585 maplibre_core::OfflineOperationResultKind::None,
586 )
587 }
588
589 pub fn start_create_offline_region(
591 &self,
592 definition: &OfflineRegionDefinition,
593 metadata: &[u8],
594 ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
595 let runtime = self.inner.native()?;
596 let definition = definition.to_native()?;
597 let raw_definition = definition.to_raw();
598 let mut operation_id: sys::mln_offline_operation_id = 0;
599 maplibre_core::check(unsafe {
602 sys::mln_runtime_offline_region_create_start(
603 runtime,
604 &raw_definition,
605 maplibre_core::runtime::metadata_ptr(metadata),
606 metadata.len(),
607 &mut operation_id,
608 )
609 })?;
610 self.start_operation(
611 operation_id,
612 maplibre_core::OfflineOperationKind::RegionCreate,
613 maplibre_core::OfflineOperationResultKind::Region,
614 )
615 }
616
617 pub fn start_offline_region(
619 &self,
620 region_id: i64,
621 ) -> Result<OfflineOperationHandle<Option<OfflineRegionInfo>>> {
622 let runtime = self.inner.native()?;
623 let mut operation_id: sys::mln_offline_operation_id = 0;
624 maplibre_core::check(unsafe {
626 sys::mln_runtime_offline_region_get_start(runtime, region_id, &mut operation_id)
627 })?;
628 self.start_operation(
629 operation_id,
630 maplibre_core::OfflineOperationKind::RegionGet,
631 maplibre_core::OfflineOperationResultKind::OptionalRegion,
632 )
633 }
634
635 pub fn start_offline_regions(&self) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
637 let runtime = self.inner.native()?;
638 let mut operation_id: sys::mln_offline_operation_id = 0;
639 maplibre_core::check(unsafe {
641 sys::mln_runtime_offline_regions_list_start(runtime, &mut operation_id)
642 })?;
643 self.start_operation(
644 operation_id,
645 maplibre_core::OfflineOperationKind::RegionsList,
646 maplibre_core::OfflineOperationResultKind::RegionList,
647 )
648 }
649
650 pub fn start_merge_offline_regions_database(
652 &self,
653 path: &str,
654 ) -> Result<OfflineOperationHandle<Vec<OfflineRegionInfo>>> {
655 let runtime = self.inner.native()?;
656 let path = maplibre_core::string::c_string(path)?;
657 let mut operation_id: sys::mln_offline_operation_id = 0;
658 maplibre_core::check(unsafe {
661 sys::mln_runtime_offline_regions_merge_database_start(
662 runtime,
663 path.as_ptr(),
664 &mut operation_id,
665 )
666 })?;
667 self.start_operation(
668 operation_id,
669 maplibre_core::OfflineOperationKind::RegionsMergeDatabase,
670 maplibre_core::OfflineOperationResultKind::RegionList,
671 )
672 }
673
674 pub fn start_update_offline_region_metadata(
676 &self,
677 region_id: i64,
678 metadata: &[u8],
679 ) -> Result<OfflineOperationHandle<OfflineRegionInfo>> {
680 let runtime = self.inner.native()?;
681 let mut operation_id: sys::mln_offline_operation_id = 0;
682 maplibre_core::check(unsafe {
685 sys::mln_runtime_offline_region_update_metadata_start(
686 runtime,
687 region_id,
688 maplibre_core::runtime::metadata_ptr(metadata),
689 metadata.len(),
690 &mut operation_id,
691 )
692 })?;
693 self.start_operation(
694 operation_id,
695 maplibre_core::OfflineOperationKind::RegionUpdateMetadata,
696 maplibre_core::OfflineOperationResultKind::Region,
697 )
698 }
699
700 pub fn start_offline_region_status(
702 &self,
703 region_id: i64,
704 ) -> Result<OfflineOperationHandle<OfflineRegionStatus>> {
705 let runtime = self.inner.native()?;
706 let mut operation_id: sys::mln_offline_operation_id = 0;
707 maplibre_core::check(unsafe {
709 sys::mln_runtime_offline_region_get_status_start(runtime, region_id, &mut operation_id)
710 })?;
711 self.start_operation(
712 operation_id,
713 maplibre_core::OfflineOperationKind::RegionGetStatus,
714 maplibre_core::OfflineOperationResultKind::RegionStatus,
715 )
716 }
717
718 pub fn start_set_offline_region_observed(
720 &self,
721 region_id: i64,
722 observed: bool,
723 ) -> Result<OfflineOperationHandle<()>> {
724 let runtime = self.inner.native()?;
725 let mut operation_id: sys::mln_offline_operation_id = 0;
726 maplibre_core::check(unsafe {
727 sys::mln_runtime_offline_region_set_observed_start(
728 runtime,
729 region_id,
730 observed,
731 &mut operation_id,
732 )
733 })?;
734 self.start_operation(
735 operation_id,
736 maplibre_core::OfflineOperationKind::RegionSetObserved,
737 maplibre_core::OfflineOperationResultKind::None,
738 )
739 }
740
741 pub fn start_set_offline_region_download_state(
743 &self,
744 region_id: i64,
745 state: OfflineRegionDownloadState,
746 ) -> Result<OfflineOperationHandle<()>> {
747 let runtime = self.inner.native()?;
748 let state = state.raw_for_set()?;
749 let mut operation_id: sys::mln_offline_operation_id = 0;
750 maplibre_core::check(unsafe {
751 sys::mln_runtime_offline_region_set_download_state_start(
752 runtime,
753 region_id,
754 state,
755 &mut operation_id,
756 )
757 })?;
758 self.start_operation(
759 operation_id,
760 maplibre_core::OfflineOperationKind::RegionSetDownloadState,
761 maplibre_core::OfflineOperationResultKind::None,
762 )
763 }
764
765 pub fn start_invalidate_offline_region(
767 &self,
768 region_id: i64,
769 ) -> Result<OfflineOperationHandle<()>> {
770 let runtime = self.inner.native()?;
771 let mut operation_id: sys::mln_offline_operation_id = 0;
772 maplibre_core::check(unsafe {
773 sys::mln_runtime_offline_region_invalidate_start(runtime, region_id, &mut operation_id)
774 })?;
775 self.start_operation(
776 operation_id,
777 maplibre_core::OfflineOperationKind::RegionInvalidate,
778 maplibre_core::OfflineOperationResultKind::None,
779 )
780 }
781
782 pub fn start_delete_offline_region(
784 &self,
785 region_id: i64,
786 ) -> Result<OfflineOperationHandle<()>> {
787 let runtime = self.inner.native()?;
788 let mut operation_id: sys::mln_offline_operation_id = 0;
789 maplibre_core::check(unsafe {
790 sys::mln_runtime_offline_region_delete_start(runtime, region_id, &mut operation_id)
791 })?;
792 self.start_operation(
793 operation_id,
794 maplibre_core::OfflineOperationKind::RegionDelete,
795 maplibre_core::OfflineOperationResultKind::None,
796 )
797 }
798
799 pub fn pump(&self, timeout: Option<Duration>, budget: Option<Duration>) -> Result<()> {
828 let runtime = self.inner.native()?;
829 let timeout_ms = timeout.map_or(-1, |timeout| {
830 i64::try_from(timeout.as_millis()).unwrap_or(i64::MAX)
831 });
832 let budget_ms = budget.map_or(-1, |budget| {
833 i64::try_from(budget.as_millis()).unwrap_or(i64::MAX)
834 });
835 maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime, timeout_ms, budget_ms) })
837 }
838
839 pub fn wake_source(&self) -> Result<WakeSource> {
842 let runtime = self.inner.native()?;
843 let mut out = maplibre_core::ptr::OutHandle::<sys::mln_wake_source>::new();
844 maplibre_core::check(unsafe {
847 sys::mln_runtime_wake_source_acquire(runtime, out.as_mut_ptr())
848 })?;
849 Ok(WakeSource {
850 handle: out_handle(out, "mln_wake_source")?,
851 })
852 }
853
854 pub fn drain_events(&mut self, max_events: usize) -> Result<RuntimeEventBatch<'_>> {
877 let runtime = self.inner.native()?;
878 let mut raw = unsafe { sys::mln_runtime_event_batch_default() };
881 maplibre_core::check(unsafe {
884 sys::mln_runtime_drain_events(runtime, max_events, &mut raw)
885 })?;
886 Ok(unsafe { RuntimeEventBatch::new(raw) })
890 }
891
892 pub fn set_event_mask(&self, mask: RuntimeEventMask) -> Result<()> {
900 let runtime = self.inner.native()?;
901 maplibre_core::check(unsafe { sys::mln_runtime_set_event_mask(runtime, mask.bits()) })
903 }
904
905 pub fn event_mask(&self) -> Result<RuntimeEventMask> {
908 let runtime = self.inner.native()?;
909 let mut raw = 0;
910 maplibre_core::check(unsafe { sys::mln_runtime_get_event_mask(runtime, &mut raw) })?;
912 Ok(RuntimeEventMask::from_bits_retain(raw))
913 }
914
915 pub fn close(self) -> std::result::Result<(), HandleOperationError<Self>> {
918 if self.inner.is_closed() {
919 return Ok(());
920 }
921 if Rc::strong_count(&self.inner) > 1 {
922 return Err(HandleOperationError::new(
923 Error::new(
924 ErrorKind::InvalidState,
925 None,
926 "RuntimeHandle cannot close while child handles are live",
927 ),
928 self,
929 ));
930 }
931 self.inner
932 .close()
933 .map_err(|error| HandleOperationError::new(error, self))
934 }
935}
936
937#[derive(Debug)]
943pub struct WakeSource {
944 handle: sys::mln_wake_source,
945}
946
947impl WakeSource {
948 pub fn signal(&self) -> Result<()> {
954 maplibre_core::check(unsafe { sys::mln_wake_source_signal(self.handle) })
957 }
958}
959
960impl Drop for WakeSource {
961 fn drop(&mut self) {
962 unsafe { sys::mln_wake_source_destroy(self.handle) };
965 }
966}
967
968#[cfg(test)]
969mod tests {
970 #[cfg(not(target_os = "emscripten"))]
973 use std::io::{Read, Write};
974 #[cfg(not(target_os = "emscripten"))]
975 use std::net::TcpListener;
976 use std::sync::atomic::{AtomicUsize, Ordering};
977 use std::sync::{Arc, Mutex};
978 use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
979
980 use super::*;
981 use crate::{
982 ErrorKind, OfflineOperationCompletedEvent, ResourceErrorReason, ResourceKind,
983 ResourceProviderDecision, ResourceResponse, RuntimeEvent, RuntimeEventPayload,
984 RuntimeEventSource, RuntimeEventType,
985 };
986 use maplibre_core::{OfflineOperationKind as Op, OfflineOperationResultKind as OpResult};
987
988 const PROVIDER_STYLE_JSON: &str = r#"{"version":8,"sources":{},"layers":[]}"#;
989
990 #[cfg(not(target_os = "emscripten"))]
991 fn spawn_style_server(
992 request_count: usize,
993 ) -> (
994 String,
995 std::sync::mpsc::Receiver<String>,
996 std::thread::JoinHandle<()>,
997 ) {
998 let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
999 let base_url = format!("http://{}", listener.local_addr().unwrap());
1000 let (sender, receiver) = std::sync::mpsc::channel();
1001 let handle = std::thread::spawn(move || {
1002 for _ in 0..request_count {
1003 let (mut stream, _) = listener.accept().unwrap();
1004 stream
1005 .set_read_timeout(Some(Duration::from_secs(5)))
1006 .unwrap();
1007 let mut request = [0; 4096];
1008 let bytes = stream.read(&mut request).unwrap();
1009 let request = String::from_utf8_lossy(&request[..bytes]);
1010 let path = request
1011 .lines()
1012 .next()
1013 .and_then(|line| line.split_whitespace().nth(1))
1014 .unwrap_or("")
1015 .to_owned();
1016 sender.send(path).unwrap();
1017
1018 let body = PROVIDER_STYLE_JSON.as_bytes();
1019 write!(
1020 stream,
1021 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1022 body.len()
1023 )
1024 .unwrap();
1025 stream.write_all(body).unwrap();
1026 }
1027 });
1028 (base_url, receiver, handle)
1029 }
1030
1031 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
1032 fn spawn_recording_style_server(
1033 request_count: usize,
1034 ) -> (
1035 String,
1036 std::sync::mpsc::Receiver<String>,
1037 std::thread::JoinHandle<()>,
1038 ) {
1039 let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1040 let base_url = format!("http://{}", listener.local_addr().unwrap());
1041 let (sender, receiver) = std::sync::mpsc::channel();
1042 let handle = std::thread::spawn(move || {
1043 for _ in 0..request_count {
1044 let (mut stream, _) = listener.accept().unwrap();
1045 stream
1046 .set_read_timeout(Some(Duration::from_secs(5)))
1047 .unwrap();
1048 let mut bytes = [0; 4096];
1049 let count = stream.read(&mut bytes).unwrap();
1050 sender
1051 .send(String::from_utf8_lossy(&bytes[..count]).into_owned())
1052 .unwrap();
1053 let body = PROVIDER_STYLE_JSON.as_bytes();
1054 write!(
1055 stream,
1056 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1057 body.len()
1058 )
1059 .unwrap();
1060 stream.write_all(body).unwrap();
1061 }
1062 });
1063 (base_url, receiver, handle)
1064 }
1065
1066 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
1067 fn spawn_redirect_style_servers() -> (
1068 String,
1069 std::sync::mpsc::Receiver<(String, bool)>,
1070 Vec<std::thread::JoinHandle<()>>,
1071 ) {
1072 let origin = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1073 let destination = TcpListener::bind(("127.0.0.1", 0)).unwrap();
1074 let origin_url = format!("http://{}", origin.local_addr().unwrap());
1075 let destination_url = format!("http://{}", destination.local_addr().unwrap());
1076 let (sender, receiver) = std::sync::mpsc::channel();
1077
1078 let origin_sender = sender.clone();
1079 let origin_destination_url = destination_url.clone();
1080 let origin_server = std::thread::spawn(move || {
1081 for _ in 0..3 {
1082 let (mut stream, _) = origin.accept().unwrap();
1083 stream
1084 .set_read_timeout(Some(Duration::from_secs(5)))
1085 .unwrap();
1086 let mut bytes = [0; 4096];
1087 let count = stream.read(&mut bytes).unwrap();
1088 let request = String::from_utf8_lossy(&bytes[..count]);
1089 let path = request
1090 .lines()
1091 .next()
1092 .and_then(|line| line.split_whitespace().nth(1))
1093 .unwrap_or("")
1094 .to_owned();
1095 let has_header = request
1096 .lines()
1097 .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"));
1098 origin_sender.send((path.clone(), has_header)).unwrap();
1099
1100 if path == "/same-start.json" {
1101 write!(
1102 stream,
1103 "HTTP/1.1 302 Found\r\nLocation: /same-final.json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1104 )
1105 .unwrap();
1106 } else if path == "/cross-start.json" {
1107 write!(
1108 stream,
1109 "HTTP/1.1 302 Found\r\nLocation: {origin_destination_url}/cross-final.json\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
1110 )
1111 .unwrap();
1112 } else {
1113 let body = PROVIDER_STYLE_JSON.as_bytes();
1114 write!(
1115 stream,
1116 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1117 body.len()
1118 )
1119 .unwrap();
1120 stream.write_all(body).unwrap();
1121 }
1122 }
1123 });
1124
1125 let destination_server = std::thread::spawn(move || {
1126 let (mut stream, _) = destination.accept().unwrap();
1127 stream
1128 .set_read_timeout(Some(Duration::from_secs(5)))
1129 .unwrap();
1130 let mut bytes = [0; 4096];
1131 let count = stream.read(&mut bytes).unwrap();
1132 let request = String::from_utf8_lossy(&bytes[..count]);
1133 let path = request
1134 .lines()
1135 .next()
1136 .and_then(|line| line.split_whitespace().nth(1))
1137 .unwrap_or("")
1138 .to_owned();
1139 let has_header = request
1140 .lines()
1141 .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"));
1142 sender.send((path, has_header)).unwrap();
1143 let body = PROVIDER_STYLE_JSON.as_bytes();
1144 write!(
1145 stream,
1146 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
1147 body.len()
1148 )
1149 .unwrap();
1150 stream.write_all(body).unwrap();
1151 });
1152
1153 (
1154 origin_url,
1155 receiver,
1156 vec![origin_server, destination_server],
1157 )
1158 }
1159
1160 fn wait_for_operation<T>(
1161 runtime: &mut RuntimeHandle,
1162 operation: &OfflineOperationHandle<T>,
1163 operation_kind: Op,
1164 result_kind: OpResult,
1165 ) -> Result<OfflineOperationCompletedEvent> {
1166 let deadline = Instant::now() + Duration::from_secs(30);
1167 loop {
1168 if Instant::now() >= deadline {
1169 return Err(Error::new(
1170 ErrorKind::InvalidState,
1171 None,
1172 format!(
1173 "timed out waiting for offline operation {:?}/{:?} with id {}",
1174 operation_kind, result_kind, operation.operation_id
1175 ),
1176 ));
1177 }
1178 runtime.pump(Some(Duration::ZERO), None)?;
1179 let mut outcome = None;
1180 for event in runtime.drain_events(0)?.iter() {
1181 let RuntimeEventPayload::OfflineOperationCompleted(completed) = event.payload()
1182 else {
1183 continue;
1184 };
1185 if completed.operation_id != operation.operation_id {
1186 continue;
1187 }
1188 assert_eq!(completed.operation_kind, operation_kind);
1189 assert_eq!(completed.raw_operation_kind, operation_kind.raw_value());
1190 assert_eq!(completed.result_kind, result_kind);
1191 assert_eq!(completed.raw_result_kind, result_kind.raw_value());
1192 outcome = Some((completed, event.message()?.unwrap_or_default().to_owned()));
1193 break;
1194 }
1195 if let Some((completed, message)) = outcome {
1196 if completed.result_status != sys::MLN_STATUS_OK {
1197 return Err(Error::from_status_and_diagnostic(
1198 completed.result_status,
1199 message,
1200 ));
1201 }
1202 return Ok(completed);
1203 }
1204 std::thread::sleep(Duration::from_millis(1));
1205 }
1206 }
1207
1208 #[test]
1209 fn runtime_ambient_cache_operations_use_real_c_abi() {
1211 let base = TempDir::new("maplibre-rust-ambient-cache");
1212 let cache = base.path().join("ambient.db");
1213
1214 let mut options = RuntimeOptions::default();
1215 options.cache_path = Some(cache.to_string_lossy().into_owned());
1216 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1217
1218 for operation in [
1219 AmbientCacheOperation::PackDatabase,
1220 AmbientCacheOperation::Invalidate,
1221 AmbientCacheOperation::Clear,
1222 AmbientCacheOperation::ResetDatabase,
1223 ] {
1224 let operation = runtime.start_ambient_cache_operation(operation).unwrap();
1225 let completed =
1226 wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None)
1227 .unwrap();
1228 assert_eq!(completed.operation_id, operation.operation_id);
1229 operation.discard().unwrap();
1230 }
1231
1232 runtime.close().unwrap();
1233 }
1234
1235 #[test]
1236 fn two_runtimes_can_use_the_same_cache_database() {
1237 let base = TempDir::new("maplibre-rust-shared-cache");
1238 let mut options = RuntimeOptions::default();
1239 options.cache_path = Some(base.path().join("shared.db").to_string_lossy().into_owned());
1240 let creation = Mutex::new(());
1241 let started = std::sync::Barrier::new(2);
1242 let touch_cache = || {
1243 let result = (|| -> Result<_> {
1244 let _creation = creation.lock().unwrap();
1245 let runtime = RuntimeHandle::with_options(&options)?;
1246 let operation =
1247 runtime.start_ambient_cache_operation(AmbientCacheOperation::Invalidate)?;
1248 Ok((runtime, operation))
1249 })();
1250 started.wait();
1251
1252 let (mut runtime, operation) = result.unwrap();
1253 let completed =
1254 wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None);
1255 let discarded = operation.discard();
1256 let closed = runtime.close();
1257 completed.unwrap();
1258 discarded.unwrap();
1259 closed.unwrap();
1260 };
1261
1262 std::thread::scope(|scope| {
1263 let first = scope.spawn(touch_cache);
1264 let second = scope.spawn(touch_cache);
1265 let first_result = first.join();
1266 let second_result = second.join();
1267 first_result.unwrap();
1268 second_result.unwrap();
1269 });
1270 }
1271
1272 #[test]
1273 fn runtime_set_maximum_ambient_cache_size_reports_completion() {
1275 let base = TempDir::new("maplibre-rust-cache-size");
1276 let cache = base.path().join("ambient-size.db");
1277
1278 let mut options = RuntimeOptions::default();
1279 options.cache_path = Some(cache.to_string_lossy().into_owned());
1280 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1281
1282 for size in [8 * 1024 * 1024, 0] {
1284 let operation = runtime.start_set_maximum_ambient_cache_size(size).unwrap();
1285 let completed = wait_for_operation(
1286 &mut runtime,
1287 &operation,
1288 Op::SetMaximumAmbientCacheSize,
1289 OpResult::None,
1290 )
1291 .unwrap();
1292 assert_eq!(completed.operation_id, operation.operation_id);
1293 operation.discard().unwrap();
1294 }
1295
1296 runtime.close().unwrap();
1297 }
1298
1299 #[test]
1300 fn offline_take_result_failure_returns_live_handle() {
1302 let mut options = RuntimeOptions::default();
1303 options.cache_path = Some(":memory:".into());
1304 let runtime = RuntimeHandle::with_options(&options).unwrap();
1305 let ambient = runtime
1306 .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1307 .unwrap();
1308 let region_result = runtime
1309 .start_operation::<OfflineRegionInfo>(
1310 ambient.operation_id,
1311 maplibre_core::OfflineOperationKind::RegionCreate,
1312 maplibre_core::OfflineOperationResultKind::Region,
1313 )
1314 .unwrap();
1315
1316 let error = region_result.take().unwrap_err();
1317
1318 assert_eq!(error.kind(), ErrorKind::InvalidState);
1319 let region_result = error.into_retryable().unwrap().into_handle();
1320 region_result.discard().unwrap();
1321 drop(ambient);
1322 runtime.close().unwrap();
1323 }
1324
1325 #[test]
1326 fn offline_region_apis_use_real_c_abi() {
1328 let mut options = RuntimeOptions::default();
1329 options.cache_path = Some(":memory:".into());
1330 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1331 let definition = test_offline_region_definition("custom://offline-style.json");
1332
1333 let create = runtime
1334 .start_create_offline_region(&definition, b"abc")
1335 .unwrap();
1336 wait_for_operation(&mut runtime, &create, Op::RegionCreate, OpResult::Region).unwrap();
1337 let created = create.take().unwrap();
1338 assert_eq!(created.definition, definition);
1339 assert_eq!(created.metadata, b"abc");
1340
1341 let geometry_definition = OfflineRegionDefinition::GeometryRegion {
1342 style_url: "custom://offline-geometry-style.json".into(),
1343 geometry: br#"{"type":"Point","coordinates":[-122.5,37.5]}"#.to_vec(),
1344 min_zoom: 0.0,
1345 max_zoom: 1.0,
1346 pixel_ratio: 1.0,
1347 include_ideographs: false,
1348 };
1349 let create_geometry = runtime
1350 .start_create_offline_region(&geometry_definition, b"geo")
1351 .unwrap();
1352 wait_for_operation(
1353 &mut runtime,
1354 &create_geometry,
1355 Op::RegionCreate,
1356 OpResult::Region,
1357 )
1358 .unwrap();
1359 let geometry_region = create_geometry.take().unwrap();
1360 assert_eq!(geometry_region.definition, geometry_definition);
1361 assert_eq!(geometry_region.metadata, b"geo");
1362
1363 let get = runtime.start_offline_region(created.id).unwrap();
1364 wait_for_operation(&mut runtime, &get, Op::RegionGet, OpResult::OptionalRegion).unwrap();
1365 let fetched = get.take().unwrap().unwrap();
1366 assert_eq!(fetched, created);
1367
1368 let list = runtime.start_offline_regions().unwrap();
1369 wait_for_operation(&mut runtime, &list, Op::RegionsList, OpResult::RegionList).unwrap();
1370 let listed = list.take().unwrap();
1371 assert!(listed.iter().any(|region| region.id == created.id));
1372
1373 let update = runtime
1374 .start_update_offline_region_metadata(created.id, b"")
1375 .unwrap();
1376 wait_for_operation(
1377 &mut runtime,
1378 &update,
1379 Op::RegionUpdateMetadata,
1380 OpResult::Region,
1381 )
1382 .unwrap();
1383 let updated = update.take().unwrap();
1384 assert_eq!(updated.id, created.id);
1385 assert!(updated.metadata.is_empty());
1386
1387 let status_operation = runtime.start_offline_region_status(created.id).unwrap();
1388 wait_for_operation(
1389 &mut runtime,
1390 &status_operation,
1391 Op::RegionGetStatus,
1392 OpResult::RegionStatus,
1393 )
1394 .unwrap();
1395 let status = status_operation.take().unwrap();
1396 assert!(matches!(
1397 status.download_state,
1398 OfflineRegionDownloadState::Inactive | OfflineRegionDownloadState::Active
1399 ));
1400
1401 let set_inactive = runtime
1402 .start_set_offline_region_download_state(
1403 created.id,
1404 OfflineRegionDownloadState::Inactive,
1405 )
1406 .unwrap();
1407 wait_for_operation(
1408 &mut runtime,
1409 &set_inactive,
1410 Op::RegionSetDownloadState,
1411 OpResult::None,
1412 )
1413 .unwrap();
1414 set_inactive.discard().unwrap();
1415 let error = runtime
1416 .start_set_offline_region_download_state(
1417 created.id,
1418 OfflineRegionDownloadState::Unknown(99),
1419 )
1420 .unwrap_err();
1421 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1422
1423 let observe = runtime
1424 .start_set_offline_region_observed(created.id, true)
1425 .unwrap();
1426 wait_for_operation(
1427 &mut runtime,
1428 &observe,
1429 Op::RegionSetObserved,
1430 OpResult::None,
1431 )
1432 .unwrap();
1433 observe.discard().unwrap();
1434 let unobserve = runtime
1435 .start_set_offline_region_observed(created.id, false)
1436 .unwrap();
1437 wait_for_operation(
1438 &mut runtime,
1439 &unobserve,
1440 Op::RegionSetObserved,
1441 OpResult::None,
1442 )
1443 .unwrap();
1444 unobserve.discard().unwrap();
1445 let invalidate = runtime.start_invalidate_offline_region(created.id).unwrap();
1446 wait_for_operation(
1447 &mut runtime,
1448 &invalidate,
1449 Op::RegionInvalidate,
1450 OpResult::None,
1451 )
1452 .unwrap();
1453 invalidate.discard().unwrap();
1454 let delete = runtime.start_delete_offline_region(created.id).unwrap();
1455 wait_for_operation(&mut runtime, &delete, Op::RegionDelete, OpResult::None).unwrap();
1456 delete.discard().unwrap();
1457 let delete_geometry = runtime
1458 .start_delete_offline_region(geometry_region.id)
1459 .unwrap();
1460 wait_for_operation(
1461 &mut runtime,
1462 &delete_geometry,
1463 Op::RegionDelete,
1464 OpResult::None,
1465 )
1466 .unwrap();
1467 delete_geometry.discard().unwrap();
1468
1469 let missing_created = runtime.start_offline_region(created.id).unwrap();
1470 wait_for_operation(
1471 &mut runtime,
1472 &missing_created,
1473 Op::RegionGet,
1474 OpResult::OptionalRegion,
1475 )
1476 .unwrap();
1477 assert!(missing_created.take().unwrap().is_none());
1478 let missing_geometry = runtime.start_offline_region(geometry_region.id).unwrap();
1479 wait_for_operation(
1480 &mut runtime,
1481 &missing_geometry,
1482 Op::RegionGet,
1483 OpResult::OptionalRegion,
1484 )
1485 .unwrap();
1486 assert!(missing_geometry.take().unwrap().is_none());
1487
1488 runtime.close().unwrap();
1489 }
1490
1491 #[test]
1492 fn offline_region_merge_database_accepts_read_only_source_through_real_c_abi() {
1494 let base = TempDir::new("maplibre-rust-offline-merge");
1495 let main_cache = base.path().join("main.db");
1496 let side_cache = base.path().join("side.db");
1497
1498 let definition = test_offline_region_definition("custom://merge-style.json");
1499 {
1500 let mut side_options = RuntimeOptions::default();
1501 side_options.cache_path = Some(side_cache.to_string_lossy().into_owned());
1502 let mut side_runtime = RuntimeHandle::with_options(&side_options).unwrap();
1503 let create = side_runtime
1504 .start_create_offline_region(&definition, b"merge")
1505 .unwrap();
1506 wait_for_operation(
1507 &mut side_runtime,
1508 &create,
1509 Op::RegionCreate,
1510 OpResult::Region,
1511 )
1512 .unwrap();
1513 create.take().unwrap();
1514 side_runtime.close().unwrap();
1515 }
1516 let side_database_before = std::fs::read(&side_cache).unwrap();
1517
1518 #[cfg(unix)]
1519 {
1520 use std::os::unix::fs::PermissionsExt;
1521
1522 let mut permissions = std::fs::metadata(&side_cache).unwrap().permissions();
1523 permissions.set_mode(0o444);
1524 std::fs::set_permissions(&side_cache, permissions).unwrap();
1525 }
1526
1527 let mut main_options = RuntimeOptions::default();
1528 main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1529 let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1530 let merge = main_runtime
1531 .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1532 .unwrap();
1533 wait_for_operation(
1534 &mut main_runtime,
1535 &merge,
1536 Op::RegionsMergeDatabase,
1537 OpResult::RegionList,
1538 )
1539 .unwrap();
1540 let merged = merge.take().unwrap();
1541 assert_eq!(merged.len(), 1);
1542 assert_eq!(merged[0].definition, definition);
1543 assert_eq!(merged[0].metadata, b"merge");
1544 assert_eq!(std::fs::read(&side_cache).unwrap(), side_database_before);
1545 main_runtime.close().unwrap();
1546 }
1547
1548 fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1549 OfflineRegionDefinition::TilePyramid {
1550 style_url: style_url.into(),
1551 bounds: LatLngBounds::new(
1552 crate::LatLng::new(37.0, -123.0),
1553 crate::LatLng::new(38.0, -122.0),
1554 ),
1555 min_zoom: 0.0,
1556 max_zoom: 1.0,
1557 pixel_ratio: 1.0,
1558 include_ideographs: false,
1559 }
1560 }
1561
1562 struct TempDir {
1563 path: std::path::PathBuf,
1564 }
1565
1566 impl TempDir {
1567 fn new(prefix: &str) -> Self {
1568 let nanos = SystemTime::now()
1569 .duration_since(UNIX_EPOCH)
1570 .unwrap()
1571 .as_nanos();
1572 let path =
1573 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1574 std::fs::create_dir_all(&path).unwrap();
1575 Self { path }
1576 }
1577
1578 fn path(&self) -> &std::path::Path {
1579 &self.path
1580 }
1581 }
1582
1583 impl Drop for TempDir {
1584 fn drop(&mut self) {
1585 let _ = std::fs::remove_dir_all(&self.path);
1586 }
1587 }
1588
1589 #[test]
1590 fn runtime_create_with_explicit_options_uses_real_c_abi() {
1592 let mut options = RuntimeOptions::default();
1593 options.asset_path = Some(String::new());
1594 options.cache_path = Some(String::new());
1595 let runtime = RuntimeHandle::with_options(&options).unwrap();
1596
1597 runtime.pump(Some(Duration::ZERO), None).unwrap();
1598 runtime.close().unwrap();
1599 }
1600
1601 #[test]
1602 fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1604 let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1605 std::ptr::null(),
1606 maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1607 )
1608 .unwrap_err();
1609
1610 assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1611 assert_eq!(error.raw_status(), None);
1612 assert!(
1613 error
1614 .diagnostic()
1615 .contains("unsupported MapLibre Native C ABI version")
1616 );
1617 }
1618
1619 fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1620 runtime
1621 .drain_events(0)
1622 .unwrap()
1623 .iter()
1624 .any(|event| event.event_type() == event_type)
1625 }
1626
1627 fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1628 for _ in 0..100 {
1629 let _ = runtime.pump(Some(Duration::ZERO), None);
1630 if drain_holds_event_type(runtime, event_type) {
1631 return true;
1632 }
1633 std::thread::sleep(Duration::from_millis(10));
1634 }
1635 false
1636 }
1637
1638 fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent {
1639 for _ in 0..100 {
1640 runtime.pump(Some(Duration::ZERO), None).unwrap();
1641 let mut failure = None;
1642 for event in runtime.drain_events(0).unwrap().iter() {
1643 if event.event_type() == RuntimeEventType::MapLoadingFailed {
1644 failure = Some(event.to_owned().unwrap());
1645 break;
1646 }
1647 }
1648 if let Some(failure) = failure {
1649 return failure;
1650 }
1651 std::thread::sleep(Duration::from_millis(10));
1652 }
1653 panic!("expected a map loading-failure event");
1654 }
1655
1656 #[test]
1657 fn runtime_create_run_drain_and_close() {
1659 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1660
1661 runtime.pump(Some(Duration::ZERO), None).unwrap();
1662 let batch = runtime.drain_events(0).unwrap();
1664 assert!(batch.is_empty());
1665 assert_eq!(batch.len(), 0);
1666 assert_eq!(batch.remaining(), 0);
1667 assert_eq!(batch.iter().count(), 0);
1668 assert!(runtime.drain_events(0).unwrap().is_empty());
1670 runtime.close().unwrap();
1671 }
1672
1673 fn quiesce(runtime: &mut RuntimeHandle) {
1676 for _ in 0..100 {
1677 runtime.pump(Some(Duration::ZERO), None).unwrap();
1678 if runtime.drain_events(0).unwrap().is_empty() {
1679 return;
1680 }
1681 }
1682 panic!("the runtime kept producing events while idle");
1683 }
1684
1685 fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1688 let started = Instant::now();
1689 for _ in 0..20 {
1690 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1691 assert!(
1692 started.elapsed() < Duration::from_secs(5),
1693 "parks sat out their timeouts instead of taking wakes"
1694 );
1695 if drain_holds_event_type(runtime, event_type) {
1696 return true;
1697 }
1698 }
1699 false
1700 }
1701
1702 #[test]
1703 fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() {
1705 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1706 runtime
1707 .set_resource_provider(move |request, handle| {
1708 if request.requested_url != "custom://style.json" {
1709 return ResourceProviderDecision::PassThrough;
1710 }
1711 handle
1712 .complete(ResourceResponse::ok(
1713 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1714 ))
1715 .unwrap();
1716 ResourceProviderDecision::PassThrough
1717 })
1718 .unwrap();
1719
1720 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1721 map.set_style_url("custom://style.json").unwrap();
1722 assert!(park_for_runtime_event(
1723 &mut runtime,
1724 RuntimeEventType::MapStyleLoaded
1725 ));
1726
1727 let source = runtime.wake_source().unwrap();
1730 quiesce(&mut runtime);
1731 let signaller = std::thread::spawn(move || {
1732 std::thread::sleep(Duration::from_millis(20));
1733 source.signal().unwrap();
1734 source
1735 });
1736 let started = Instant::now();
1737 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1738 assert!(
1739 started.elapsed() < Duration::from_secs(5),
1740 "the parked owner thread timed out instead of taking the signal"
1741 );
1742 let source = signaller.join().unwrap();
1743
1744 map.close().unwrap();
1747 runtime.close().unwrap();
1748 source.signal().unwrap();
1749 }
1750
1751 #[test]
1752 fn a_pump_clears_the_wake_flag_it_returns_on() {
1754 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1755 let source = runtime.wake_source().unwrap();
1756 quiesce(&mut runtime);
1757
1758 source.signal().unwrap();
1759 let started = Instant::now();
1760 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1761 assert!(
1762 started.elapsed() < Duration::from_secs(5),
1763 "a pump waited even though the wake flag was set"
1764 );
1765
1766 let started = Instant::now();
1768 runtime
1769 .pump(Some(Duration::from_millis(200)), None)
1770 .unwrap();
1771 assert!(
1772 started.elapsed() >= Duration::from_millis(100),
1773 "the first pump left the wake flag set"
1774 );
1775
1776 runtime.close().unwrap();
1777 }
1778
1779 #[test]
1780 fn a_bounded_drain_reports_the_events_it_left_queued() {
1782 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1783 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1784 map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap();
1785 runtime.pump(Some(Duration::ZERO), None).unwrap();
1786
1787 let bounded = runtime.drain_events(1).unwrap();
1788 assert_eq!(bounded.len(), 1);
1789 assert!(
1790 bounded.remaining() > 0,
1791 "a style load should queue more than one event"
1792 );
1793 let rest = runtime.drain_events(0).unwrap();
1794 assert!(rest.len() > 1, "one drain should report the whole queue");
1795 assert_eq!(rest.remaining(), 0);
1796 let first = rest.iter().next().unwrap();
1797 assert_eq!(first.source(), RuntimeEventSource::Map(map.id()));
1798
1799 map.close().unwrap();
1800 runtime.close().unwrap();
1801 }
1802
1803 #[test]
1804 fn a_creation_mask_narrows_a_runtime_before_its_first_operation() {
1806 let mut options = crate::RuntimeOptions::default();
1807 options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED;
1808 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1809
1810 assert_eq!(
1811 runtime.event_mask().unwrap(),
1812 RuntimeEventMask::OFFLINE_OPERATION_COMPLETED
1813 );
1814
1815 let operation = runtime
1818 .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1819 .unwrap();
1820 let completed =
1821 wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
1822 assert_eq!(completed.operation_id, operation.operation_id);
1823 operation.discard().unwrap();
1824
1825 runtime.close().unwrap();
1826
1827 options.event_mask = RuntimeEventMask::from_bits_retain(1 << 63);
1830 let error = RuntimeHandle::with_options(&options).unwrap_err();
1831 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1832 }
1833
1834 #[test]
1835 fn a_runtime_mask_round_trips_and_rejects_undefined_bits() {
1837 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1838
1839 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1841
1842 runtime.set_event_mask(RuntimeEventMask::ALL).unwrap();
1843 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1844
1845 let mut mask = runtime.event_mask().unwrap();
1847 mask.remove(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED);
1848 runtime.set_event_mask(mask).unwrap();
1849 let read_back = runtime.event_mask().unwrap();
1850 assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED));
1851 assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED));
1852 assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED));
1853
1854 let undefined = RuntimeEventMask::from_bits_retain(1 << 63);
1855 let error = runtime.set_event_mask(undefined).unwrap_err();
1856 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1857 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1858 assert_eq!(runtime.event_mask().unwrap(), read_back);
1859
1860 runtime.close().unwrap();
1861 }
1862
1863 #[test]
1864 fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1866 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1867 let runtime_handle = runtime.inner.native().unwrap();
1868
1869 let error = std::thread::spawn(move || {
1870 maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) })
1873 .unwrap_err()
1874 })
1875 .join()
1876 .unwrap();
1877
1878 assert_eq!(error.kind(), ErrorKind::WrongThread);
1879 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1880 assert!(!error.diagnostic().is_empty());
1881 runtime.close().unwrap();
1882 }
1883
1884 #[test]
1885 fn resource_provider_installs_replaces_clears_and_releases_state() {
1887 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1888 let first = Arc::new(());
1889 let first_callback = Arc::clone(&first);
1890
1891 runtime
1892 .set_resource_provider(move |_, _| {
1893 let _ = &first_callback;
1894 crate::ResourceProviderDecision::PassThrough
1895 })
1896 .unwrap();
1897 assert_eq!(Arc::strong_count(&first), 2);
1898
1899 let second = Arc::new(());
1900 let second_callback = Arc::clone(&second);
1901 runtime
1902 .set_resource_provider(move |_, _| {
1903 let _ = &second_callback;
1904 crate::ResourceProviderDecision::PassThrough
1905 })
1906 .unwrap();
1907 assert_eq!(Arc::strong_count(&first), 1);
1908 assert_eq!(Arc::strong_count(&second), 2);
1909
1910 runtime.clear_resource_provider().unwrap();
1911 assert_eq!(Arc::strong_count(&second), 1);
1912
1913 let third = Arc::new(());
1914 let third_callback = Arc::clone(&third);
1915 runtime
1916 .set_resource_provider(move |_, _| {
1917 let _ = &third_callback;
1918 crate::ResourceProviderDecision::PassThrough
1919 })
1920 .unwrap();
1921 assert_eq!(Arc::strong_count(&third), 2);
1922
1923 runtime.close().unwrap();
1924 assert_eq!(Arc::strong_count(&third), 1);
1925 }
1926
1927 #[test]
1928 fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1930 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1931 let first = Arc::new(());
1932 let first_callback = Arc::clone(&first);
1933 runtime
1934 .set_resource_provider(move |_, _| {
1935 let _ = &first_callback;
1936 crate::ResourceProviderDecision::PassThrough
1937 })
1938 .unwrap();
1939
1940 let second = Arc::new(());
1941 let second_callback = Arc::clone(&second);
1942 let error = runtime
1943 .inner
1944 .set_resource_provider_with_rejected_descriptor_for_testing(move |_, _| {
1945 let _ = &second_callback;
1946 crate::ResourceProviderDecision::PassThrough
1947 })
1948 .unwrap_err();
1949
1950 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1951 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1952 assert_eq!(Arc::strong_count(&first), 2);
1953 assert_eq!(Arc::strong_count(&second), 1);
1954
1955 runtime.close().unwrap();
1956 assert_eq!(Arc::strong_count(&first), 1);
1957 }
1958
1959 fn load_probe_style(runtime: &mut RuntimeHandle, map: &MapHandle, style_url: &str) {
1963 map.set_style_url(style_url).unwrap();
1964 let event = wait_for_map_loading_failure(runtime);
1965 assert!(
1966 event
1967 .message
1968 .as_deref()
1969 .is_some_and(|message| message.contains("\"jar\""))
1970 );
1971 }
1972
1973 #[test]
1974 fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() {
1976 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1977 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1978
1979 let first_calls = Arc::new(AtomicUsize::new(0));
1980 let first_callback_calls = Arc::clone(&first_calls);
1981 runtime
1982 .set_resource_provider(move |_, _| {
1983 first_callback_calls.fetch_add(1, Ordering::SeqCst);
1984 ResourceProviderDecision::PassThrough
1985 })
1986 .unwrap();
1987 load_probe_style(&mut runtime, &map, "jar:file:/packaged/first.json");
1988 assert!(first_calls.load(Ordering::SeqCst) > 0);
1989
1990 let second_calls = Arc::new(AtomicUsize::new(0));
1991 let second_callback_calls = Arc::clone(&second_calls);
1992 runtime
1993 .set_resource_provider(move |_, _| {
1994 second_callback_calls.fetch_add(1, Ordering::SeqCst);
1995 ResourceProviderDecision::PassThrough
1996 })
1997 .unwrap();
1998 let first_calls_after_replace = first_calls.load(Ordering::SeqCst);
1999 load_probe_style(&mut runtime, &map, "jar:file:/packaged/second.json");
2000 assert!(second_calls.load(Ordering::SeqCst) > 0);
2001 assert_eq!(
2002 first_calls.load(Ordering::SeqCst),
2003 first_calls_after_replace
2004 );
2005
2006 runtime.clear_resource_provider().unwrap();
2007 let second_calls_after_clear = second_calls.load(Ordering::SeqCst);
2008 load_probe_style(&mut runtime, &map, "jar:file:/packaged/third.json");
2009 assert_eq!(
2010 first_calls.load(Ordering::SeqCst),
2011 first_calls_after_replace
2012 );
2013 assert_eq!(
2014 second_calls.load(Ordering::SeqCst),
2015 second_calls_after_clear
2016 );
2017
2018 runtime.clear_resource_provider().unwrap();
2020
2021 map.close().unwrap();
2022 runtime.close().unwrap();
2023 }
2024
2025 #[test]
2026 fn resource_provider_completes_style_request_inline_through_c_abi() {
2028 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2029 let calls = Arc::new(AtomicUsize::new(0));
2030 let callback_calls = Arc::clone(&calls);
2031 runtime
2032 .set_resource_provider(move |request, handle| {
2033 if request.requested_url != "custom://style.json" {
2034 return ResourceProviderDecision::PassThrough;
2035 }
2036 callback_calls.fetch_add(1, Ordering::SeqCst);
2037 assert_eq!(request.kind, ResourceKind::Style);
2038 handle
2039 .complete(ResourceResponse::ok(
2040 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2041 ))
2042 .unwrap();
2043 ResourceProviderDecision::PassThrough
2044 })
2045 .unwrap();
2046
2047 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2048 map.set_style_url("custom://style.json").unwrap();
2049
2050 assert!(wait_for_runtime_event(
2051 &mut runtime,
2052 RuntimeEventType::MapStyleLoaded
2053 ));
2054 assert_eq!(calls.load(Ordering::SeqCst), 1);
2055 map.close().unwrap();
2056 runtime.close().unwrap();
2057 }
2058
2059 #[test]
2060 fn resource_provider_sees_scheme_alias_and_its_resolved_url() {
2062 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2063 let resolved = Arc::new(Mutex::new(None));
2064 let callback_resolved = Arc::clone(&resolved);
2065 runtime
2066 .set_resource_provider(move |request, handle| {
2067 if request.requested_url != "maplibre://maps/style" {
2068 return ResourceProviderDecision::PassThrough;
2069 }
2070 *callback_resolved.lock().unwrap() = Some(request.resolved_url.clone());
2071 handle
2072 .complete(ResourceResponse::ok(
2073 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2074 ))
2075 .unwrap();
2076 ResourceProviderDecision::Handle
2077 })
2078 .unwrap();
2079
2080 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2081 map.set_style_url("maplibre://maps/style").unwrap();
2082
2083 assert!(wait_for_runtime_event(
2084 &mut runtime,
2085 RuntimeEventType::MapStyleLoaded
2086 ));
2087 assert_eq!(
2088 resolved.lock().unwrap().as_deref(),
2089 Some("https://demotiles.maplibre.org/style.json")
2090 );
2091 map.close().unwrap();
2092 runtime.close().unwrap();
2093 }
2094
2095 #[test]
2096 fn resource_provider_completes_style_request_from_another_thread() {
2098 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2099 let (sender, receiver) = std::sync::mpsc::channel();
2100 runtime
2101 .set_resource_provider(move |request, handle| {
2102 if request.requested_url == "custom://async-style.json" {
2103 sender.send(handle).unwrap();
2104 ResourceProviderDecision::Handle
2105 } else {
2106 ResourceProviderDecision::PassThrough
2107 }
2108 })
2109 .unwrap();
2110
2111 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2112 map.set_style_url("custom://async-style.json").unwrap();
2113 let handle = receiver
2114 .recv_timeout(Duration::from_secs(5))
2115 .expect("provider should send handled request");
2116 assert!(!handle.is_cancelled().unwrap());
2117 std::thread::spawn(move || {
2118 handle
2119 .complete(ResourceResponse::ok(
2120 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2121 ))
2122 .unwrap();
2123 })
2124 .join()
2125 .unwrap();
2126
2127 assert!(wait_for_runtime_event(
2128 &mut runtime,
2129 RuntimeEventType::MapStyleLoaded
2130 ));
2131 map.close().unwrap();
2132 runtime.close().unwrap();
2133 }
2134
2135 fn pump_until(runtime: &mut RuntimeHandle, condition: impl Fn() -> bool) -> bool {
2137 for _ in 0..100 {
2138 if condition() {
2139 return true;
2140 }
2141 runtime.pump(Some(Duration::ZERO), None).unwrap();
2142 std::thread::sleep(Duration::from_millis(10));
2143 }
2144 condition()
2145 }
2146
2147 #[test]
2148 fn cancel_callback_runs_once_when_the_map_discards_the_request() {
2150 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2151 let cancels = Arc::new(AtomicUsize::new(0));
2152 let callback_cancels = Arc::clone(&cancels);
2153 let (sender, receiver) = std::sync::mpsc::channel();
2154 runtime
2155 .set_resource_provider(move |request, handle| {
2156 if request.requested_url != "custom://cancel-style.json" {
2157 return ResourceProviderDecision::PassThrough;
2158 }
2159 let cancels = Arc::clone(&callback_cancels);
2160 handle
2161 .set_cancel_callback(move || {
2162 cancels.fetch_add(1, Ordering::SeqCst);
2163 })
2164 .unwrap();
2165 sender.send(handle).unwrap();
2166 ResourceProviderDecision::Handle
2167 })
2168 .unwrap();
2169
2170 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2171 map.set_style_url("custom://cancel-style.json").unwrap();
2172 let handle = receiver
2173 .recv_timeout(Duration::from_secs(5))
2174 .expect("provider should send handled request");
2175 assert_eq!(cancels.load(Ordering::SeqCst), 0);
2176
2177 map.close().unwrap();
2178
2179 assert!(pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) == 1));
2180 assert!(handle.is_cancelled().unwrap());
2181 assert_eq!(
2183 handle
2184 .complete(ResourceResponse::no_content())
2185 .unwrap_err()
2186 .kind(),
2187 ErrorKind::InvalidState
2188 );
2189 assert_eq!(cancels.load(Ordering::SeqCst), 1);
2190 runtime.close().unwrap();
2191 }
2192
2193 #[test]
2194 fn cancel_callback_may_close_its_own_request() {
2196 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2197 let cancelled_request: Arc<Mutex<Option<crate::ResourceRequestHandle>>> =
2198 Arc::new(Mutex::new(None));
2199 let provider_request = Arc::clone(&cancelled_request);
2200 let closed = Arc::new(AtomicUsize::new(0));
2201 let callback_closed = Arc::clone(&closed);
2202 runtime
2203 .set_resource_provider(move |request, handle| {
2204 if request.requested_url != "custom://cancel-style.json" {
2205 return ResourceProviderDecision::PassThrough;
2206 }
2207 let callback_request = Arc::clone(&provider_request);
2208 let closed = Arc::clone(&callback_closed);
2209 handle
2210 .set_cancel_callback(move || {
2211 let request = callback_request.lock().unwrap().take();
2212 request.expect("the cancelled request").close();
2213 closed.fetch_add(1, Ordering::SeqCst);
2214 })
2215 .unwrap();
2216 *provider_request.lock().unwrap() = Some(handle);
2217 ResourceProviderDecision::Handle
2218 })
2219 .unwrap();
2220
2221 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2222 map.set_style_url("custom://cancel-style.json").unwrap();
2223 assert!(pump_until(&mut runtime, || cancelled_request
2224 .lock()
2225 .unwrap()
2226 .is_some()));
2227
2228 map.close().unwrap();
2229
2230 assert!(pump_until(&mut runtime, || closed.load(Ordering::SeqCst) == 1));
2231 assert!(cancelled_request.lock().unwrap().is_none());
2232 runtime.close().unwrap();
2233 }
2234
2235 #[test]
2236 fn cancel_callback_skips_a_completed_request() {
2238 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2239 let cancels = Arc::new(AtomicUsize::new(0));
2240 let callback_cancels = Arc::clone(&cancels);
2241 runtime
2242 .set_resource_provider(move |request, handle| {
2243 if request.requested_url != "custom://cancel-style.json" {
2244 return ResourceProviderDecision::PassThrough;
2245 }
2246 let cancels = Arc::clone(&callback_cancels);
2247 handle
2248 .set_cancel_callback(move || {
2249 cancels.fetch_add(1, Ordering::SeqCst);
2250 })
2251 .unwrap();
2252 handle
2253 .complete(ResourceResponse::ok(
2254 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2255 ))
2256 .unwrap();
2257 ResourceProviderDecision::Handle
2258 })
2259 .unwrap();
2260
2261 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2262 map.set_style_url("custom://cancel-style.json").unwrap();
2263 assert!(wait_for_runtime_event(
2264 &mut runtime,
2265 RuntimeEventType::MapStyleLoaded
2266 ));
2267
2268 map.close().unwrap();
2269
2270 assert!(!pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) > 0));
2273 runtime.close().unwrap();
2274 }
2275
2276 #[test]
2277 fn cancel_registration_on_a_cancelled_request_runs_before_returning() {
2279 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2280 let (sender, receiver) = std::sync::mpsc::channel();
2281 runtime
2282 .set_resource_provider(move |request, handle| {
2283 if request.requested_url != "custom://cancel-style.json" {
2284 return ResourceProviderDecision::PassThrough;
2285 }
2286 sender.send(handle).unwrap();
2287 ResourceProviderDecision::Handle
2288 })
2289 .unwrap();
2290
2291 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2292 map.set_style_url("custom://cancel-style.json").unwrap();
2293 let handle = receiver
2294 .recv_timeout(Duration::from_secs(5))
2295 .expect("provider should send handled request");
2296 map.close().unwrap();
2297 assert!(pump_until(&mut runtime, || handle.is_cancelled().unwrap()));
2298
2299 let cancels = Arc::new(AtomicUsize::new(0));
2300 let callback_cancels = Arc::clone(&cancels);
2301 handle
2302 .set_cancel_callback(move || {
2303 callback_cancels.fetch_add(1, Ordering::SeqCst);
2304 })
2305 .unwrap();
2306 assert_eq!(cancels.load(Ordering::SeqCst), 1);
2307
2308 assert_eq!(
2310 handle.set_cancel_callback(|| {}).unwrap_err().kind(),
2311 ErrorKind::InvalidState
2312 );
2313 runtime.close().unwrap();
2314 }
2315
2316 #[test]
2317 fn resource_provider_error_response_becomes_copied_loading_failure_event() {
2319 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2320 runtime
2321 .set_resource_provider(move |request, handle| {
2322 if request.requested_url == "custom://broken-style.json" {
2323 handle
2324 .complete(ResourceResponse::error(
2325 ResourceErrorReason::Other,
2326 "provider failed",
2327 ))
2328 .unwrap();
2329 ResourceProviderDecision::Handle
2330 } else {
2331 ResourceProviderDecision::PassThrough
2332 }
2333 })
2334 .unwrap();
2335
2336 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2337 let map_id = map.id();
2338 map.set_style_url("custom://broken-style.json").unwrap();
2339
2340 let event = wait_for_map_loading_failure(&mut runtime);
2341 let copied_message = event.message.clone();
2342 let _ = runtime.drain_events(0).unwrap();
2344
2345 assert_eq!(event.source, RuntimeEventSource::Map(map_id));
2346 assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
2347 assert_eq!(event.message, copied_message);
2348 assert!(
2349 event
2350 .message
2351 .as_deref()
2352 .is_some_and(|message| message.contains("provider failed"))
2353 );
2354
2355 map.close().unwrap();
2356 runtime.close().unwrap();
2357 }
2358
2359 #[test]
2360 fn resource_transform_installs_replaces_clears_and_releases_state() {
2362 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2363 let first = Arc::new(());
2364 let first_callback = Arc::clone(&first);
2365
2366 runtime
2367 .set_resource_transform(move |request| {
2368 let _ = &first_callback;
2369 assert!(matches!(
2370 request.kind,
2371 ResourceKind::Style | ResourceKind::UnknownRaw(_)
2372 ));
2373 None
2374 })
2375 .unwrap();
2376 assert_eq!(Arc::strong_count(&first), 2);
2377
2378 let second = Arc::new(());
2379 let second_callback = Arc::clone(&second);
2380 runtime
2381 .set_resource_transform(move |_| {
2382 let _ = &second_callback;
2383 Some("https://example.test/replacement".to_owned())
2384 })
2385 .unwrap();
2386 assert_eq!(Arc::strong_count(&first), 1);
2387 assert_eq!(Arc::strong_count(&second), 2);
2388
2389 runtime.clear_resource_transform().unwrap();
2390 assert_eq!(Arc::strong_count(&second), 1);
2391 runtime.close().unwrap();
2392 }
2393
2394 #[cfg(target_os = "emscripten")]
2400 #[test]
2401 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2403 let origin = std::env::var("MLN_FFI_TEST_FIXTURE_ORIGIN").expect(
2404 "MLN_FFI_TEST_FIXTURE_ORIGIN is unset; run the suite through \
2405 `mise run //bindings/rust:test emscripten-wasm32-webgl`",
2406 );
2407 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2408 let transform_url = format!("{origin}/__fixture/rewritten-style.json");
2409 runtime
2412 .set_resource_transform(move |request| {
2413 (request.url.ends_with("/original-style.json")
2414 || request.url.ends_with("/original-after-clear.json"))
2415 .then(|| transform_url.clone())
2416 })
2417 .unwrap();
2418
2419 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2420 map.set_style_url(&format!("{origin}/__fixture/original-style.json"))
2421 .unwrap();
2422 assert!(wait_for_runtime_event(
2423 &mut runtime,
2424 RuntimeEventType::MapStyleLoaded
2425 ));
2426 assert_eq!(map.style_layer_ids().unwrap(), ["rewritten"]);
2427
2428 runtime.clear_resource_transform().unwrap();
2429 map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json"))
2430 .unwrap();
2431 assert!(wait_for_runtime_event(
2432 &mut runtime,
2433 RuntimeEventType::MapStyleLoaded
2434 ));
2435 assert_eq!(map.style_layer_ids().unwrap(), ["original-after-clear"]);
2436
2437 map.close().unwrap();
2438 runtime.close().unwrap();
2439 }
2440
2441 #[cfg(not(target_os = "emscripten"))]
2442 #[test]
2443 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2445 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2446 let (base_url, requests, server) = spawn_style_server(2);
2447 let transform_base_url = base_url.clone();
2448
2449 runtime
2453 .set_resource_transform(move |request| {
2454 if request.url.ends_with("/original-style.json")
2455 || request.url.ends_with("/original-after-clear.json")
2456 {
2457 Some(format!("{transform_base_url}/rewritten-style.json"))
2458 } else {
2459 None
2460 }
2461 })
2462 .unwrap();
2463
2464 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2465 map.set_style_url(&format!("{base_url}/original-style.json"))
2466 .unwrap();
2467 assert!(wait_for_runtime_event(
2468 &mut runtime,
2469 RuntimeEventType::MapStyleLoaded
2470 ));
2471 assert_eq!(
2472 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2473 "/rewritten-style.json"
2474 );
2475
2476 runtime.clear_resource_transform().unwrap();
2477 map.set_style_url(&format!("{base_url}/original-after-clear.json"))
2478 .unwrap();
2479 assert!(wait_for_runtime_event(
2480 &mut runtime,
2481 RuntimeEventType::MapStyleLoaded
2482 ));
2483 assert_eq!(
2484 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2485 "/original-after-clear.json"
2486 );
2487
2488 map.close().unwrap();
2489 runtime.close().unwrap();
2490 server.join().unwrap();
2491 }
2492
2493 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2494 #[test]
2495 fn http_header_transform_reaches_requests_and_clear_stops_it() {
2497 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2498 let (base_url, requests, server) = spawn_recording_style_server(2);
2499 runtime
2500 .set_http_header_transform(|request| {
2501 assert_eq!(request.kind, ResourceKind::Style);
2502 vec![crate::HttpHeader::new("X-Map-Token", "secret")]
2503 })
2504 .unwrap();
2505
2506 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2507 map.set_style_url(&format!("{base_url}/with-header.json"))
2508 .unwrap();
2509 assert!(wait_for_runtime_event(
2510 &mut runtime,
2511 RuntimeEventType::MapStyleLoaded
2512 ));
2513 let first = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2514 assert!(
2515 first
2516 .lines()
2517 .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"))
2518 );
2519
2520 runtime.clear_http_header_transform().unwrap();
2521 map.set_style_url(&format!("{base_url}/after-clear.json"))
2522 .unwrap();
2523 assert!(wait_for_runtime_event(
2524 &mut runtime,
2525 RuntimeEventType::MapStyleLoaded
2526 ));
2527 let second = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2528 assert!(
2529 !second
2530 .lines()
2531 .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:"))
2532 );
2533
2534 map.close().unwrap();
2535 runtime.close().unwrap();
2536 server.join().unwrap();
2537 }
2538
2539 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2540 #[test]
2541 fn http_header_transform_skips_non_http_urls() {
2543 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2544 runtime.set_resource_transform(|_| None).unwrap();
2545 let calls = Arc::new(AtomicUsize::new(0));
2546 let callback_calls = Arc::clone(&calls);
2547 runtime
2548 .set_http_header_transform(move |_| {
2549 callback_calls.fetch_add(1, Ordering::SeqCst);
2550 Vec::new()
2551 })
2552 .unwrap();
2553 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2554
2555 map.set_style_url("jar:file:/packaged/style.json").unwrap();
2556 let _ = wait_for_map_loading_failure(&mut runtime);
2557 assert_eq!(calls.load(Ordering::SeqCst), 0);
2558
2559 map.close().unwrap();
2560 runtime.close().unwrap();
2561 }
2562
2563 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2564 #[test]
2565 fn http_header_transform_preserves_same_origin_and_strips_cross_origin_redirects() {
2567 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2568 let (origin_url, requests, servers) = spawn_redirect_style_servers();
2569 runtime
2570 .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")])
2571 .unwrap();
2572 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2573
2574 map.set_style_url(&format!("{origin_url}/same-start.json"))
2575 .unwrap();
2576 assert!(wait_for_runtime_event(
2577 &mut runtime,
2578 RuntimeEventType::MapStyleLoaded
2579 ));
2580 assert_eq!(
2581 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2582 ("/same-start.json".to_owned(), true)
2583 );
2584 assert_eq!(
2585 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2586 ("/same-final.json".to_owned(), true)
2587 );
2588
2589 map.set_style_url(&format!("{origin_url}/cross-start.json"))
2590 .unwrap();
2591 assert!(wait_for_runtime_event(
2592 &mut runtime,
2593 RuntimeEventType::MapStyleLoaded
2594 ));
2595 assert_eq!(
2596 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2597 ("/cross-start.json".to_owned(), true)
2598 );
2599 assert_eq!(
2600 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2601 ("/cross-final.json".to_owned(), false)
2602 );
2603
2604 map.close().unwrap();
2605 runtime.close().unwrap();
2606 for server in servers {
2607 server.join().unwrap();
2608 }
2609 }
2610
2611 #[cfg(any(target_env = "ohos", target_os = "emscripten"))]
2616 #[test]
2617 fn http_header_transform_reports_unsupported() {
2618 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2619 let error = runtime
2620 .set_http_header_transform(|_| Vec::new())
2621 .unwrap_err();
2622 assert_eq!(error.kind(), ErrorKind::Unsupported);
2623 runtime.close().unwrap();
2624 }
2625
2626 #[test]
2627 fn resource_transform_replacement_after_map_creation_releases_previous_state() {
2629 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2630 let first = Arc::new(());
2631 let first_callback = Arc::clone(&first);
2632 runtime
2633 .set_resource_transform(move |_| {
2634 let _ = &first_callback;
2635 None
2636 })
2637 .unwrap();
2638 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2639
2640 let second = Arc::new(());
2641 let second_callback = Arc::clone(&second);
2642 runtime
2643 .set_resource_transform(move |_| {
2644 let _ = &second_callback;
2645 None
2646 })
2647 .unwrap();
2648
2649 assert_eq!(Arc::strong_count(&first), 1);
2650 assert_eq!(Arc::strong_count(&second), 2);
2651
2652 map.close().unwrap();
2653 runtime.close().unwrap();
2654 assert_eq!(Arc::strong_count(&second), 1);
2655 }
2656
2657 #[test]
2658 fn runtime_teardown_releases_resource_transform_state() {
2660 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2661 let token = Arc::new(());
2662 let callback_token = Arc::clone(&token);
2663 runtime
2664 .set_resource_transform(move |_| {
2665 let _ = &callback_token;
2666 None
2667 })
2668 .unwrap();
2669 assert_eq!(Arc::strong_count(&token), 2);
2670
2671 runtime.close().unwrap();
2672
2673 assert_eq!(Arc::strong_count(&token), 1);
2674 }
2675
2676 #[test]
2677 fn resource_transform_installs_after_map_creation() {
2680 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2681 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2682
2683 runtime.set_resource_transform(|_| None).unwrap();
2684
2685 map.close().unwrap();
2686 runtime.close().unwrap();
2687 }
2688
2689 #[test]
2690 fn resource_transform_clears_after_map_was_closed_and_releases_state() {
2692 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2693 let token = Arc::new(());
2694 let callback_token = Arc::clone(&token);
2695 runtime
2696 .set_resource_transform(move |_| {
2697 let _ = &callback_token;
2698 None
2699 })
2700 .unwrap();
2701 assert_eq!(Arc::strong_count(&token), 2);
2702
2703 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2704 map.close().unwrap();
2705
2706 runtime.clear_resource_transform().unwrap();
2707
2708 assert_eq!(Arc::strong_count(&token), 1);
2709
2710 runtime.close().unwrap();
2711 }
2712
2713 #[test]
2714 fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() {
2716 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2717 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2718 let map_id = map.id();
2719
2720 let error = map.set_style_json(b"{").unwrap_err();
2721 assert_eq!(error.kind(), ErrorKind::NativeError);
2722 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
2723
2724 let batch = runtime.drain_events(0).unwrap();
2725 let types = batch
2726 .iter()
2727 .map(|event| event.event_type())
2728 .collect::<Vec<_>>();
2729 assert!(
2730 types.len() > 1,
2731 "a failed style load should queue more than one event, got {types:?}"
2732 );
2733 let loading_failed = batch
2734 .iter()
2735 .find(|event| event.event_type() == RuntimeEventType::MapLoadingFailed)
2736 .expect("a malformed style should queue a loading-failed event");
2737 assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id));
2738 assert!(
2739 loading_failed
2740 .message()
2741 .unwrap()
2742 .is_some_and(|message| !message.is_empty())
2743 );
2744 let owned = loading_failed.to_owned().unwrap();
2745
2746 assert!(runtime.drain_events(0).unwrap().is_empty());
2749 assert_eq!(owned.source, RuntimeEventSource::Map(map_id));
2750 assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed);
2751 assert!(
2752 owned
2753 .message
2754 .as_deref()
2755 .is_some_and(|message| !message.is_empty())
2756 );
2757
2758 map.close().unwrap();
2759 runtime.close().unwrap();
2760 }
2761
2762 #[test]
2763 fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
2765 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2766 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2767
2768 let error = runtime.close().unwrap_err();
2769 assert_eq!(error.kind(), ErrorKind::InvalidState);
2770 assert_eq!(error.raw_status(), None);
2771 let runtime = error.into_handle();
2772
2773 runtime.pump(Some(Duration::ZERO), None).unwrap();
2774 map.close().unwrap();
2775 runtime.close().unwrap();
2776 }
2777}