Skip to content

Commit 3835866

Browse files
committed
Optimize startup: license cache, async validation
- Cache verified LicenseInfo in a static Mutex to avoid re-parsing and re-verifying Ed25519 on every call (6x → 1x) - Make server license validation fire-and-forget so startup doesn't block on a 10s network timeout
1 parent 1d7e77c commit 3835866

3 files changed

Lines changed: 45 additions & 12 deletions

File tree

apps/desktop/src-tauri/src/licensing/app_status.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,7 @@ pub fn get_window_title(status: &AppStatus) -> String {
381381
/// Reset license data (for testing only).
382382
#[cfg(debug_assertions)]
383383
pub fn reset_license(app: &tauri::AppHandle) {
384+
crate::licensing::verification::clear_license_cache();
384385
if let Ok(store) = app.store("license.json") {
385386
store.delete("license_key");
386387
store.delete(STORE_KEY_CACHED_STATUS);

apps/desktop/src-tauri/src/licensing/verification.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ use crate::licensing::validation_client::{activate_short_code, is_short_code};
66
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
77
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
88
use serde::{Deserialize, Serialize};
9+
use std::sync::Mutex;
910
use tauri_plugin_store::StoreExt;
1011

12+
/// In-memory cache for verified license info. Avoids re-parsing and re-verifying
13+
/// the Ed25519 signature on every call to `get_license_info`.
14+
static LICENSE_CACHE: Mutex<Option<LicenseInfo>> = Mutex::new(None);
15+
1116
// Ed25519 public key (32 bytes, hex-encoded).
1217
// Generate this with: cd apps/license-server && pnpm run generate-keys
1318
// Then copy the public key here.
@@ -49,13 +54,20 @@ fn activate_license_internal(
4954
store.set(STORE_KEY_SHORT_CODE, serde_json::json!(code));
5055
}
5156

52-
Ok(LicenseInfo {
57+
let info = LicenseInfo {
5358
email: data.email,
5459
transaction_id: data.transaction_id,
5560
issued_at: data.issued_at,
5661
organization_name: data.organization_name,
5762
short_code: short_code.map(|s| s.to_string()),
58-
})
63+
};
64+
65+
// Update the in-memory cache with the newly activated license
66+
if let Ok(mut cache) = LICENSE_CACHE.lock() {
67+
*cache = Some(info.clone());
68+
}
69+
70+
Ok(info)
5971
}
6072

6173
/// Activate a license key (full key, not short code). Returns the license info if valid.
@@ -79,21 +91,43 @@ pub async fn activate_license_async(app: &tauri::AppHandle, input: &str) -> Resu
7991
activate_license_internal(app, &full_key, short_code)
8092
}
8193

82-
/// Get stored license info, if any.
94+
/// Get stored license info, if any. Returns a cached result after the first successful verification.
8395
pub fn get_license_info(app: &tauri::AppHandle) -> Option<LicenseInfo> {
96+
// Fast path: return cached info if available
97+
if let Ok(cache) = LICENSE_CACHE.lock()
98+
&& let Some(ref info) = *cache
99+
{
100+
return Some(info.clone());
101+
}
102+
103+
// Slow path: read from store and verify Ed25519 signature
84104
let store = app.store("license.json").ok()?;
85105
let license_key = store.get(STORE_KEY_LICENSE)?.as_str()?.to_string();
86106
let short_code = store
87107
.get(STORE_KEY_SHORT_CODE)
88108
.and_then(|v| v.as_str().map(|s| s.to_string()));
89109

90-
validate_license_key(&license_key).ok().map(|data| LicenseInfo {
110+
let info = validate_license_key(&license_key).ok().map(|data| LicenseInfo {
91111
email: data.email,
92112
transaction_id: data.transaction_id,
93113
issued_at: data.issued_at,
94114
organization_name: data.organization_name,
95115
short_code,
96-
})
116+
})?;
117+
118+
// Populate cache for subsequent calls
119+
if let Ok(mut cache) = LICENSE_CACHE.lock() {
120+
*cache = Some(info.clone());
121+
}
122+
123+
Some(info)
124+
}
125+
126+
/// Clear the in-memory license cache. Called when the license is reset.
127+
pub fn clear_license_cache() {
128+
if let Ok(mut cache) = LICENSE_CACHE.lock() {
129+
*cache = None;
130+
}
97131
}
98132

99133
/// Validate a license key and extract the data.

apps/desktop/src/routes/(main)/+page.svelte

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -444,15 +444,13 @@
444444
// Register known dialog types with backend (for MCP "available dialogs" resource)
445445
void registerKnownDialogs(SOFT_DIALOG_REGISTRY)
446446
447-
// Load license status first (non-blocking - don't prevent app load on failure)
447+
// Load license status from cache (fast, no network)
448448
try {
449-
let licenseStatus = await loadLicenseStatus()
449+
const licenseStatus = await loadLicenseStatus()
450450
451-
// Trigger background validation if needed
452-
const validatedStatus = await triggerValidationIfNeeded()
453-
if (validatedStatus) {
454-
licenseStatus = validatedStatus
455-
}
451+
// Fire-and-forget: validate with server in background if needed.
452+
// Updates the cache silently; next launch picks up the result.
453+
void triggerValidationIfNeeded()
456454
457455
// Check if we need to show expiration modal
458456
if (licenseStatus.type === 'expired' && licenseStatus.showModal) {

0 commit comments

Comments
 (0)