Skip to main content

longbridge_geo/
lib.rs

1//! Geo-detection helper for Longbridge OpenAPI.
2//!
3//! Determines whether the current access point is in China Mainland so that
4//! callers can choose between `*.longbridge.cn` and `*.longbridge.com`
5//! endpoints.
6
7use std::{
8    sync::{
9        OnceLock,
10        atomic::{AtomicBool, Ordering},
11    },
12    time::Duration,
13};
14
15// Process-wide cache so the probe is done at most once regardless of which
16// tokio worker thread calls `is_cn()`.
17static IS_CN_DONE: OnceLock<bool> = OnceLock::new();
18
19// Used to prevent multiple concurrent probes racing at startup.
20static IS_CN_PROBING: AtomicBool = AtomicBool::new(false);
21
22/// Do the best to guess whether the access point is in China Mainland or not.
23///
24/// Detection priority:
25/// 1. `LONGBRIDGE_REGION` environment variable (takes precedence).
26/// 2. `LONGPORT_REGION` environment variable (fallback alias).
27/// 3. Process-wide cached result from a previous probe.
28/// 4. Live HTTP probe to `https://geotest.lbkrs.com` — HTTP 200 → CN, anything
29///    else (error or non-200) → not CN.
30pub async fn is_cn() -> bool {
31    // 1 & 2: explicit region override
32    let user_region = std::env::var("LONGBRIDGE_REGION")
33        .ok()
34        .or_else(|| std::env::var("LONGPORT_REGION").ok());
35    if let Some(region) = user_region {
36        return region.eq_ignore_ascii_case("CN");
37    }
38
39    // 3: already probed
40    if let Some(&cached) = IS_CN_DONE.get() {
41        return cached;
42    }
43
44    // 4: live probe — only one task does the actual probe; others fall back
45    //    to `false` (global endpoint) which is safe and avoids a pile-up.
46    if IS_CN_PROBING
47        .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
48        .is_ok()
49    {
50        let result = reqwest::Client::new()
51            .get("https://geotest.lbkrs.com")
52            .timeout(Duration::from_secs(5))
53            .send()
54            .await
55            .is_ok_and(|resp| resp.status().is_success());
56
57        let _ = IS_CN_DONE.set(result);
58        result
59    } else {
60        // Another task is probing; use the cached value if it finished in the
61        // meantime, otherwise default to global endpoint.
62        IS_CN_DONE.get().copied().unwrap_or(false)
63    }
64}