131 lines
4.9 KiB
Rust
131 lines
4.9 KiB
Rust
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::Arc;
|
|
|
|
use parking_lot::RwLock;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use crate::auth::TokenCache;
|
|
use crate::data::{
|
|
POICategoryGroup, POIData, PlaceData, PostcodeData, PropertyData, TravelTimeStore,
|
|
};
|
|
use crate::routes::FeaturesResponse;
|
|
use crate::utils::GridIndex;
|
|
|
|
pub struct AppState {
|
|
// --- Rebuilt on reload ---
|
|
pub data: PropertyData,
|
|
pub grid: GridIndex,
|
|
/// h3_cells[row_idx] = precomputed H3 cell ID at max resolution (12).
|
|
/// Parent cells for lower resolutions derived via CellIndex::parent().
|
|
pub h3_cells: Vec<u64>,
|
|
/// O(1) lookup: feature name → index in feature_names/feature_data
|
|
pub feature_name_to_index: FxHashMap<String, usize>,
|
|
/// Precomputed JSON key names: "min_{feature_name}" for each feature
|
|
pub min_keys: Vec<String>,
|
|
/// Precomputed JSON key names: "max_{feature_name}" for each feature
|
|
pub max_keys: Vec<String>,
|
|
/// Precomputed JSON key names: "avg_{feature_name}" for each feature
|
|
pub avg_keys: Vec<String>,
|
|
/// Precomputed features response for /api/features endpoint
|
|
pub features_response: FeaturesResponse,
|
|
/// Complete system prompt for AI filters (features + examples + instructions)
|
|
pub ai_filters_system_prompt: String,
|
|
|
|
// --- Shared across reloads (Arc for cheap cloning) ---
|
|
pub poi_data: Arc<POIData>,
|
|
pub poi_grid: Arc<GridIndex>,
|
|
pub place_data: Arc<PlaceData>,
|
|
/// Postcode boundary data for high-zoom rendering
|
|
pub postcode_data: Arc<PostcodeData>,
|
|
/// Precomputed POI category groups (sorted)
|
|
pub poi_category_groups: Arc<Vec<POICategoryGroup>>,
|
|
/// Precomputed travel time data store
|
|
pub travel_time_store: Arc<TravelTimeStore>,
|
|
/// Token validation cache (60s TTL)
|
|
pub token_cache: Arc<TokenCache>,
|
|
|
|
// --- Config (cheap to clone) ---
|
|
/// URL of the screenshot service (e.g. http://screenshot:8002)
|
|
pub screenshot_url: String,
|
|
/// Public-facing URL for absolute og:image URLs (e.g. https://perfectpostcodes.dev)
|
|
pub public_url: String,
|
|
/// True when --dist is not provided (no static serving, relaxed auth checks)
|
|
pub is_dev: bool,
|
|
/// Contents of index.html read at startup, used for crawler OG injection (None when --dist omitted)
|
|
pub index_html: Option<String>,
|
|
/// Shared HTTP client for proxying to the screenshot service and PocketBase
|
|
pub http_client: reqwest::Client,
|
|
/// PocketBase server URL for authentication (e.g. http://localhost:8090)
|
|
pub pocketbase_url: String,
|
|
/// PocketBase superuser email (needed for admin-only operations like subscription updates)
|
|
pub pocketbase_admin_email: String,
|
|
/// PocketBase superuser password
|
|
pub pocketbase_admin_password: String,
|
|
/// Gemini API key for AI filters
|
|
pub gemini_api_key: String,
|
|
/// Gemini model name (e.g. gemini-2.0-flash)
|
|
pub gemini_model: String,
|
|
/// Google Maps API key for Street View metadata lookups
|
|
pub google_maps_api_key: String,
|
|
/// Stripe secret key for creating checkout sessions
|
|
pub stripe_secret_key: String,
|
|
/// Stripe webhook signing secret
|
|
pub stripe_webhook_secret: String,
|
|
/// Stripe Coupon ID for referral discounts
|
|
pub stripe_referral_coupon_id: String,
|
|
}
|
|
|
|
/// Wraps AppState with atomic swap capability for hot-reloading.
|
|
/// Route handlers call `load_state()` to get the current snapshot.
|
|
/// The reload endpoint builds a new AppState and swaps it in atomically.
|
|
pub struct SharedState {
|
|
current: RwLock<Arc<AppState>>,
|
|
reloading: AtomicBool,
|
|
/// Paths needed for data reload
|
|
pub properties_path: PathBuf,
|
|
pub postcode_features_path: PathBuf,
|
|
pub listings_buy_path: PathBuf,
|
|
pub listings_rent_path: PathBuf,
|
|
}
|
|
|
|
impl SharedState {
|
|
pub fn new(
|
|
state: AppState,
|
|
properties_path: PathBuf,
|
|
postcode_features_path: PathBuf,
|
|
listings_buy_path: PathBuf,
|
|
listings_rent_path: PathBuf,
|
|
) -> Self {
|
|
Self {
|
|
current: RwLock::new(Arc::new(state)),
|
|
reloading: AtomicBool::new(false),
|
|
properties_path,
|
|
postcode_features_path,
|
|
listings_buy_path,
|
|
listings_rent_path,
|
|
}
|
|
}
|
|
|
|
/// Get the current AppState snapshot. Cheap (Arc clone under a brief read lock).
|
|
pub fn load_state(&self) -> Arc<AppState> {
|
|
self.current.read().clone()
|
|
}
|
|
|
|
/// Atomically swap in a new AppState. Old state is dropped when all references are gone.
|
|
pub fn swap_state(&self, new_state: AppState) {
|
|
*self.current.write() = Arc::new(new_state);
|
|
}
|
|
|
|
/// Try to mark reload as in-progress. Returns false if already reloading.
|
|
pub fn try_start_reload(&self) -> bool {
|
|
self.reloading
|
|
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
|
|
.is_ok()
|
|
}
|
|
|
|
/// Mark reload as complete.
|
|
pub fn finish_reload(&self) {
|
|
self.reloading.store(false, Ordering::Release);
|
|
}
|
|
}
|