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_uses_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
1480 let mut main_options = RuntimeOptions::default();
1481 main_options.cache_path = Some(main_cache.to_string_lossy().into_owned());
1482 let mut main_runtime = RuntimeHandle::with_options(&main_options).unwrap();
1483 let merge = main_runtime
1484 .start_merge_offline_regions_database(&side_cache.to_string_lossy())
1485 .unwrap();
1486 wait_for_operation(
1487 &mut main_runtime,
1488 &merge,
1489 Op::RegionsMergeDatabase,
1490 OpResult::RegionList,
1491 )
1492 .unwrap();
1493 let merged = merge.take().unwrap();
1494 assert_eq!(merged.len(), 1);
1495 assert_eq!(merged[0].definition, definition);
1496 assert_eq!(merged[0].metadata, b"merge");
1497 main_runtime.close().unwrap();
1498 }
1499
1500 fn test_offline_region_definition(style_url: &str) -> OfflineRegionDefinition {
1501 OfflineRegionDefinition::TilePyramid {
1502 style_url: style_url.into(),
1503 bounds: LatLngBounds::new(
1504 crate::LatLng::new(37.0, -123.0),
1505 crate::LatLng::new(38.0, -122.0),
1506 ),
1507 min_zoom: 0.0,
1508 max_zoom: 1.0,
1509 pixel_ratio: 1.0,
1510 include_ideographs: false,
1511 }
1512 }
1513
1514 struct TempDir {
1515 path: std::path::PathBuf,
1516 }
1517
1518 impl TempDir {
1519 fn new(prefix: &str) -> Self {
1520 let nanos = SystemTime::now()
1521 .duration_since(UNIX_EPOCH)
1522 .unwrap()
1523 .as_nanos();
1524 let path =
1525 std::env::temp_dir().join(format!("{prefix}-{}-{nanos}", std::process::id()));
1526 std::fs::create_dir_all(&path).unwrap();
1527 Self { path }
1528 }
1529
1530 fn path(&self) -> &std::path::Path {
1531 &self.path
1532 }
1533 }
1534
1535 impl Drop for TempDir {
1536 fn drop(&mut self) {
1537 let _ = std::fs::remove_dir_all(&self.path);
1538 }
1539 }
1540
1541 #[test]
1542 fn runtime_create_with_explicit_options_uses_real_c_abi() {
1544 let mut options = RuntimeOptions::default();
1545 options.asset_path = Some(String::new());
1546 options.cache_path = Some(String::new());
1547 let runtime = RuntimeHandle::with_options(&options).unwrap();
1548
1549 runtime.pump(Some(Duration::ZERO), None).unwrap();
1550 runtime.close().unwrap();
1551 }
1552
1553 #[test]
1554 fn runtime_creation_rejects_abi_mismatch_before_storing_handle() {
1556 let error = RuntimeHandle::create_with_native_options_after_abi_version_check_for_testing(
1557 std::ptr::null(),
1558 maplibre_core::EXPECTED_C_ABI_VERSION + 1,
1559 )
1560 .unwrap_err();
1561
1562 assert_eq!(error.kind(), ErrorKind::AbiVersionMismatch);
1563 assert_eq!(error.raw_status(), None);
1564 assert!(
1565 error
1566 .diagnostic()
1567 .contains("unsupported MapLibre Native C ABI version")
1568 );
1569 }
1570
1571 fn drain_holds_event_type(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1572 runtime
1573 .drain_events(0)
1574 .unwrap()
1575 .iter()
1576 .any(|event| event.event_type() == event_type)
1577 }
1578
1579 fn wait_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1580 for _ in 0..100 {
1581 let _ = runtime.pump(Some(Duration::ZERO), None);
1582 if drain_holds_event_type(runtime, event_type) {
1583 return true;
1584 }
1585 std::thread::sleep(Duration::from_millis(10));
1586 }
1587 false
1588 }
1589
1590 fn wait_for_map_loading_failure(runtime: &mut RuntimeHandle) -> RuntimeEvent {
1591 for _ in 0..100 {
1592 runtime.pump(Some(Duration::ZERO), None).unwrap();
1593 let mut failure = None;
1594 for event in runtime.drain_events(0).unwrap().iter() {
1595 if event.event_type() == RuntimeEventType::MapLoadingFailed {
1596 failure = Some(event.to_owned().unwrap());
1597 break;
1598 }
1599 }
1600 if let Some(failure) = failure {
1601 return failure;
1602 }
1603 std::thread::sleep(Duration::from_millis(10));
1604 }
1605 panic!("expected a map loading-failure event");
1606 }
1607
1608 #[test]
1609 fn runtime_create_run_drain_and_close() {
1611 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1612
1613 runtime.pump(Some(Duration::ZERO), None).unwrap();
1614 let batch = runtime.drain_events(0).unwrap();
1616 assert!(batch.is_empty());
1617 assert_eq!(batch.len(), 0);
1618 assert_eq!(batch.remaining(), 0);
1619 assert_eq!(batch.iter().count(), 0);
1620 assert!(runtime.drain_events(0).unwrap().is_empty());
1622 runtime.close().unwrap();
1623 }
1624
1625 fn quiesce(runtime: &mut RuntimeHandle) {
1628 for _ in 0..100 {
1629 runtime.pump(Some(Duration::ZERO), None).unwrap();
1630 if runtime.drain_events(0).unwrap().is_empty() {
1631 return;
1632 }
1633 }
1634 panic!("the runtime kept producing events while idle");
1635 }
1636
1637 fn park_for_runtime_event(runtime: &mut RuntimeHandle, event_type: RuntimeEventType) -> bool {
1640 let started = Instant::now();
1641 for _ in 0..20 {
1642 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1643 assert!(
1644 started.elapsed() < Duration::from_secs(5),
1645 "parks sat out their timeouts instead of taking wakes"
1646 );
1647 if drain_holds_event_type(runtime, event_type) {
1648 return true;
1649 }
1650 }
1651 false
1652 }
1653
1654 #[test]
1655 fn parked_owner_thread_wakes_for_native_work_and_for_a_wake_source() {
1657 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1658 runtime
1659 .set_resource_provider(move |request, handle| {
1660 if request.requested_url != "custom://style.json" {
1661 return ResourceProviderDecision::PassThrough;
1662 }
1663 handle
1664 .complete(ResourceResponse::ok(
1665 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1666 ))
1667 .unwrap();
1668 ResourceProviderDecision::PassThrough
1669 })
1670 .unwrap();
1671
1672 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1673 map.set_style_url("custom://style.json").unwrap();
1674 assert!(park_for_runtime_event(
1675 &mut runtime,
1676 RuntimeEventType::MapStyleLoaded
1677 ));
1678
1679 let source = runtime.wake_source().unwrap();
1682 quiesce(&mut runtime);
1683 let signaller = std::thread::spawn(move || {
1684 std::thread::sleep(Duration::from_millis(20));
1685 source.signal().unwrap();
1686 source
1687 });
1688 let started = Instant::now();
1689 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1690 assert!(
1691 started.elapsed() < Duration::from_secs(5),
1692 "the parked owner thread timed out instead of taking the signal"
1693 );
1694 let source = signaller.join().unwrap();
1695
1696 map.close().unwrap();
1699 runtime.close().unwrap();
1700 source.signal().unwrap();
1701 }
1702
1703 #[test]
1704 fn a_pump_clears_the_wake_flag_it_returns_on() {
1706 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1707 let source = runtime.wake_source().unwrap();
1708 quiesce(&mut runtime);
1709
1710 source.signal().unwrap();
1711 let started = Instant::now();
1712 runtime.pump(Some(Duration::from_secs(10)), None).unwrap();
1713 assert!(
1714 started.elapsed() < Duration::from_secs(5),
1715 "a pump waited even though the wake flag was set"
1716 );
1717
1718 let started = Instant::now();
1720 runtime
1721 .pump(Some(Duration::from_millis(200)), None)
1722 .unwrap();
1723 assert!(
1724 started.elapsed() >= Duration::from_millis(100),
1725 "the first pump left the wake flag set"
1726 );
1727
1728 runtime.close().unwrap();
1729 }
1730
1731 #[test]
1732 fn a_bounded_drain_reports_the_events_it_left_queued() {
1734 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1735 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1736 map.set_style_json(PROVIDER_STYLE_JSON.as_bytes()).unwrap();
1737 runtime.pump(Some(Duration::ZERO), None).unwrap();
1738
1739 let bounded = runtime.drain_events(1).unwrap();
1740 assert_eq!(bounded.len(), 1);
1741 assert!(
1742 bounded.remaining() > 0,
1743 "a style load should queue more than one event"
1744 );
1745 let rest = runtime.drain_events(0).unwrap();
1746 assert!(rest.len() > 1, "one drain should report the whole queue");
1747 assert_eq!(rest.remaining(), 0);
1748 let first = rest.iter().next().unwrap();
1749 assert_eq!(first.source(), RuntimeEventSource::Map(map.id()));
1750
1751 map.close().unwrap();
1752 runtime.close().unwrap();
1753 }
1754
1755 #[test]
1756 fn a_creation_mask_narrows_a_runtime_before_its_first_operation() {
1758 let mut options = crate::RuntimeOptions::default();
1759 options.event_mask = RuntimeEventMask::OFFLINE_OPERATION_COMPLETED;
1760 let mut runtime = RuntimeHandle::with_options(&options).unwrap();
1761
1762 assert_eq!(
1763 runtime.event_mask().unwrap(),
1764 RuntimeEventMask::OFFLINE_OPERATION_COMPLETED
1765 );
1766
1767 let operation = runtime
1770 .start_ambient_cache_operation(AmbientCacheOperation::Clear)
1771 .unwrap();
1772 let completed =
1773 wait_for_operation(&mut runtime, &operation, Op::AmbientCache, OpResult::None).unwrap();
1774 assert_eq!(completed.operation_id, operation.operation_id);
1775 operation.discard().unwrap();
1776
1777 runtime.close().unwrap();
1778
1779 options.event_mask = RuntimeEventMask::from_bits_retain(1 << 63);
1782 let error = RuntimeHandle::with_options(&options).unwrap_err();
1783 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1784 }
1785
1786 #[test]
1787 fn a_runtime_mask_round_trips_and_rejects_undefined_bits() {
1789 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1790
1791 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1793
1794 runtime.set_event_mask(RuntimeEventMask::ALL).unwrap();
1795 assert_eq!(runtime.event_mask().unwrap(), RuntimeEventMask::ALL);
1796
1797 let mut mask = runtime.event_mask().unwrap();
1799 mask.remove(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED);
1800 runtime.set_event_mask(mask).unwrap();
1801 let read_back = runtime.event_mask().unwrap();
1802 assert!(!read_back.contains(RuntimeEventMask::OFFLINE_REGION_STATUS_CHANGED));
1803 assert!(read_back.contains(RuntimeEventMask::OFFLINE_OPERATION_COMPLETED));
1804 assert!(read_back.contains(RuntimeEventMask::MAP_STYLE_LOADED));
1805
1806 let undefined = RuntimeEventMask::from_bits_retain(1 << 63);
1807 let error = runtime.set_event_mask(undefined).unwrap_err();
1808 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1809 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1810 assert_eq!(runtime.event_mask().unwrap(), read_back);
1811
1812 runtime.close().unwrap();
1813 }
1814
1815 #[test]
1816 fn runtime_wrong_thread_status_maps_error_and_copies_diagnostic() {
1818 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1819 let runtime_handle = runtime.inner.native().unwrap();
1820
1821 let error = std::thread::spawn(move || {
1822 maplibre_core::check(unsafe { sys::mln_runtime_pump(runtime_handle, 0, -1) })
1825 .unwrap_err()
1826 })
1827 .join()
1828 .unwrap();
1829
1830 assert_eq!(error.kind(), ErrorKind::WrongThread);
1831 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_WRONG_THREAD));
1832 assert!(!error.diagnostic().is_empty());
1833 runtime.close().unwrap();
1834 }
1835
1836 #[test]
1837 fn resource_provider_installs_replaces_clears_and_releases_state() {
1839 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1840 let first = Arc::new(());
1841 let first_callback = Arc::clone(&first);
1842
1843 runtime
1844 .set_resource_provider(move |_, _| {
1845 let _ = &first_callback;
1846 crate::ResourceProviderDecision::PassThrough
1847 })
1848 .unwrap();
1849 assert_eq!(Arc::strong_count(&first), 2);
1850
1851 let second = Arc::new(());
1852 let second_callback = Arc::clone(&second);
1853 runtime
1854 .set_resource_provider(move |_, _| {
1855 let _ = &second_callback;
1856 crate::ResourceProviderDecision::PassThrough
1857 })
1858 .unwrap();
1859 assert_eq!(Arc::strong_count(&first), 1);
1860 assert_eq!(Arc::strong_count(&second), 2);
1861
1862 runtime.clear_resource_provider().unwrap();
1863 assert_eq!(Arc::strong_count(&second), 1);
1864
1865 let third = Arc::new(());
1866 let third_callback = Arc::clone(&third);
1867 runtime
1868 .set_resource_provider(move |_, _| {
1869 let _ = &third_callback;
1870 crate::ResourceProviderDecision::PassThrough
1871 })
1872 .unwrap();
1873 assert_eq!(Arc::strong_count(&third), 2);
1874
1875 runtime.close().unwrap();
1876 assert_eq!(Arc::strong_count(&third), 1);
1877 }
1878
1879 #[test]
1880 fn resource_provider_replacement_rolls_back_when_native_install_fails() {
1882 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1883 let first = Arc::new(());
1884 let first_callback = Arc::clone(&first);
1885 runtime
1886 .set_resource_provider(move |_, _| {
1887 let _ = &first_callback;
1888 crate::ResourceProviderDecision::PassThrough
1889 })
1890 .unwrap();
1891
1892 let second = Arc::new(());
1893 let second_callback = Arc::clone(&second);
1894 let error = runtime
1895 .inner
1896 .set_resource_provider_with_rejected_descriptor_for_testing(move |_, _| {
1897 let _ = &second_callback;
1898 crate::ResourceProviderDecision::PassThrough
1899 })
1900 .unwrap_err();
1901
1902 assert_eq!(error.kind(), ErrorKind::InvalidArgument);
1903 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_INVALID_ARGUMENT));
1904 assert_eq!(Arc::strong_count(&first), 2);
1905 assert_eq!(Arc::strong_count(&second), 1);
1906
1907 runtime.close().unwrap();
1908 assert_eq!(Arc::strong_count(&first), 1);
1909 }
1910
1911 fn load_probe_style(runtime: &mut RuntimeHandle, map: &MapHandle, style_url: &str) {
1915 map.set_style_url(style_url).unwrap();
1916 let event = wait_for_map_loading_failure(runtime);
1917 assert!(
1918 event
1919 .message
1920 .as_deref()
1921 .is_some_and(|message| message.contains("\"jar\""))
1922 );
1923 }
1924
1925 #[test]
1926 fn resource_provider_is_consulted_until_replaced_and_cleared_while_a_map_is_live() {
1928 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1929 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
1930
1931 let first_calls = Arc::new(AtomicUsize::new(0));
1932 let first_callback_calls = Arc::clone(&first_calls);
1933 runtime
1934 .set_resource_provider(move |_, _| {
1935 first_callback_calls.fetch_add(1, Ordering::SeqCst);
1936 ResourceProviderDecision::PassThrough
1937 })
1938 .unwrap();
1939 load_probe_style(&mut runtime, &map, "jar:file:/packaged/first.json");
1940 assert!(first_calls.load(Ordering::SeqCst) > 0);
1941
1942 let second_calls = Arc::new(AtomicUsize::new(0));
1943 let second_callback_calls = Arc::clone(&second_calls);
1944 runtime
1945 .set_resource_provider(move |_, _| {
1946 second_callback_calls.fetch_add(1, Ordering::SeqCst);
1947 ResourceProviderDecision::PassThrough
1948 })
1949 .unwrap();
1950 let first_calls_after_replace = first_calls.load(Ordering::SeqCst);
1951 load_probe_style(&mut runtime, &map, "jar:file:/packaged/second.json");
1952 assert!(second_calls.load(Ordering::SeqCst) > 0);
1953 assert_eq!(
1954 first_calls.load(Ordering::SeqCst),
1955 first_calls_after_replace
1956 );
1957
1958 runtime.clear_resource_provider().unwrap();
1959 let second_calls_after_clear = second_calls.load(Ordering::SeqCst);
1960 load_probe_style(&mut runtime, &map, "jar:file:/packaged/third.json");
1961 assert_eq!(
1962 first_calls.load(Ordering::SeqCst),
1963 first_calls_after_replace
1964 );
1965 assert_eq!(
1966 second_calls.load(Ordering::SeqCst),
1967 second_calls_after_clear
1968 );
1969
1970 runtime.clear_resource_provider().unwrap();
1972
1973 map.close().unwrap();
1974 runtime.close().unwrap();
1975 }
1976
1977 #[test]
1978 fn resource_provider_completes_style_request_inline_through_c_abi() {
1980 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
1981 let calls = Arc::new(AtomicUsize::new(0));
1982 let callback_calls = Arc::clone(&calls);
1983 runtime
1984 .set_resource_provider(move |request, handle| {
1985 if request.requested_url != "custom://style.json" {
1986 return ResourceProviderDecision::PassThrough;
1987 }
1988 callback_calls.fetch_add(1, Ordering::SeqCst);
1989 assert_eq!(request.kind, ResourceKind::Style);
1990 handle
1991 .complete(ResourceResponse::ok(
1992 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
1993 ))
1994 .unwrap();
1995 ResourceProviderDecision::PassThrough
1996 })
1997 .unwrap();
1998
1999 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2000 map.set_style_url("custom://style.json").unwrap();
2001
2002 assert!(wait_for_runtime_event(
2003 &mut runtime,
2004 RuntimeEventType::MapStyleLoaded
2005 ));
2006 assert_eq!(calls.load(Ordering::SeqCst), 1);
2007 map.close().unwrap();
2008 runtime.close().unwrap();
2009 }
2010
2011 #[test]
2012 fn resource_provider_sees_scheme_alias_and_its_resolved_url() {
2014 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2015 let resolved = Arc::new(Mutex::new(None));
2016 let callback_resolved = Arc::clone(&resolved);
2017 runtime
2018 .set_resource_provider(move |request, handle| {
2019 if request.requested_url != "maplibre://maps/style" {
2020 return ResourceProviderDecision::PassThrough;
2021 }
2022 *callback_resolved.lock().unwrap() = Some(request.resolved_url.clone());
2023 handle
2024 .complete(ResourceResponse::ok(
2025 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2026 ))
2027 .unwrap();
2028 ResourceProviderDecision::Handle
2029 })
2030 .unwrap();
2031
2032 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2033 map.set_style_url("maplibre://maps/style").unwrap();
2034
2035 assert!(wait_for_runtime_event(
2036 &mut runtime,
2037 RuntimeEventType::MapStyleLoaded
2038 ));
2039 assert_eq!(
2040 resolved.lock().unwrap().as_deref(),
2041 Some("https://demotiles.maplibre.org/style.json")
2042 );
2043 map.close().unwrap();
2044 runtime.close().unwrap();
2045 }
2046
2047 #[test]
2048 fn resource_provider_completes_style_request_from_another_thread() {
2050 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2051 let (sender, receiver) = std::sync::mpsc::channel();
2052 runtime
2053 .set_resource_provider(move |request, handle| {
2054 if request.requested_url == "custom://async-style.json" {
2055 sender.send(handle).unwrap();
2056 ResourceProviderDecision::Handle
2057 } else {
2058 ResourceProviderDecision::PassThrough
2059 }
2060 })
2061 .unwrap();
2062
2063 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2064 map.set_style_url("custom://async-style.json").unwrap();
2065 let handle = receiver
2066 .recv_timeout(Duration::from_secs(5))
2067 .expect("provider should send handled request");
2068 assert!(!handle.is_cancelled().unwrap());
2069 std::thread::spawn(move || {
2070 handle
2071 .complete(ResourceResponse::ok(
2072 PROVIDER_STYLE_JSON.as_bytes().to_vec(),
2073 ))
2074 .unwrap();
2075 })
2076 .join()
2077 .unwrap();
2078
2079 assert!(wait_for_runtime_event(
2080 &mut runtime,
2081 RuntimeEventType::MapStyleLoaded
2082 ));
2083 map.close().unwrap();
2084 runtime.close().unwrap();
2085 }
2086
2087 #[test]
2088 fn resource_provider_error_response_becomes_copied_loading_failure_event() {
2090 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2091 runtime
2092 .set_resource_provider(move |request, handle| {
2093 if request.requested_url == "custom://broken-style.json" {
2094 handle
2095 .complete(ResourceResponse::error(
2096 ResourceErrorReason::Other,
2097 "provider failed",
2098 ))
2099 .unwrap();
2100 ResourceProviderDecision::Handle
2101 } else {
2102 ResourceProviderDecision::PassThrough
2103 }
2104 })
2105 .unwrap();
2106
2107 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2108 let map_id = map.id();
2109 map.set_style_url("custom://broken-style.json").unwrap();
2110
2111 let event = wait_for_map_loading_failure(&mut runtime);
2112 let copied_message = event.message.clone();
2113 let _ = runtime.drain_events(0).unwrap();
2115
2116 assert_eq!(event.source, RuntimeEventSource::Map(map_id));
2117 assert_eq!(event.event_type, RuntimeEventType::MapLoadingFailed);
2118 assert_eq!(event.message, copied_message);
2119 assert!(
2120 event
2121 .message
2122 .as_deref()
2123 .is_some_and(|message| message.contains("provider failed"))
2124 );
2125
2126 map.close().unwrap();
2127 runtime.close().unwrap();
2128 }
2129
2130 #[test]
2131 fn resource_transform_installs_replaces_clears_and_releases_state() {
2133 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2134 let first = Arc::new(());
2135 let first_callback = Arc::clone(&first);
2136
2137 runtime
2138 .set_resource_transform(move |request| {
2139 let _ = &first_callback;
2140 assert!(matches!(
2141 request.kind,
2142 ResourceKind::Style | ResourceKind::UnknownRaw(_)
2143 ));
2144 None
2145 })
2146 .unwrap();
2147 assert_eq!(Arc::strong_count(&first), 2);
2148
2149 let second = Arc::new(());
2150 let second_callback = Arc::clone(&second);
2151 runtime
2152 .set_resource_transform(move |_| {
2153 let _ = &second_callback;
2154 Some("https://example.test/replacement".to_owned())
2155 })
2156 .unwrap();
2157 assert_eq!(Arc::strong_count(&first), 1);
2158 assert_eq!(Arc::strong_count(&second), 2);
2159
2160 runtime.clear_resource_transform().unwrap();
2161 assert_eq!(Arc::strong_count(&second), 1);
2162 runtime.close().unwrap();
2163 }
2164
2165 #[cfg(target_os = "emscripten")]
2171 #[test]
2172 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2174 let origin = std::env::var("MLN_FFI_TEST_FIXTURE_ORIGIN").expect(
2175 "MLN_FFI_TEST_FIXTURE_ORIGIN is unset; run the suite through \
2176 `mise run //bindings/rust:test emscripten-wasm32-webgl`",
2177 );
2178 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2179 let transform_url = format!("{origin}/__fixture/rewritten-style.json");
2180 runtime
2183 .set_resource_transform(move |request| {
2184 (request.url.ends_with("/original-style.json")
2185 || request.url.ends_with("/original-after-clear.json"))
2186 .then(|| transform_url.clone())
2187 })
2188 .unwrap();
2189
2190 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2191 map.set_style_url(&format!("{origin}/__fixture/original-style.json"))
2192 .unwrap();
2193 assert!(wait_for_runtime_event(
2194 &mut runtime,
2195 RuntimeEventType::MapStyleLoaded
2196 ));
2197 assert!(
2200 map.style_layer_ids()
2201 .unwrap()
2202 .iter()
2203 .any(|id| id == "rewritten")
2204 );
2205
2206 runtime.clear_resource_transform().unwrap();
2207 map.set_style_url(&format!("{origin}/__fixture/original-after-clear.json"))
2208 .unwrap();
2209 assert!(wait_for_runtime_event(
2210 &mut runtime,
2211 RuntimeEventType::MapStyleLoaded
2212 ));
2213 assert!(
2214 map.style_layer_ids()
2215 .unwrap()
2216 .iter()
2217 .any(|id| id == "original-after-clear")
2218 );
2219
2220 map.close().unwrap();
2221 runtime.close().unwrap();
2222 }
2223
2224 #[cfg(not(target_os = "emscripten"))]
2225 #[test]
2226 fn resource_transform_rewrites_style_url_and_clear_restores_original_url() {
2228 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2229 let (base_url, requests, server) = spawn_style_server(2);
2230 let transform_base_url = base_url.clone();
2231
2232 runtime
2236 .set_resource_transform(move |request| {
2237 if request.url.ends_with("/original-style.json")
2238 || request.url.ends_with("/original-after-clear.json")
2239 {
2240 Some(format!("{transform_base_url}/rewritten-style.json"))
2241 } else {
2242 None
2243 }
2244 })
2245 .unwrap();
2246
2247 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2248 map.set_style_url(&format!("{base_url}/original-style.json"))
2249 .unwrap();
2250 assert!(wait_for_runtime_event(
2251 &mut runtime,
2252 RuntimeEventType::MapStyleLoaded
2253 ));
2254 assert_eq!(
2255 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2256 "/rewritten-style.json"
2257 );
2258
2259 runtime.clear_resource_transform().unwrap();
2260 map.set_style_url(&format!("{base_url}/original-after-clear.json"))
2261 .unwrap();
2262 assert!(wait_for_runtime_event(
2263 &mut runtime,
2264 RuntimeEventType::MapStyleLoaded
2265 ));
2266 assert_eq!(
2267 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2268 "/original-after-clear.json"
2269 );
2270
2271 map.close().unwrap();
2272 runtime.close().unwrap();
2273 server.join().unwrap();
2274 }
2275
2276 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2277 #[test]
2278 fn http_header_transform_reaches_requests_and_clear_stops_it() {
2280 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2281 let (base_url, requests, server) = spawn_recording_style_server(2);
2282 runtime
2283 .set_http_header_transform(|request| {
2284 assert_eq!(request.kind, ResourceKind::Style);
2285 vec![crate::HttpHeader::new("X-Map-Token", "secret")]
2286 })
2287 .unwrap();
2288
2289 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2290 map.set_style_url(&format!("{base_url}/with-header.json"))
2291 .unwrap();
2292 assert!(wait_for_runtime_event(
2293 &mut runtime,
2294 RuntimeEventType::MapStyleLoaded
2295 ));
2296 let first = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2297 assert!(
2298 first
2299 .lines()
2300 .any(|line| line.eq_ignore_ascii_case("X-Map-Token: secret"))
2301 );
2302
2303 runtime.clear_http_header_transform().unwrap();
2304 map.set_style_url(&format!("{base_url}/after-clear.json"))
2305 .unwrap();
2306 assert!(wait_for_runtime_event(
2307 &mut runtime,
2308 RuntimeEventType::MapStyleLoaded
2309 ));
2310 let second = requests.recv_timeout(Duration::from_secs(5)).unwrap();
2311 assert!(
2312 !second
2313 .lines()
2314 .any(|line| line.to_ascii_lowercase().starts_with("x-map-token:"))
2315 );
2316
2317 map.close().unwrap();
2318 runtime.close().unwrap();
2319 server.join().unwrap();
2320 }
2321
2322 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2323 #[test]
2324 fn http_header_transform_skips_non_http_urls() {
2326 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2327 runtime.set_resource_transform(|_| None).unwrap();
2328 let calls = Arc::new(AtomicUsize::new(0));
2329 let callback_calls = Arc::clone(&calls);
2330 runtime
2331 .set_http_header_transform(move |_| {
2332 callback_calls.fetch_add(1, Ordering::SeqCst);
2333 Vec::new()
2334 })
2335 .unwrap();
2336 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2337
2338 map.set_style_url("jar:file:/packaged/style.json").unwrap();
2339 let _ = wait_for_map_loading_failure(&mut runtime);
2340 assert_eq!(calls.load(Ordering::SeqCst), 0);
2341
2342 map.close().unwrap();
2343 runtime.close().unwrap();
2344 }
2345
2346 #[cfg(not(any(target_env = "ohos", target_os = "emscripten")))]
2347 #[test]
2348 fn http_header_transform_preserves_same_origin_and_strips_cross_origin_redirects() {
2350 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2351 let (origin_url, requests, servers) = spawn_redirect_style_servers();
2352 runtime
2353 .set_http_header_transform(|_| vec![crate::HttpHeader::new("X-Map-Token", "secret")])
2354 .unwrap();
2355 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2356
2357 map.set_style_url(&format!("{origin_url}/same-start.json"))
2358 .unwrap();
2359 assert!(wait_for_runtime_event(
2360 &mut runtime,
2361 RuntimeEventType::MapStyleLoaded
2362 ));
2363 assert_eq!(
2364 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2365 ("/same-start.json".to_owned(), true)
2366 );
2367 assert_eq!(
2368 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2369 ("/same-final.json".to_owned(), true)
2370 );
2371
2372 map.set_style_url(&format!("{origin_url}/cross-start.json"))
2373 .unwrap();
2374 assert!(wait_for_runtime_event(
2375 &mut runtime,
2376 RuntimeEventType::MapStyleLoaded
2377 ));
2378 assert_eq!(
2379 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2380 ("/cross-start.json".to_owned(), true)
2381 );
2382 assert_eq!(
2383 requests.recv_timeout(Duration::from_secs(5)).unwrap(),
2384 ("/cross-final.json".to_owned(), false)
2385 );
2386
2387 map.close().unwrap();
2388 runtime.close().unwrap();
2389 for server in servers {
2390 server.join().unwrap();
2391 }
2392 }
2393
2394 #[cfg(any(target_env = "ohos", target_os = "emscripten"))]
2399 #[test]
2400 fn http_header_transform_reports_unsupported() {
2401 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2402 let error = runtime
2403 .set_http_header_transform(|_| Vec::new())
2404 .unwrap_err();
2405 assert_eq!(error.kind(), ErrorKind::Unsupported);
2406 runtime.close().unwrap();
2407 }
2408
2409 #[test]
2410 fn resource_transform_replacement_after_map_creation_releases_previous_state() {
2412 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2413 let first = Arc::new(());
2414 let first_callback = Arc::clone(&first);
2415 runtime
2416 .set_resource_transform(move |_| {
2417 let _ = &first_callback;
2418 None
2419 })
2420 .unwrap();
2421 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2422
2423 let second = Arc::new(());
2424 let second_callback = Arc::clone(&second);
2425 runtime
2426 .set_resource_transform(move |_| {
2427 let _ = &second_callback;
2428 None
2429 })
2430 .unwrap();
2431
2432 assert_eq!(Arc::strong_count(&first), 1);
2433 assert_eq!(Arc::strong_count(&second), 2);
2434
2435 map.close().unwrap();
2436 runtime.close().unwrap();
2437 assert_eq!(Arc::strong_count(&second), 1);
2438 }
2439
2440 #[test]
2441 fn runtime_teardown_releases_resource_transform_state() {
2443 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2444 let token = Arc::new(());
2445 let callback_token = Arc::clone(&token);
2446 runtime
2447 .set_resource_transform(move |_| {
2448 let _ = &callback_token;
2449 None
2450 })
2451 .unwrap();
2452 assert_eq!(Arc::strong_count(&token), 2);
2453
2454 runtime.close().unwrap();
2455
2456 assert_eq!(Arc::strong_count(&token), 1);
2457 }
2458
2459 #[test]
2460 fn resource_transform_installs_after_map_creation() {
2463 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2464 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2465
2466 runtime.set_resource_transform(|_| None).unwrap();
2467
2468 map.close().unwrap();
2469 runtime.close().unwrap();
2470 }
2471
2472 #[test]
2473 fn resource_transform_clears_after_map_was_closed_and_releases_state() {
2475 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2476 let token = Arc::new(());
2477 let callback_token = Arc::clone(&token);
2478 runtime
2479 .set_resource_transform(move |_| {
2480 let _ = &callback_token;
2481 None
2482 })
2483 .unwrap();
2484 assert_eq!(Arc::strong_count(&token), 2);
2485
2486 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2487 map.close().unwrap();
2488
2489 runtime.clear_resource_transform().unwrap();
2490
2491 assert_eq!(Arc::strong_count(&token), 1);
2492
2493 runtime.close().unwrap();
2494 }
2495
2496 #[test]
2497 fn a_drain_reports_map_events_in_queue_order_and_copies_outlive_the_batch() {
2499 let mut runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2500 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2501 let map_id = map.id();
2502
2503 let error = map.set_style_json(b"{").unwrap_err();
2504 assert_eq!(error.kind(), ErrorKind::NativeError);
2505 assert_eq!(error.raw_status(), Some(sys::MLN_STATUS_NATIVE_ERROR));
2506
2507 let batch = runtime.drain_events(0).unwrap();
2508 let types = batch
2509 .iter()
2510 .map(|event| event.event_type())
2511 .collect::<Vec<_>>();
2512 assert!(
2513 types.len() > 1,
2514 "a failed style load should queue more than one event, got {types:?}"
2515 );
2516 let loading_failed = batch
2517 .iter()
2518 .find(|event| event.event_type() == RuntimeEventType::MapLoadingFailed)
2519 .expect("a malformed style should queue a loading-failed event");
2520 assert_eq!(loading_failed.source(), RuntimeEventSource::Map(map_id));
2521 assert!(
2522 loading_failed
2523 .message()
2524 .unwrap()
2525 .is_some_and(|message| !message.is_empty())
2526 );
2527 let owned = loading_failed.to_owned().unwrap();
2528
2529 assert!(runtime.drain_events(0).unwrap().is_empty());
2532 assert_eq!(owned.source, RuntimeEventSource::Map(map_id));
2533 assert_eq!(owned.event_type, RuntimeEventType::MapLoadingFailed);
2534 assert!(
2535 owned
2536 .message
2537 .as_deref()
2538 .is_some_and(|message| !message.is_empty())
2539 );
2540
2541 map.close().unwrap();
2542 runtime.close().unwrap();
2543 }
2544
2545 #[test]
2546 fn runtime_close_with_live_map_is_rust_invalid_state_and_retryable() {
2548 let runtime = RuntimeHandle::with_options(&crate::RuntimeOptions::default()).unwrap();
2549 let map = MapHandle::with_options(&runtime, &MapOptions::default()).unwrap();
2550
2551 let error = runtime.close().unwrap_err();
2552 assert_eq!(error.kind(), ErrorKind::InvalidState);
2553 assert_eq!(error.raw_status(), None);
2554 let runtime = error.into_handle();
2555
2556 runtime.pump(Some(Duration::ZERO), None).unwrap();
2557 map.close().unwrap();
2558 runtime.close().unwrap();
2559 }
2560}