Client Caching Strategies
This document covers caching approaches used in the mobile app for assets, data, and AI analysis results.
Key Files
crates/bf_mobile/src/ota.rs- OTA asset cachingcrates/bf_resident/src/handlers/floor_plan_layouts.rs- AI analysis caching- Native Lynx plugin - Credential and session caching
Overview
Multiple caching layers optimize performance and offline capability:
┌─────────────────────────────────────────────────────────────────────┐
│ Caching Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Mobile App │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ OTA Assets │ │ Credentials │ │ Session │ │ │
│ │ │ A/B dirs │ │ Keychain │ │ Cache │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │ │ │ │
│ │ │ Native Storage │ │ │
│ │ │ │ │ │ │
│ └─────────┼────────────────┼────────────────┼───────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Backend Services │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ R2 Storage │ │ KV Cache │ │ D1 │ │ │
│ │ │ (assets) │ │ (AI cache) │ │ (metadata) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
OTA Asset Caching
Compressed Storage
Assets stored compressed on device:
// crates/bf_mobile/src/ota.rs
pub struct RuntimeAssets {
/// Brotli-compressed assets
assets: Vec<(String, Vec<u8>)>,
/// CSP hashes
global_hashes: Vec<RuntimeCspHash>,
html_hashes: Vec<(String, Vec<RuntimeCspHash>)>,
}
On-Demand Decompression
Assets decompressed when requested:
impl<R: Runtime> Assets<R> for OtaAssets<R> {
fn get(&self, key: &AssetKey) -> Option<Cow<'_, [u8]>> {
if let Some(displaying) = self.displaying.as_ref() {
displaying
.assets
.get(key.as_ref())
.map(|compressed_data| {
let mut buf = Vec::with_capacity(compressed_data.len());
brotli::BrotliDecompress(&mut compressed_data, &mut buf)
.map(|()| buf)
})
.and_then(Result::ok)
.map(Cow::Owned)
} else {
// Fall back to embedded assets
self.default_assets.get(key)
}
}
}
Embedded Fallback
Embedded assets serve as fallback when no OTA version exists:
impl GlobalOtaAssets {
pub fn new(default_assets: Box<dyn Assets<Wry>>) -> Self {
// Wrap default assets with OTA layer
// OTA versions override when available
}
}
Credential Caching
Native Secure Storage
iOS Keychain and Android Keystore via Lynx plugin:
// Exposed as Tauri commands
#[tauri::command]
fn store_credential(key: String, value: String) -> Result<(), String> {
lynx::store_credential(&key, &value)
}
#[tauri::command]
fn get_credential(key: String) -> Result<Option<String>, String> {
lynx::get_credential(&key)
}
Clerk Session Caching
Clerk SDK handles session persistence internally:
#[tauri::command]
pub async fn clerk_restore_session() -> Result<ClerkUser, String> {
// Restores session from native storage
lynx::clerk_restore_session().await
}
Backend KV Caching
AI Floor Plan Analysis
Expensive AI analysis cached in Cloudflare KV:
// crates/bf_resident/src/handlers/floor_plan_layouts.rs
const AI_CACHE_TTL_SECONDS: u64 = 30 * 24 * 60 * 60; // 30 days
pub async fn get_floor_plan_analysis(
env: &Env,
floor_plan_hash: &str,
) -> Result<Option<FloorPlanAnalysis>> {
let kv = env.kv("AI_CACHE")?;
// Check cache first
if let Some(cached) = kv.get(floor_plan_hash).json().await? {
return Ok(Some(cached));
}
// Run AI analysis (expensive)
let analysis = run_ai_analysis(floor_plan_url).await?;
// Cache result
kv.put(floor_plan_hash, &analysis)?
.expiration_ttl(AI_CACHE_TTL_SECONDS)
.execute()
.await?;
Ok(Some(analysis))
}
Cache Key Strategy
Content-addressed caching for floor plans:
// Hash the floor plan image URL for cache key
let cache_key = format!("floor_plan:{}", sha256_hash(floor_plan_url));
Session Token Caching
OTA Token Format
Token includes version info for backend version checking:
// crates/bf_mobile/src/ota.rs
pub async fn get_token() -> String {
let current = match &client.displaying_version {
AssetVersion::A(x) | AssetVersion::B(x) => *x,
_ => 0,
};
let app = handle.config().version.unwrap_or("0.0.0");
let access_token = lynx::clerk_get_token().await?;
// Format: "ota_version|app_version|clerk_token"
format!("{current}|{app}|{access_token}")
}
Network-Aware Caching
Connectivity Check
OTA checks only when connected:
pub async fn check_latest_version_fn() {
// 0 = connected, 1 = cellular only, 2 = no connection
if lynx::get_connectivity_error() == 0 {
let token = get_token().await;
if !token.is_empty() {
lynx::check_latest_version(token, base_url);
}
}
}
Platform-Specific Behavior
// iOS: Regular background checks
#[cfg(target_os = "ios")]
{
let mut interval = tokio::time::interval(Duration::from_secs(60 * 5));
loop {
interval.tick().await;
check_latest_version_fn().await;
}
}
// Android: Single check to conserve battery
#[cfg(target_os = "android")]
{
tokio::time::sleep(Duration::from_secs(1)).await;
check_latest_version_fn().await;
}
Cache Invalidation
App Version Change
OTA cache cleared on native app update:
fn add_app_handle(&mut self, handle: AppHandle) {
let current_version = handle.package_info().version.to_string();
if current_version != previous_version {
// Clear OTA directories
std::fs::remove_dir_all(&version_a_dir)?;
std::fs::remove_dir_all(&version_b_dir)?;
// Store new version
std::fs::write(&version_check_path, ¤t_version)?;
}
}
Manual Reset
User can force reset to embedded assets:
#[tauri::command]
pub fn reset_remote() {
if let Some(singleton) = OTA_ASSETS.get() {
singleton.reset() // Clears A/ and B/ directories
}
}
Environment Switch
Changing environments requires fresh start:
#[tauri::command]
fn set_environment(app_handle: AppHandle, env_name: String) -> Result<(), String> {
// Write new environment selection
// App restart required for Clerk SDK reinitialization
}
#[tauri::command]
fn exit_app(app_handle: AppHandle) {
app_handle.exit(0); // Force restart
}
Data Persistence Locations
iOS
| Data Type | Location | Notes |
|---|---|---|
| OTA Assets | Documents/A/, Documents/B/ | Backed up by iCloud |
| Credentials | Keychain | Secure enclave |
| Environment | Documents/env.json | User preference |
| Version Check | Documents/version.check | App version marker |
Android
| Data Type | Location | Notes |
|---|---|---|
| OTA Assets | files/A/, files/B/ | Internal storage |
| Credentials | Keystore | Hardware-backed |
| Environment | files/env.json | User preference |
| Version Check | files/version.check | App version marker |
Cache Size Considerations
OTA Bundle Size
Logged on export for monitoring:
let zip = ota::make_data();
lynx::nslog(format!("OTA bundle size: {} bytes", zip.len()));
Compression Ratios
| Stage | Compression | Typical Ratio |
|---|---|---|
| Build time | Brotli | ~70-80% reduction |
| Transfer | Zstd level 22 | ~50-60% further |
| Storage | Brotli (kept) | Saves device space |
| Serve | Decompress | On-demand |
See Also
- OTA Updates - Update mechanism
- Mobile Architecture - App structure
- Notification System - Push caching