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, /// O(1) lookup: feature name → index in feature_names/feature_data pub feature_name_to_index: FxHashMap, /// Precomputed JSON key names: "min_{feature_name}" for each feature pub min_keys: Vec, /// Precomputed JSON key names: "max_{feature_name}" for each feature pub max_keys: Vec, /// Precomputed JSON key names: "avg_{feature_name}" for each feature pub avg_keys: Vec, /// 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, pub poi_grid: Arc, pub place_data: Arc, /// Postcode boundary data for high-zoom rendering pub postcode_data: Arc, /// Precomputed POI category groups (sorted) pub poi_category_groups: Arc>, /// Precomputed travel time data store pub travel_time_store: Arc, /// Token validation cache (60s TTL) pub token_cache: Arc, // --- 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, /// 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>, 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 { 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); } }