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 runtime_set_maximum_ambient_cache_size_reports_completion() {
1238 let base = TempDir::new("maplibre-rust-cache-size");
1239 let cache = base.path().join("ambient-size.db");
1240
1241 let mut options = RuntimeOptions::default();
1242 options.cache_path = Some(cache.to_string_lossy().into_owned());
1243 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1244
1245 for size in [8 * 1024 * 1024, 0] {
1247 let operation = runtime.start_set_maximum_ambient_cache_size(size).unwrap();
1248 let completed = wait_for_operation(
1249 &mut runtime,
1250 &operation,
1251 Op::SetMaximumAmbientCacheSize,
1252 OpResult::None,
1253 )
1254 .unwrap();
1255 assert_eq!(completed.operation_id, operation.operation_id);
1256 operation.discard().unwrap();
1257 }
1258
1259 runtime.close().unwrap();
1260 }
1261
1262 #[test]
1263 fn offline_take_result_failure_returns_live_handle() {
1265 let mut options = RuntimeOptions::default();
1266 options.cache_path = Some(":memory:".into());
1267 let runtime = RuntimeHandle::with_options(&options).unwrap();
1268 let ambient = runtime
1269 .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1270 .unwrap();
1271 let region_result = runtime
1272 .start_operation::<OfflineRegionInfo>(
1273 ambient.operation_id,
1274 maplibre_core::OfflineOperationKind::RegionCreate,
1275 maplibre_core::OfflineOperationResultKind::Region,
1276 )
1277 .unwrap();
1278
1279 let error = region_result.take().unwrap_err();
1280
1281 assert_eq!(error.kind(), ErrorKind::InvalidState);
1282 let region_result = error.into_retryable().unwrap().into_handle();
1283 region_result.discard().unwrap();
1284 drop(ambient);
1285 runtime.close().unwrap();
1286 }
1287
1288 #[test]
1289 fn offline_region_apis_use_real_c_abi() {
1291 let mut options = RuntimeOptions::default();
1292 options.cache_path = Some(":memory:".into());
1293 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1294 let definition = test_offline_region_definition("custom://offline-style.json");
1295
1296 let create = runtime
1297 .start_create_offline_region(&definition, b"abc")
1298 .unwrap();
1299 wait_for_operation(&mut runtime, &create, Op::RegionCreate, OpResult::Region).unwrap();
1300 let created = create.take().unwrap();
1301 assert_eq!(created.definition, definition);
1302 assert_eq!(created.metadata, b"abc");
1303
1304 let geometry_definition = OfflineRegionDefinition::GeometryRegion {
1305 style_url: "custom://offline-geometry-style.json".into(),
1306 geometry: br#"{"type":"Point","coordinates":[-122.5,37.5]}"#.to_vec(),
1307 min_zoom: 0.0,
1308 max_zoom: 1.0,
1309 pixel_ratio: 1.0,
1310 include_ideographs: false,
1311 };
1312 let create_geometry = runtime
1313 .start_create_offline_region(&geometry_definition, b"geo")
1314 .unwrap();
1315 wait_for_operation(
1316 &mut runtime,
1317 &create_geometry,
1318 Op::RegionCreate,
1319 OpResult::Region,
1320 )
1321 .unwrap();
1322 let geometry_region = create_geometry.take().unwrap();
1323 assert_eq!(geometry_region.definition, geometry_definition);
1324 assert_eq!(geometry_region.metadata, b"geo");
1325
1326 let get = runtime.start_offline_region(created.id).unwrap();
1327 wait_for_operation(&mut runtime, &get, Op::RegionGet, OpResult::OptionalRegion).unwrap();
1328 let fetched = get.take().unwrap().unwrap();
1329 assert_eq!(fetched, created);
1330
1331 let list = runtime.start_offline_regions().unwrap();
1332 wait_for_operation(&mut runtime, &list, Op::RegionsList, OpResult::RegionList).unwrap();
1333 let listed = list.take().unwrap();
1334 assert!(listed.iter().any(|region| region.id == created.id));
1335
1336 let update = runtime
1337 .start_update_offline_region_metadata(created.id, b"")
1338 .unwrap();
1339 wait_for_operation(
1340 &mut runtime,
1341 &update,
1342 Op::RegionUpdateMetadata,
1343 OpResult::Region,
1344 )
1345 .unwrap();
1346 let updated = update.take().unwrap();
1347 assert_eq!(updated.id, created.id);
1348 assert!(updated.metadata.is_empty());
1349
1350 let status_operation = runtime.start_offline_region_status(created.id).unwrap();
1351 wait_for_operation(
1352 &mut runtime,
1353 &status_operation,
1354 Op::RegionGetStatus,
1355 OpResult::RegionStatus,
1356 )
1357 .unwrap();
1358 let status = status_operation.take().unwrap();
1359 assert!(matches!(
1360 status.download_state,
1361 OfflineRegionDownloadState::Inactive | OfflineRegionDownloadState::Active
1362 ));
1363
1364 let set_inactive = runtime
1365 .start_set_offline_region_download_state(
1366 created.id,
1367 OfflineRegionDownloadState::Inactive,
1368 )
1369 .unwrap();
1370 wait_for_operation(
1371 &mut runtime,
1372 &set_inactive,
1373 Op::RegionSetDownloadState,
1374 OpResult::None,
1375 )
1376 .unwrap();
1377 set_inactive.discard().unwrap();
1378 let error = runtime
1379 .start_set_offline_region_download_state(
1380 created.id,
1381 OfflineRegionDownloadState::Unknown(99),
1382 )
1383 .unwrap_err();
1384 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1385
1386 let observe = runtime
1387 .start_set_offline_region_observed(created.id, true)
1388 .unwrap();
1389 wait_for_operation(
1390 &mut runtime,
1391 &observe,
1392 Op::RegionSetObserved,
1393 OpResult::None,
1394 )
1395 .unwrap();
1396 observe.discard().unwrap();
1397 let unobserve = runtime
1398 .start_set_offline_region_observed(created.id, false)
1399 .unwrap();
1400 wait_for_operation(
1401 &mut runtime,
1402 &unobserve,
1403 Op::RegionSetObserved,
1404 OpResult::None,
1405 )
1406 .unwrap();
1407 unobserve.discard().unwrap();
1408 let invalidate = runtime.start_invalidate_offline_region(created.id).unwrap();
1409 wait_for_operation(
1410 &mut runtime,
1411 &invalidate,
1412 Op::RegionInvalidate,
1413 OpResult::None,
1414 )
1415 .unwrap();
1416 invalidate.discard().unwrap();
1417 let delete = runtime.start_delete_offline_region(created.id).unwrap();
1418 wait_for_operation(&mut runtime, &delete, Op::RegionDelete, OpResult::None).unwrap();
1419 delete.discard().unwrap();
1420 let delete_geometry = runtime
1421 .start_delete_offline_region(geometry_region.id)
1422 .unwrap();
1423 wait_for_operation(
1424 &mut runtime,
1425 &delete_geometry,
1426 Op::RegionDelete,
1427 OpResult::None,
1428 )
1429 .unwrap();
1430 delete_geometry.discard().unwrap();
1431
1432 let missing_created = runtime.start_offline_region(created.id).unwrap();
1433 wait_for_operation(
1434 &mut runtime,
1435 &missing_created,
1436 Op::RegionGet,
1437 OpResult::OptionalRegion,
1438 )
1439 .unwrap();
1440 assert!(missing_created.take().unwrap().is_none());
1441 let missing_geometry = runtime.start_offline_region(geometry_region.id).unwrap();
1442 wait_for_operation(
1443 &mut runtime,
1444 &missing_geometry,
1445 Op::RegionGet,
1446 OpResult::OptionalRegion,
1447 )
1448 .unwrap();
1449 assert!(missing_geometry.take().unwrap().is_none());
1450
1451 runtime.close().unwrap();
1452 }
1453
1454 #[test]
1455 fn offline_region_merge_database_accepts_read_only_source_through_real_c_abi() {
1457 let base = TempDir::new("maplibre-rust-offline-merge");
1458 let main_cache = base.path().join("main.db");
1459 let side_cache = base.path().join("side.db");
1460
1461 let definition = test_offline_region_definition("custom://merge-style.json");
1462 {
1463 let mut side_options = RuntimeOptions::default();
1464 side_options.cache_path = Some(side_cache.to_string_lossy().into_owned());
1465 let mut side_runtime = RuntimeHandle::with_options(&side_options).unwrap();
1466 let create = side_runtime
1467 .start_create_offline_region(&definition, b"merge")
1468 .unwrap();
1469 wait_for_operation(
1470 &mut side_runtime,
1471 &create,
1472 Op::RegionCreate,
1473 OpResult::Region,
1474 )
1475 .unwrap();
1476 create.take().unwrap();
1477 side_runtime.close().unwrap();
1478 }
1479 let side_database_before = std::fs::read(&side_cache).unwrap();
1480
1481 #[cfg(unix)]
1482 {
1483 use std::os::unix::fs::PermissionsExt;
1484
1485 let mut permissions = std::fs::metadata(&side_cache).unwrap().permissions();
1486 permissions.set_mode(0o444);
1487 std::fs::set_permissions(&side_cache, permissions).unwrap();
1488 }
1489
1490 let mut main_options = RuntimeOptions::default();
1491 main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1492 let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1493 let merge = main_runtime
1494 .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1495 .unwrap();
1496 wait_for_operation(
1497 &mut main_runtime,
1498 &merge,
1499 Op::RegionsMergeDatabase,
1500 OpResult::RegionList,
1501 )
1502 .unwrap();
1503 let merged = merge.take().unwrap();
1504 assert_eq!(merged.len(), 1);
1505 assert_eq!(merged[0].definition, definition);
1506 assert_eq!(merged[0].metadata, b"merge");
1507 assert_eq!(std::fs::read(&side_cache).unwrap(), side_database_before);
1508 main_runtime.close().unwrap();
1509 }
1510
1511 fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1512 OfflineRegionDefinition::TilePyramid {
1513 style_url: style_url.into(),
1514 bounds: LatLngBounds::new(
1515 crate::LatLng::new(37.0, -123.0),
1516 crate::LatLng::new(38.0, -122.0),
1517 ),
1518 min_zoom: 0.0,
1519 max_zoom: 1.0,
1520 pixel_ratio: 1.0,
1521 include_ideographs: false,
1522 }
1523 }
1524
1525 struct TempDir {
1526 path: std::path::PathBuf,
1527 }
1528
1529 impl TempDir {
1530 fn new(prefix: &str) -> Self {
1531 let nanos = SystemTime::now()
1532 .duration_since(UNIX_EPOCH)
1533 .unwrap()
1534 .as_nanos();
1535 let path =
1536 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1537 std::fs::create_dir_all(&path).unwrap();
1538 Self { path }
1539 }
1540
1541 fn path(&self) -> &std::path::Path {
1542 &self.path
1543 }
1544 }
1545
1546 impl Drop for TempDir {
1547 fn drop(&mut self) {
1548 let _ = std::fs::remove_dir_all(&self.path);
1549 }
1550 }
1551
1552 #[test]
1553 fn runtime_create_with_explicit_options_uses_real_c_abi() {
1555 let mut options = RuntimeOptions::default();
1556 options.asset_path = Some(String::new());
1557 options.cache_path = Some(String::new());
1558 let runtime = RuntimeHandle::with_options(&options).unwrap();
1559
1560 runtime.pump(Some(Duration::ZERO), None).unwrap();
1561 runtime.close().unwrap();
1562 }
1563
1564 #[test]
1565 fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1567 let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1568 std::ptr::null(),
1569 maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1570 )
1571 .unwrap_err();
1572
1573 assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1574 assert_eq!(error.raw_status(), None);
1575 assert!(
1576 error
1577 .diagnostic()
1578 .contains("unsupported MapLibre Native C ABI version")
1579 );
1580 }
1581
1582 fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1583 runtime
1584 .drain_events(0)
1585 .unwrap()
1586 .iter()
1587 .any(|event| event.event_type() == event_type)
1588 }
1589
1590 fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1591 for _ in 0..100 {
1592 let _ = runtime.pump(Some(Duration::ZERO), None);
1593 if drain_holds_event_type(runtime, event_type) {
1594 return true;
1595 }
1596 std::thread::sleep(Duration::from_millis(10));
1597 }
1598 false
1599 }
1600
1601 fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent {
1602 for _ in 0..100 {
1603 runtime.pump(Some(Duration::ZERO), None).unwrap();
1604 let mut failure = None;
1605 for event in runtime.drain_events(0).unwrap().iter() {
1606 if event.event_type() == RuntimeEventType::MapLoadingFailed {
1607 failure = Some(event.to_owned().unwrap());
1608 break;
1609 }
1610 }
1611 if let Some(failure) = failure {
1612 return failure;
1613 }
1614 std::thread::sleep(Duration::from_millis(10));
1615 }
1616 panic!("expected a map loading-failure event");
1617 }
1618
1619 #[test]
1620 fn runtime_create_run_drain_and_close() {
1622 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1623
1624 runtime.pump(Some(Duration::ZERO), None).unwrap();
1625 let batch = runtime.drain_events(0).unwrap();
1627 assert!(batch.is_empty());
1628 assert_eq!(batch.len(), 0);
1629 assert_eq!(batch.remaining(), 0);
1630 assert_eq!(batch.iter().count(), 0);
1631 assert!(runtime.drain_events(0).unwrap().is_empty());
1633 runtime.close().unwrap();
1634 }
1635
1636 fn quiesce(runtime: &mut RuntimeHandle) {
1639 for _ in 0..100 {
1640 runtime.pump(Some(Duration::ZERO), None).unwrap();
1641 if runtime.drain_events(0).unwrap().is_empty() {
1642 return;
1643 }
1644 }
1645 panic!("the runtime kept producing events while idle");
1646 }
1647
1648 fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1651 let started = Instant::now();
1652 for _ in 0..20 {
1653 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1654 assert!(
1655 started.elapsed() < Duration::from_secs(5),
1656 "parks sat out their timeouts instead of taking wakes"
1657 );
1658 if drain_holds_event_type(runtime, event_type) {
1659 return true;
1660 }
1661 }
1662 false
1663 }
1664
1665 #[test]
1666 fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() {
1668 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1669 runtime
1670 .set_resource_provider(move |request, handle| {
1671 if request.requested_url != "custom://style.json" {
1672 return ResourceProviderDecision::PassThrough;
1673 }
1674 handle
1675 .complete(ResourceResponse::ok(
1676 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1677 ))
1678 .unwrap();
1679 ResourceProviderDecision::PassThrough
1680 })
1681 .unwrap();
1682
1683 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1684 map.set_style_url("custom://style.json").unwrap();
1685 assert!(park_for_runtime_event(
1686 &mut runtime,
1687 RuntimeEventType::MapStyleLoaded
1688 ));
1689
1690 let source = runtime.wake_source().unwrap();
1693 quiesce(&mut runtime);
1694 let signaller = std::thread::spawn(move || {
1695 std::thread::sleep(Duration::from_millis(20));
1696 source.signal().unwrap();
1697 source
1698 });
1699 let started = Instant::now();
1700 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1701 assert!(
1702 started.elapsed() < Duration::from_secs(5),
1703 "the parked owner thread timed out instead of taking the signal"
1704 );
1705 let source = signaller.join().unwrap();
1706
1707 map.close().unwrap();
1710 runtime.close().unwrap();
1711 source.signal().unwrap();
1712 }
1713
1714 #[test]
1715 fn a_pump_clears_the_wake_flag_it_returns_on() {
1717 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1718 let source = runtime.wake_source().unwrap();
1719 quiesce(&mut runtime);
1720
1721 source.signal().unwrap();
1722 let started = Instant::now();
1723 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1724 assert!(
1725 started.elapsed() < Duration::from_secs(5),
1726 "a pump waited even though the wake flag was set"
1727 );
1728
1729 let started = Instant::now();
1731 runtime
1732 .pump(Some(Duration::from_millis(200)), None)
1733 .unwrap();
1734 assert!(
1735 started.elapsed() >= Duration::from_millis(100),
1736 "the first pump left the wake flag set"
1737 );
1738
1739 runtime.close().unwrap();
1740 }
1741
1742 #[test]
1743 fn a_bounded_drain_reports_the_events_it_left_queued() {
1745 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1746 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1747 map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap();
1748 runtime.pump(Some(Duration::ZERO), None).unwrap();
1749
1750 let bounded = runtime.drain_events(1).unwrap();
1751 assert_eq!(bounded.len(), 1);
1752 assert!(
1753 bounded.remaining() > 0,
1754 "a style load should queue more than one event"
1755 );
1756 let rest = runtime.drain_events(0).unwrap();
1757 assert!(rest.len() > 1, "one drain should report the whole queue");
1758 assert_eq!(rest.remaining(), 0);
1759 let first = rest.iter().next().unwrap();
1760 assert_eq!(first.source(), RuntimeEventSource::Map(map.id()));
1761
1762 map.close().unwrap();
1763 runtime.close().unwrap();
1764 }
1765
1766 #[test]
1767 fn a_creation_mask_narrows_a_runtime_before_its_first_operation() {
1769 let mut options = crate::RuntimeOptions::default();
1770 options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED;
1771 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1772
1773 assert_eq!(
1774 runtime.event_mask().unwrap(),
1775 RuntimeEventMask::OFFLINE_OPERATION_COMPLETED
1776 );
1777
1778 let operation = runtime
1781 .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1782 .unwrap();
1783 let completed =
1784 wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
1785 assert_eq!(completed.operation_id, operation.operation_id);
1786 operation.discard().unwrap();
1787
1788 runtime.close().unwrap();
1789
1790 options.event_mask = RuntimeEventMask::from_bits_retain(1 << 63);
1793 let error = RuntimeHandle::with_options(&options).unwrap_err();
1794 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1795 }
1796
1797 #[test]
1798 fn a_runtime_mask_round_trips_and_rejects_undefined_bits() {
1800 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1801
1802 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1804
1805 runtime.set_event_mask(RuntimeEventMask::ALL).unwrap();
1806 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1807
1808 let mut mask = runtime.event_mask().unwrap();
1810 mask.remove(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED);
1811 runtime.set_event_mask(mask).unwrap();
1812 let read_back = runtime.event_mask().unwrap();
1813 assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED));
1814 assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED));
1815 assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED));
1816
1817 let undefined = RuntimeEventMask::from_bits_retain(1 << 63);
1818 let error = runtime.set_event_mask(undefined).unwrap_err();
1819 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1820 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1821 assert_eq!(runtime.event_mask().unwrap(), read_back);
1822
1823 runtime.close().unwrap();
1824 }
1825
1826 #[test]
1827 fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1829 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1830 let runtime_handle = runtime.inner.native().unwrap();
1831
1832 let error = std::thread::spawn(move || {
1833 maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) })
1836 .unwrap_err()
1837 })
1838 .join()
1839 .unwrap();
1840
1841 assert_eq!(error.kind(), ErrorKind::WrongThread);
1842 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1843 assert!(!error.diagnostic().is_empty());
1844 runtime.close().unwrap();
1845 }
1846
1847 #[test]
1848 fn resource_provider_installs_replaces_clears_and_releases_state() {
1850 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1851 let first = Arc::new(());
1852 let first_callback = Arc::clone(&first);
1853
1854 runtime
1855 .set_resource_provider(move |_, _| {
1856 let _ = &first_callback;
1857 crate::ResourceProviderDecision::PassThrough
1858 })
1859 .unwrap();
1860 assert_eq!(Arc::strong_count(&first), 2);
1861
1862 let second = Arc::new(());
1863 let second_callback = Arc::clone(&second);
1864 runtime
1865 .set_resource_provider(move |_, _| {
1866 let _ = &second_callback;
1867 crate::ResourceProviderDecision::PassThrough
1868 })
1869 .unwrap();
1870 assert_eq!(Arc::strong_count(&first), 1);
1871 assert_eq!(Arc::strong_count(&second), 2);
1872
1873 runtime.clear_resource_provider().unwrap();
1874 assert_eq!(Arc::strong_count(&second), 1);
1875
1876 let third = Arc::new(());
1877 let third_callback = Arc::clone(&third);
1878 runtime
1879 .set_resource_provider(move |_, _| {
1880 let _ = &third_callback;
1881 crate::ResourceProviderDecision::PassThrough
1882 })
1883 .unwrap();
1884 assert_eq!(Arc::strong_count(&third), 2);
1885
1886 runtime.close().unwrap();
1887 assert_eq!(Arc::strong_count(&third), 1);
1888 }
1889
1890 #[test]
1891 fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1893 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1894 let first = Arc::new(());
1895 let first_callback = Arc::clone(&first);
1896 runtime
1897 .set_resource_provider(move |_, _| {
1898 let _ = &first_callback;
1899 crate::ResourceProviderDecision::PassThrough
1900 })
1901 .unwrap();
1902
1903 let second = Arc::new(());
1904 let second_callback = Arc::clone(&second);
1905 let error = runtime
1906 .inner
1907 .set_resource_provider_with_rejected_descriptor_for_testing(move |_, _| {
1908 let _ = &second_callback;
1909 crate::ResourceProviderDecision::PassThrough
1910 })
1911 .unwrap_err();
1912
1913 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1914 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1915 assert_eq!(Arc::strong_count(&first), 2);
1916 assert_eq!(Arc::strong_count(&second), 1);
1917
1918 runtime.close().unwrap();
1919 assert_eq!(Arc::strong_count(&first), 1);
1920 }
1921
1922 fn load_probe_style(runtime: &mut RuntimeHandle, map: &MapHandle, style_url: &str) {
1926 map.set_style_url(style_url).unwrap();
1927 let event = wait_for_map_loading_failure(runtime);
1928 assert!(
1929 event
1930 .message
1931 .as_deref()
1932 .is_some_and(|message| message.contains("\"jar\""))
1933 );
1934 }
1935
1936 #[test]
1937 fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() {
1939 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1940 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1941
1942 let first_calls = Arc::new(AtomicUsize::new(0));
1943 let first_callback_calls = Arc::clone(&first_calls);
1944 runtime
1945 .set_resource_provider(move |_, _| {
1946 first_callback_calls.fetch_add(1, Ordering::SeqCst);
1947 ResourceProviderDecision::PassThrough
1948 })
1949 .unwrap();
1950 load_probe_style(&mut runtime, &map, "jar:file:/packaged/first.json");
1951 assert!(first_calls.load(Ordering::SeqCst) > 0);
1952
1953 let second_calls = Arc::new(AtomicUsize::new(0));
1954 let second_callback_calls = Arc::clone(&second_calls);
1955 runtime
1956 .set_resource_provider(move |_, _| {
1957 second_callback_calls.fetch_add(1, Ordering::SeqCst);
1958 ResourceProviderDecision::PassThrough
1959 })
1960 .unwrap();
1961 let first_calls_after_replace = first_calls.load(Ordering::SeqCst);
1962 load_probe_style(&mut runtime, &map, "jar:file:/packaged/second.json");
1963 assert!(second_calls.load(Ordering::SeqCst) > 0);
1964 assert_eq!(
1965 first_calls.load(Ordering::SeqCst),
1966 first_calls_after_replace
1967 );
1968
1969 runtime.clear_resource_provider().unwrap();
1970 let second_calls_after_clear = second_calls.load(Ordering::SeqCst);
1971 load_probe_style(&mut runtime, &map, "jar:file:/packaged/third.json");
1972 assert_eq!(
1973 first_calls.load(Ordering::SeqCst),
1974 first_calls_after_replace
1975 );
1976 assert_eq!(
1977 second_calls.load(Ordering::SeqCst),
1978 second_calls_after_clear
1979 );
1980
1981 runtime.clear_resource_provider().unwrap();
1983
1984 map.close().unwrap();
1985 runtime.close().unwrap();
1986 }
1987
1988 #[test]
1989 fn resource_provider_completes_style_request_inline_through_c_abi() {
1991 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1992 let calls = Arc::new(AtomicUsize::new(0));
1993 let callback_calls = Arc::clone(&calls);
1994 runtime
1995 .set_resource_provider(move |request, handle| {
1996 if request.requested_url != "custom://style.json" {
1997 return ResourceProviderDecision::PassThrough;
1998 }
1999 callback_calls.fetch_add(1, Ordering::SeqCst);
2000 assert_eq!(request.kind, ResourceKind::Style);
2001 handle
2002 .complete(ResourceResponse::ok(
2003 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2004 ))
2005 .unwrap();
2006 ResourceProviderDecision::PassThrough
2007 })
2008 .unwrap();
2009
2010 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2011 map.set_style_url("custom://style.json").unwrap();
2012
2013 assert!(wait_for_runtime_event(
2014 &mut runtime,
2015 RuntimeEventType::MapStyleLoaded
2016 ));
2017 assert_eq!(calls.load(Ordering::SeqCst), 1);
2018 map.close().unwrap();
2019 runtime.close().unwrap();
2020 }
2021
2022 #[test]
2023 fn resource_provider_sees_scheme_alias_and_its_resolved_url() {
2025 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2026 let resolved = Arc::new(Mutex::new(None));
2027 let callback_resolved = Arc::clone(&resolved);
2028 runtime
2029 .set_resource_provider(move |request, handle| {
2030 if request.requested_url != "maplibre://maps/style" {
2031 return ResourceProviderDecision::PassThrough;
2032 }
2033 *callback_resolved.lock().unwrap() = Some(request.resolved_url.clone());
2034 handle
2035 .complete(ResourceResponse::ok(
2036 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2037 ))
2038 .unwrap();
2039 ResourceProviderDecision::Handle
2040 })
2041 .unwrap();
2042
2043 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2044 map.set_style_url("maplibre://maps/style").unwrap();
2045
2046 assert!(wait_for_runtime_event(
2047 &mut runtime,
2048 RuntimeEventType::MapStyleLoaded
2049 ));
2050 assert_eq!(
2051 resolved.lock().unwrap().as_deref(),
2052 Some("https://demotiles.maplibre.org/style.json")
2053 );
2054 map.close().unwrap();
2055 runtime.close().unwrap();
2056 }
2057
2058 #[test]
2059 fn resource_provider_completes_style_request_from_another_thread() {
2061 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2062 let (sender, receiver) = std::sync::mpsc::channel();
2063 runtime
2064 .set_resource_provider(move |request, handle| {
2065 if request.requested_url == "custom://async-style.json" {
2066 sender.send(handle).unwrap();
2067 ResourceProviderDecision::Handle
2068 } else {
2069 ResourceProviderDecision::PassThrough
2070 }
2071 })
2072 .unwrap();
2073
2074 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2075 map.set_style_url("custom://async-style.json").unwrap();
2076 let handle = receiver
2077 .recv_timeout(Duration::from_secs(5))
2078 .expect("provider should send handled request");
2079 assert!(!handle.is_cancelled().unwrap());
2080 std::thread::spawn(move || {
2081 handle
2082 .complete(ResourceResponse::ok(
2083 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2084 ))
2085 .unwrap();
2086 })
2087 .join()
2088 .unwrap();
2089
2090 assert!(wait_for_runtime_event(
2091 &mut runtime,
2092 RuntimeEventType::MapStyleLoaded
2093 ));
2094 map.close().unwrap();
2095 runtime.close().unwrap();
2096 }
2097
2098 fn pump_until(runtime: &mut RuntimeHandle, condition: impl Fn() -> bool) -> bool {
2100 for _ in 0..100 {
2101 if condition() {
2102 return true;
2103 }
2104 runtime.pump(Some(Duration::ZERO), None).unwrap();
2105 std::thread::sleep(Duration::from_millis(10));
2106 }
2107 condition()
2108 }
2109
2110 #[test]
2111 fn cancel_callback_runs_once_when_the_map_discards_the_request() {
2113 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2114 let cancels = Arc::new(AtomicUsize::new(0));
2115 let callback_cancels = Arc::clone(&cancels);
2116 let (sender, receiver) = std::sync::mpsc::channel();
2117 runtime
2118 .set_resource_provider(move |request, handle| {
2119 if request.requested_url != "custom://cancel-style.json" {
2120 return ResourceProviderDecision::PassThrough;
2121 }
2122 let cancels = Arc::clone(&callback_cancels);
2123 handle
2124 .set_cancel_callback(move || {
2125 cancels.fetch_add(1, Ordering::SeqCst);
2126 })
2127 .unwrap();
2128 sender.send(handle).unwrap();
2129 ResourceProviderDecision::Handle
2130 })
2131 .unwrap();
2132
2133 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2134 map.set_style_url("custom://cancel-style.json").unwrap();
2135 let handle = receiver
2136 .recv_timeout(Duration::from_secs(5))
2137 .expect("provider should send handled request");
2138 assert_eq!(cancels.load(Ordering::SeqCst), 0);
2139
2140 map.close().unwrap();
2141
2142 assert!(pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) == 1));
2143 assert!(handle.is_cancelled().unwrap());
2144 assert_eq!(
2146 handle
2147 .complete(ResourceResponse::no_content())
2148 .unwrap_err()
2149 .kind(),
2150 ErrorKind::InvalidState
2151 );
2152 assert_eq!(cancels.load(Ordering::SeqCst), 1);
2153 runtime.close().unwrap();
2154 }
2155
2156 #[test]
2157 fn cancel_callback_may_close_its_own_request() {
2159 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2160 let cancelled_request: Arc<Mutex<Option<crate::ResourceRequestHandle>>> =
2161 Arc::new(Mutex::new(None));
2162 let provider_request = Arc::clone(&cancelled_request);
2163 let closed = Arc::new(AtomicUsize::new(0));
2164 let callback_closed = Arc::clone(&closed);
2165 runtime
2166 .set_resource_provider(move |request, handle| {
2167 if request.requested_url != "custom://cancel-style.json" {
2168 return ResourceProviderDecision::PassThrough;
2169 }
2170 let callback_request = Arc::clone(&provider_request);
2171 let closed = Arc::clone(&callback_closed);
2172 handle
2173 .set_cancel_callback(move || {
2174 let request = callback_request.lock().unwrap().take();
2175 request.expect("the cancelled request").close();
2176 closed.fetch_add(1, Ordering::SeqCst);
2177 })
2178 .unwrap();
2179 *provider_request.lock().unwrap() = Some(handle);
2180 ResourceProviderDecision::Handle
2181 })
2182 .unwrap();
2183
2184 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2185 map.set_style_url("custom://cancel-style.json").unwrap();
2186 assert!(pump_until(&mut runtime, || cancelled_request
2187 .lock()
2188 .unwrap()
2189 .is_some()));
2190
2191 map.close().unwrap();
2192
2193 assert!(pump_until(&mut runtime, || closed.load(Ordering::SeqCst) == 1));
2194 assert!(cancelled_request.lock().unwrap().is_none());
2195 runtime.close().unwrap();
2196 }
2197
2198 #[test]
2199 fn cancel_callback_skips_a_completed_request() {
2201 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2202 let cancels = Arc::new(AtomicUsize::new(0));
2203 let callback_cancels = Arc::clone(&cancels);
2204 runtime
2205 .set_resource_provider(move |request, handle| {
2206 if request.requested_url != "custom://cancel-style.json" {
2207 return ResourceProviderDecision::PassThrough;
2208 }
2209 let cancels = Arc::clone(&callback_cancels);
2210 handle
2211 .set_cancel_callback(move || {
2212 cancels.fetch_add(1, Ordering::SeqCst);
2213 })
2214 .unwrap();
2215 handle
2216 .complete(ResourceResponse::ok(
2217 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2218 ))
2219 .unwrap();
2220 ResourceProviderDecision::Handle
2221 })
2222 .unwrap();
2223
2224 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2225 map.set_style_url("custom://cancel-style.json").unwrap();
2226 assert!(wait_for_runtime_event(
2227 &mut runtime,
2228 RuntimeEventType::MapStyleLoaded
2229 ));
2230
2231 map.close().unwrap();
2232
2233 assert!(!pump_until(&mut runtime, || cancels.load(Ordering::SeqCst) > 0));
2236 runtime.close().unwrap();
2237 }
2238
2239 #[test]
2240 fn cancel_registration_on_a_cancelled_request_runs_before_returning() {
2242 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2243 let (sender, receiver) = std::sync::mpsc::channel();
2244 runtime
2245 .set_resource_provider(move |request, handle| {
2246 if request.requested_url != "custom://cancel-style.json" {
2247 return ResourceProviderDecision::PassThrough;
2248 }
2249 sender.send(handle).unwrap();
2250 ResourceProviderDecision::Handle
2251 })
2252 .unwrap();
2253
2254 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2255 map.set_style_url("custom://cancel-style.json").unwrap();
2256 let handle = receiver
2257 .recv_timeout(Duration::from_secs(5))
2258 .expect("provider should send handled request");
2259 map.close().unwrap();
2260 assert!(pump_until(&mut runtime, || handle.is_cancelled().unwrap()));
2261
2262 let cancels = Arc::new(AtomicUsize::new(0));
2263 let callback_cancels = Arc::clone(&cancels);
2264 handle
2265 .set_cancel_callback(move || {
2266 callback_cancels.fetch_add(1, Ordering::SeqCst);
2267 })
2268 .unwrap();
2269 assert_eq!(cancels.load(Ordering::SeqCst), 1);
2270
2271 assert_eq!(
2273 handle.set_cancel_callback(|| {}).unwrap_err().kind(),
2274 ErrorKind::InvalidState
2275 );
2276 runtime.close().unwrap();
2277 }
2278
2279 #[test]
2280 fn resource_provider_error_response_becomes_copied_loading_failure_event() {
2282 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2283 runtime
2284 .set_resource_provider(move |request, handle| {
2285 if request.requested_url == "custom://broken-style.json" {
2286 handle
2287 .complete(ResourceResponse::error(
2288 ResourceErrorReason::Other,
2289 "provider failed",
2290 ))
2291 .unwrap();
2292 ResourceProviderDecision::Handle
2293 } else {
2294 ResourceProviderDecision::PassThrough
2295 }
2296 })
2297 .unwrap();
2298
2299 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2300 let map_id = map.id();
2301 map.set_style_url("custom://broken-style.json").unwrap();
2302
2303 let event = wait_for_map_loading_failure(&mut runtime);
2304 let copied_message = event.message.clone();
2305 let _ = runtime.drain_events(0).unwrap();
2307
2308 assert_eq!(event.source, RuntimeEventSource::Map(map_id));
2309 assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
2310 assert_eq!(event.message, copied_message);
2311 assert!(
2312 event
2313 .message
2314 .as_deref()
2315 .is_some_and(|message| message.contains("provider failed"))
2316 );
2317
2318 map.close().unwrap();
2319 runtime.close().unwrap();
2320 }
2321
2322 #[test]
2323 fn resource_transform_installs_replaces_clears_and_releases_state() {
2325 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2326 let first = Arc::new(());
2327 let first_callback = Arc::clone(&first);
2328
2329 runtime
2330 .set_resource_transform(move |request| {
2331 let _ = &first_callback;
2332 assert!(matches!(
2333 request.kind,
2334 ResourceKind::Style | ResourceKind::UnknownRaw(_)
2335 ));
2336 None
2337 })
2338 .unwrap();
2339 assert_eq!(Arc::strong_count(&first), 2);
2340
2341 let second = Arc::new(());
2342 let second_callback = Arc::clone(&second);
2343 runtime
2344 .set_resource_transform(move |_| {
2345 let _ = &second_callback;
2346 Some("https://example.test/replacement".to_owned())
2347 })
2348 .unwrap();
2349 assert_eq!(Arc::strong_count(&first), 1);
2350 assert_eq!(Arc::strong_count(&second), 2);
2351
2352 runtime.clear_resource_transform().unwrap();
2353 assert_eq!(Arc::strong_count(&second), 1);
2354 runtime.close().unwrap();
2355 }
2356
2357 #[cfg(target_os = "emscripten")]
2363 #[test]
2364 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2366 let origin = std::env::var("MLN_FFI_TEST_FIXTURE_ORIGIN").expect(
2367 "MLN_FFI_TEST_FIXTURE_ORIGIN is unset; run the suite through \
2368 `mise run //bindings/rust:test emscripten-wasm32-webgl`",
2369 );
2370 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2371 let transform_url = format!("{origin}/__fixture/rewritten-style.json");
2372 runtime
2375 .set_resource_transform(move |request| {
2376 (request.url.ends_with("/original-style.json")
2377 || request.url.ends_with("/original-after-clear.json"))
2378 .then(|| transform_url.clone())
2379 })
2380 .unwrap();
2381
2382 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2383 map.set_style_url(&format!("{origin}/__fixture/original-style.json"))
2384 .unwrap();
2385 assert!(wait_for_runtime_event(
2386 &mut runtime,
2387 RuntimeEventType::MapStyleLoaded
2388 ));
2389 assert!(
2392 map.style_layer_ids()
2393 .unwrap()
2394 .iter()
2395 .any(|id| id == "rewritten")
2396 );
2397
2398 runtime.clear_resource_transform().unwrap();
2399 map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json"))
2400 .unwrap();
2401 assert!(wait_for_runtime_event(
2402 &mut runtime,
2403 RuntimeEventType::MapStyleLoaded
2404 ));
2405 assert!(
2406 map.style_layer_ids()
2407 .unwrap()
2408 .iter()
2409 .any(|id| id == "original-after-clear")
2410 );
2411
2412 map.close().unwrap();
2413 runtime.close().unwrap();
2414 }
2415
2416 #[cfg(not(target_os = "emscripten"))]
2417 #[test]
2418 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2420 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2421 let (base_url, requests, server) = spawn_style_server(2);
2422 let transform_base_url = base_url.clone();
2423
2424 runtime
2428 .set_resource_transform(move |request| {
2429 if request.url.ends_with("/original-style.json")
2430 || request.url.ends_with("/original-after-clear.json")
2431 {
2432 Some(format!("{transform_base_url}/rewritten-style.json"))
2433 } else {
2434 None
2435 }
2436 })
2437 .unwrap();
2438
2439 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2440 map.set_style_url(&format!("{base_url}/original-style.json"))
2441 .unwrap();
2442 assert!(wait_for_runtime_event(
2443 &mut runtime,
2444 RuntimeEventType::MapStyleLoaded
2445 ));
2446 assert_eq!(
2447 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2448 "/rewritten-style.json"
2449 );
2450
2451 runtime.clear_resource_transform().unwrap();
2452 map.set_style_url(&format!("{base_url}/original-after-clear.json"))
2453 .unwrap();
2454 assert!(wait_for_runtime_event(
2455 &mut runtime,
2456 RuntimeEventType::MapStyleLoaded
2457 ));
2458 assert_eq!(
2459 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2460 "/original-after-clear.json"
2461 );
2462
2463 map.close().unwrap();
2464 runtime.close().unwrap();
2465 server.join().unwrap();
2466 }
2467
2468 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2469 #[test]
2470 fn http_header_transform_reaches_requests_and_clear_stops_it() {
2472 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2473 let (base_url, requests, server) = spawn_recording_style_server(2);
2474 runtime
2475 .set_http_header_transform(|request| {
2476 assert_eq!(request.kind, ResourceKind::Style);
2477 vec![crate::HttpHeader::new("X-Map-Token", "secret")]
2478 })
2479 .unwrap();
2480
2481 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2482 map.set_style_url(&format!("{base_url}/with-header.json"))
2483 .unwrap();
2484 assert!(wait_for_runtime_event(
2485 &mut runtime,
2486 RuntimeEventType::MapStyleLoaded
2487 ));
2488 let first = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2489 assert!(
2490 first
2491 .lines()
2492 .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"))
2493 );
2494
2495 runtime.clear_http_header_transform().unwrap();
2496 map.set_style_url(&format!("{base_url}/after-clear.json"))
2497 .unwrap();
2498 assert!(wait_for_runtime_event(
2499 &mut runtime,
2500 RuntimeEventType::MapStyleLoaded
2501 ));
2502 let second = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2503 assert!(
2504 !second
2505 .lines()
2506 .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:"))
2507 );
2508
2509 map.close().unwrap();
2510 runtime.close().unwrap();
2511 server.join().unwrap();
2512 }
2513
2514 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2515 #[test]
2516 fn http_header_transform_skips_non_http_urls() {
2518 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2519 runtime.set_resource_transform(|_| None).unwrap();
2520 let calls = Arc::new(AtomicUsize::new(0));
2521 let callback_calls = Arc::clone(&calls);
2522 runtime
2523 .set_http_header_transform(move |_| {
2524 callback_calls.fetch_add(1, Ordering::SeqCst);
2525 Vec::new()
2526 })
2527 .unwrap();
2528 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2529
2530 map.set_style_url("jar:file:/packaged/style.json").unwrap();
2531 let _ = wait_for_map_loading_failure(&mut runtime);
2532 assert_eq!(calls.load(Ordering::SeqCst), 0);
2533
2534 map.close().unwrap();
2535 runtime.close().unwrap();
2536 }
2537
2538 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2539 #[test]
2540 fn http_header_transform_preserves_same_origin_and_strips_cross_origin_redirects() {
2542 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2543 let (origin_url, requests, servers) = spawn_redirect_style_servers();
2544 runtime
2545 .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")])
2546 .unwrap();
2547 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2548
2549 map.set_style_url(&format!("{origin_url}/same-start.json"))
2550 .unwrap();
2551 assert!(wait_for_runtime_event(
2552 &mut runtime,
2553 RuntimeEventType::MapStyleLoaded
2554 ));
2555 assert_eq!(
2556 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2557 ("/same-start.json".to_owned(), true)
2558 );
2559 assert_eq!(
2560 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2561 ("/same-final.json".to_owned(), true)
2562 );
2563
2564 map.set_style_url(&format!("{origin_url}/cross-start.json"))
2565 .unwrap();
2566 assert!(wait_for_runtime_event(
2567 &mut runtime,
2568 RuntimeEventType::MapStyleLoaded
2569 ));
2570 assert_eq!(
2571 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2572 ("/cross-start.json".to_owned(), true)
2573 );
2574 assert_eq!(
2575 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2576 ("/cross-final.json".to_owned(), false)
2577 );
2578
2579 map.close().unwrap();
2580 runtime.close().unwrap();
2581 for server in servers {
2582 server.join().unwrap();
2583 }
2584 }
2585
2586 #[cfg(any(target_env = "ohos", target_os = "emscripten"))]
2591 #[test]
2592 fn http_header_transform_reports_unsupported() {
2593 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2594 let error = runtime
2595 .set_http_header_transform(|_| Vec::new())
2596 .unwrap_err();
2597 assert_eq!(error.kind(), ErrorKind::Unsupported);
2598 runtime.close().unwrap();
2599 }
2600
2601 #[test]
2602 fn resource_transform_replacement_after_map_creation_releases_previous_state() {
2604 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2605 let first = Arc::new(());
2606 let first_callback = Arc::clone(&first);
2607 runtime
2608 .set_resource_transform(move |_| {
2609 let _ = &first_callback;
2610 None
2611 })
2612 .unwrap();
2613 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2614
2615 let second = Arc::new(());
2616 let second_callback = Arc::clone(&second);
2617 runtime
2618 .set_resource_transform(move |_| {
2619 let _ = &second_callback;
2620 None
2621 })
2622 .unwrap();
2623
2624 assert_eq!(Arc::strong_count(&first), 1);
2625 assert_eq!(Arc::strong_count(&second), 2);
2626
2627 map.close().unwrap();
2628 runtime.close().unwrap();
2629 assert_eq!(Arc::strong_count(&second), 1);
2630 }
2631
2632 #[test]
2633 fn runtime_teardown_releases_resource_transform_state() {
2635 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2636 let token = Arc::new(());
2637 let callback_token = Arc::clone(&token);
2638 runtime
2639 .set_resource_transform(move |_| {
2640 let _ = &callback_token;
2641 None
2642 })
2643 .unwrap();
2644 assert_eq!(Arc::strong_count(&token), 2);
2645
2646 runtime.close().unwrap();
2647
2648 assert_eq!(Arc::strong_count(&token), 1);
2649 }
2650
2651 #[test]
2652 fn resource_transform_installs_after_map_creation() {
2655 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2656 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2657
2658 runtime.set_resource_transform(|_| None).unwrap();
2659
2660 map.close().unwrap();
2661 runtime.close().unwrap();
2662 }
2663
2664 #[test]
2665 fn resource_transform_clears_after_map_was_closed_and_releases_state() {
2667 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2668 let token = Arc::new(());
2669 let callback_token = Arc::clone(&token);
2670 runtime
2671 .set_resource_transform(move |_| {
2672 let _ = &callback_token;
2673 None
2674 })
2675 .unwrap();
2676 assert_eq!(Arc::strong_count(&token), 2);
2677
2678 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2679 map.close().unwrap();
2680
2681 runtime.clear_resource_transform().unwrap();
2682
2683 assert_eq!(Arc::strong_count(&token), 1);
2684
2685 runtime.close().unwrap();
2686 }
2687
2688 #[test]
2689 fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() {
2691 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2692 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2693 let map_id = map.id();
2694
2695 let error = map.set_style_json(b"{").unwrap_err();
2696 assert_eq!(error.kind(), ErrorKind::NativeError);
2697 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
2698
2699 let batch = runtime.drain_events(0).unwrap();
2700 let types = batch
2701 .iter()
2702 .map(|event| event.event_type())
2703 .collect::<Vec<_>>();
2704 assert!(
2705 types.len() > 1,
2706 "a failed style load should queue more than one event, got {types:?}"
2707 );
2708 let loading_failed = batch
2709 .iter()
2710 .find(|event| event.event_type() == RuntimeEventType::MapLoadingFailed)
2711 .expect("a malformed style should queue a loading-failed event");
2712 assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id));
2713 assert!(
2714 loading_failed
2715 .message()
2716 .unwrap()
2717 .is_some_and(|message| !message.is_empty())
2718 );
2719 let owned = loading_failed.to_owned().unwrap();
2720
2721 assert!(runtime.drain_events(0).unwrap().is_empty());
2724 assert_eq!(owned.source, RuntimeEventSource::Map(map_id));
2725 assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed);
2726 assert!(
2727 owned
2728 .message
2729 .as_deref()
2730 .is_some_and(|message| !message.is_empty())
2731 );
2732
2733 map.close().unwrap();
2734 runtime.close().unwrap();
2735 }
2736
2737 #[test]
2738 fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
2740 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2741 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2742
2743 let error = runtime.close().unwrap_err();
2744 assert_eq!(error.kind(), ErrorKind::InvalidState);
2745 assert_eq!(error.raw_status(), None);
2746 let runtime = error.into_handle();
2747
2748 runtime.pump(Some(Duration::ZERO), None).unwrap();
2749 map.close().unwrap();
2750 runtime.close().unwrap();
2751 }
2752}