use super::client::{PlmClient, PlmError};
#[cfg(not(target_arch = "wasm32"))]
use super::PlmFuture;
use crate::store::mirror_store::{BackendFuture, Entry, KeySpaceRouter, MirrorStore, StoreBackend, DIR_PREFIX, PREFIX};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
use web_time::{Duration, Instant};
thread_local! {
static MIRRORS_IN_FLIGHT: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
pub(crate) fn mirror_in_flight() -> bool {
MIRRORS_IN_FLIGHT.with(|n| n.get() > 0)
}
pub const FIRST_BACKOFF: Duration = Duration::from_secs(1);
pub const MAX_BACKOFF: Duration = Duration::from_secs(60);
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn open_session(
config: super::config::PlmConfig,
local: Rc<dyn StoreBackend>,
) -> PlmFuture<Rc<dyn StoreBackend>> {
Box::pin(async move {
let client = Rc::new(PlmClient::new(Rc::new(super::transport::EhttpTransport::new(config.url.clone()))));
let Some(token) = config.token.as_deref() else {
return Err(PlmError::SignIn(format!("no token for {}: paste one, or sign in with a password", config.url)).to_string());
};
let me = client.sign_in_with_token(token).await.map_err(|e| e.to_string())?;
Ok(compose(client, &config.url, &me.username, local))
})
}
#[cfg(target_arch = "wasm32")]
pub(crate) async fn open_web_session(local: Rc<dyn StoreBackend>) -> Result<Option<Rc<dyn StoreBackend>>, String> {
let Some(window) = web_sys::window() else { return Ok(None) };
let location = window.location();
let (Ok(path), Ok(origin)) = (location.pathname(), location.origin()) else { return Ok(None) };
if !served_by_plm(&path) {
return Ok(None);
}
let client = Rc::new(PlmClient::new(Rc::new(super::transport::EhttpTransport::new(origin.clone()))));
let me = client.sign_in_with_browser_session().await.map_err(|e| e.to_string())?;
Ok(Some(compose(client, &origin, &me.username, local)))
}
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] pub(crate) fn served_by_plm(path: &str) -> bool {
path.starts_with("/cad/app/")
}
pub(crate) fn compose(client: Rc<PlmClient>, url: &str, username: &str, local: Rc<dyn StoreBackend>) -> Rc<dyn StoreBackend> {
let on_saved = crate::plm::thumbnail::chain(crate::plm::uses::publish_on_save(), crate::plm::thumbnail::upload_on_save());
compose_with(client, url, username, local, Some(on_saved))
}
pub(crate) fn compose_with(
client: Rc<PlmClient>,
url: &str,
username: &str,
local: Rc<dyn StoreBackend>,
on_saved: Option<SavedHook>,
) -> Rc<dyn StoreBackend> {
let label = format!("PLM {url} as {username}");
let preferences = Rc::new(PreferencesBackend { client: client.clone() });
let mut documents = DocumentsBackend::new(client, label);
documents.on_saved = on_saved;
Rc::new(KeySpaceRouter {
documents: Rc::new(documents),
preferences: preferences.clone(),
local: Rc::new(RecoveryMirror::new(local, preferences)),
})
}
fn document_key(key: &str) -> Result<&str, String> {
if key.starts_with(DIR_PREFIX) {
return Err("folders are not stored on the PLM; your folders are your workspace".into());
}
key.strip_prefix(PREFIX).ok_or_else(|| format!("`{key}` is not a document key"))
}
fn text(bytes: Vec<u8>, key: &str) -> Result<String, String> {
String::from_utf8(bytes).map_err(|_| format!("{key}: the server's document is not UTF-8 text"))
}
#[derive(Default)]
struct Backoff {
failed: BTreeMap<String, (Instant, u32, String)>,
}
impl Backoff {
fn holding(&self, key: &str, now: Instant) -> Option<String> {
let (at, failures, why) = self.failed.get(key)?;
(now < *at + window(*failures)).then(|| why.clone())
}
fn fail(&mut self, key: &str, now: Instant, why: &str) {
let failures = self.failed.get(key).map_or(0, |(_, n, _)| *n) + 1;
self.failed.insert(key.to_string(), (now, failures, why.to_string()));
}
fn clear(&mut self, key: &str) {
self.failed.remove(key);
}
}
fn window(failures: u32) -> Duration {
let doublings = failures.saturating_sub(1).min(16);
(FIRST_BACKOFF * 2u32.pow(doublings)).min(MAX_BACKOFF)
}
pub(crate) type SavedHook = Rc<dyn Fn(Rc<PlmClient>, String, String) -> BackendFuture<()>>;
pub(crate) struct DocumentsBackend {
client: Rc<PlmClient>,
label: String,
backoff: Rc<RefCell<Backoff>>,
now: Rc<dyn Fn() -> Instant>,
on_saved: Option<SavedHook>,
index: Rc<RefCell<BTreeMap<String, super::client::IndexEntry>>>,
}
impl DocumentsBackend {
pub(crate) fn new(client: Rc<PlmClient>, label: String) -> Self {
Self { client, label, backoff: Rc::default(), now: Rc::new(Instant::now), on_saved: None, index: Rc::default() }
}
}
fn remember(into: &RefCell<BTreeMap<String, super::client::IndexEntry>>, index: &super::client::StoreIndex) {
*into.borrow_mut() = index.entries.iter().map(|e| (e.key.clone(), e.clone())).collect();
}
pub(crate) const INDEX_ROWS_PER_READ: usize = 100;
impl StoreBackend for DocumentsBackend {
fn label(&self) -> String {
self.label.clone()
}
fn plm_client(&self) -> Option<Rc<PlmClient>> {
Some(self.client.clone())
}
fn plm_revision(&self, key: &str) -> Option<super::client::IndexEntry> {
self.index.borrow().get(key).cloned()
}
fn refresh_plm_index(&self, keys: &[String], stale: bool) {
if !stale && keys.is_empty() {
return;
}
let (client, index) = (self.client.clone(), self.index.clone());
let keys = keys.to_vec();
detach(async move {
if stale {
if let Ok(fresh) = client.index().await {
remember(&index, &fresh);
}
return;
}
for chunk in keys.chunks(INDEX_ROWS_PER_READ) {
let Ok(rows) = client.index_rows(chunk).await else { return };
let mut held = index.borrow_mut();
for key in chunk {
held.remove(key);
}
for row in rows.entries {
held.insert(row.key.clone(), row);
}
}
});
}
fn load_all(&self) -> BackendFuture<Vec<(String, String)>> {
let client = self.client.clone();
Box::pin(async move {
let index = client.index().await.map_err(|e| e.to_string())?;
let mut all = Vec::new();
for entry in index.entries.into_iter().filter(|e| !e.content_hash.is_empty()) {
if let Some(bytes) = client.get_document(&entry.key).await.map_err(|e| e.to_string())? {
all.push((format!("{PREFIX}{}", entry.key), text(bytes, &entry.key)?));
}
}
Ok(all)
})
}
fn load_index(&self) -> BackendFuture<Vec<(String, Entry)>> {
let (client, keep) = (self.client.clone(), self.index.clone());
Box::pin(async move {
let index = client.index().await.map_err(|e| e.to_string())?;
remember(&keep, &index);
Ok(index
.entries
.into_iter()
.filter(|e| !e.content_hash.is_empty())
.map(|e| (format!("{PREFIX}{}", e.key), Entry::OnDisk { bytes: e.size }))
.collect())
})
}
fn orders_writes_per_key(&self) -> bool {
false
}
fn get(&self, key: &str) -> BackendFuture<Option<String>> {
let now = (self.now)();
if let Some(why) = self.backoff.borrow().holding(key, now) {
return Box::pin(async move { Err(why) });
}
let (client, backoff, clock, key) = (self.client.clone(), self.backoff.clone(), self.now.clone(), key.to_string());
Box::pin(async move {
let store_key = document_key(&key)?.to_string();
match client.get_document(&store_key).await {
Ok(bytes) => {
backoff.borrow_mut().clear(&key);
bytes.map(|b| text(b, &store_key)).transpose()
}
Err(PlmError::NotFound(_)) => {
backoff.borrow_mut().clear(&key);
Ok(None)
}
Err(e) => {
let why = e.to_string();
backoff.borrow_mut().fail(&key, clock(), &why);
Err(why)
}
}
})
}
fn put(&self, key: &str, value: &str) -> BackendFuture<()> {
self.backoff.borrow_mut().clear(key);
let (client, key, value, on_saved) = (self.client.clone(), key.to_string(), value.to_string(), self.on_saved.clone());
Box::pin(async move {
let store_key = document_key(&key)?.to_string();
client.put_document(&store_key, value.clone().into_bytes()).await.map_err(|e| e.to_string())?;
match on_saved {
Some(hook) => hook(client, store_key, value).await,
None => Ok(()),
}
})
}
fn delete(&self, key: &str) -> BackendFuture<()> {
self.backoff.borrow_mut().clear(key);
let (client, key) = (self.client.clone(), key.to_string());
Box::pin(async move {
let store_key = document_key(&key)?;
match client.delete_document(store_key).await {
Ok(()) | Err(PlmError::NotFound(_)) => Ok(()),
Err(e) => Err(e.to_string()),
}
})
}
}
fn preference_name(key: &str) -> Result<&'static str, String> {
crate::store::PREFERENCE_KEYS
.iter()
.chain(crate::store::LOCAL_KEYS)
.copied()
.find(|name| MirrorStore::key(name) == key)
.ok_or_else(|| format!("`{key}` is not a preference"))
}
pub(crate) struct PreferencesBackend {
client: Rc<PlmClient>,
}
impl StoreBackend for PreferencesBackend {
fn label(&self) -> String {
"PLM preferences".into()
}
fn load_all(&self) -> BackendFuture<Vec<(String, String)>> {
let client = self.client.clone();
Box::pin(async move {
let mut all = Vec::new();
for name in crate::store::PREFERENCE_KEYS {
if let Some(value) = client.get_preference(name).await.map_err(|e| e.to_string())? {
all.push((MirrorStore::key(name), value));
}
}
Ok(all)
})
}
fn orders_writes_per_key(&self) -> bool {
false
}
fn get(&self, key: &str) -> BackendFuture<Option<String>> {
let (client, key) = (self.client.clone(), key.to_string());
Box::pin(async move { client.get_preference(preference_name(&key)?).await.map_err(|e| e.to_string()) })
}
fn put(&self, key: &str, value: &str) -> BackendFuture<()> {
let (client, key, value) = (self.client.clone(), key.to_string(), value.to_string());
Box::pin(async move { client.put_preference(preference_name(&key)?, value).await.map_err(|e| e.to_string()) })
}
fn delete(&self, key: &str) -> BackendFuture<()> {
let (client, key) = (self.client.clone(), key.to_string());
Box::pin(async move { client.delete_preference(preference_name(&key)?).await.map_err(|e| e.to_string()) })
}
}
pub(crate) struct RecoveryMirror {
local: Rc<dyn StoreBackend>,
remote: Rc<dyn StoreBackend>,
outbox: Rc<RefCell<Outbox>>,
}
#[derive(Default)]
struct Outbox {
waiting: Option<(String, Option<String>)>,
sending: bool,
failures: u64,
}
impl RecoveryMirror {
pub(crate) fn new(local: Rc<dyn StoreBackend>, remote: Rc<dyn StoreBackend>) -> Self {
Self { local, remote, outbox: Rc::default() }
}
#[cfg_attr(not(test), allow(dead_code))] pub(crate) fn failures(&self) -> u64 {
self.outbox.borrow().failures
}
fn mirror(&self, key: &str, value: Option<String>) {
let mut outbox = self.outbox.borrow_mut();
outbox.waiting = Some((key.to_string(), value));
if outbox.sending {
return;
}
outbox.sending = true;
drop(outbox);
MIRRORS_IN_FLIGHT.with(|n| n.set(n.get() + 1));
let (outbox, remote) = (self.outbox.clone(), self.remote.clone());
detach(async move {
loop {
let next = outbox.borrow_mut().waiting.take();
let Some((key, value)) = next else {
outbox.borrow_mut().sending = false;
MIRRORS_IN_FLIGHT.with(|n| n.set(n.get().saturating_sub(1)));
return;
};
let sent = match value {
Some(value) => remote.put(&key, &value).await,
None => remote.delete(&key).await,
};
if sent.is_err() {
outbox.borrow_mut().failures += 1;
}
}
});
}
}
impl StoreBackend for RecoveryMirror {
fn label(&self) -> String {
self.local.label()
}
fn load_all(&self) -> BackendFuture<Vec<(String, String)>> {
let local = self.local.clone();
Box::pin(async move {
let mut all = Vec::new();
for name in crate::store::LOCAL_KEYS {
let key = MirrorStore::key(name);
if let Some(value) = local.get(&key).await? {
all.push((key, value));
}
}
Ok(all)
})
}
fn orders_writes_per_key(&self) -> bool {
self.local.orders_writes_per_key()
}
fn get(&self, key: &str) -> BackendFuture<Option<String>> {
self.local.get(key)
}
fn put(&self, key: &str, value: &str) -> BackendFuture<()> {
let local = self.local.put(key, value);
let (this, key, value) = (self.clone_handle(), key.to_string(), value.to_string());
Box::pin(async move {
local.await?;
if mirrored(&key) {
this.mirror(&key, Some(value));
}
Ok(())
})
}
fn delete(&self, key: &str) -> BackendFuture<()> {
let local = self.local.delete(key);
let (this, key) = (self.clone_handle(), key.to_string());
Box::pin(async move {
local.await?;
if mirrored(&key) {
this.mirror(&key, None);
}
Ok(())
})
}
}
fn mirrored(key: &str) -> bool {
key == MirrorStore::key(crate::store::RECOVERY_KEY)
}
impl RecoveryMirror {
fn clone_handle(&self) -> Self {
Self { local: self.local.clone(), remote: self.remote.clone(), outbox: self.outbox.clone() }
}
}
fn detach(task: impl std::future::Future<Output = ()> + 'static) {
#[cfg(target_arch = "wasm32")]
wasm_bindgen_futures::spawn_local(task);
#[cfg(not(target_arch = "wasm32"))]
super::native::spawn(task);
}