atagen

macltok

public
Star0
Watch0
9c05b1dd327a57081652545c709cc8d7e961de15
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/store_lock.rsRust117 lines4.2 KB
1
use crate::{AccountError, account::account_error};
2
3
/// the platform's own exclusive lock on a store. matrix-sdk 0.18's cross process lease drops
4
/// itself silently after one failed renewal or a stall past 500ms, so it can't be what keeps a
5
/// second engine out
6
pub(crate) struct StoreLock {
7
    #[cfg(feature = "sqlite")]
8
    _file: std::fs::File,
9
    #[cfg(feature = "indexeddb")]
10
    release: js_sys::Function,
11
}
12
13
#[cfg(feature = "sqlite")]
14
pub(crate) const LOCK_FILE: &str = "macltok.lock";
15
16
#[cfg(feature = "sqlite")]
17
impl StoreLock {
18
    pub(crate) fn file(directory: &std::path::Path) -> Result<Self, AccountError> {
19
        use std::fs::{self, File, TryLockError};
20
21
        fs::create_dir_all(directory)
22
            .map_err(|error| account_error("creating the Matrix store directory", error))?;
23
        let file = File::options()
24
            .create(true)
25
            .truncate(false)
26
            .write(true)
27
            .open(directory.join(LOCK_FILE))
28
            .map_err(|error| account_error("opening the Matrix store's lock file", error))?;
29
        match file.try_lock() {
30
            Ok(()) => Ok(Self { _file: file }),
31
            Err(TryLockError::WouldBlock) => Err(AccountError::StoreInUse),
32
            Err(TryLockError::Error(error)) => {
33
                Err(account_error("locking the Matrix store", error))
34
            }
35
        }
36
    }
37
}
38
39
#[cfg(feature = "indexeddb")]
40
#[derive(Debug, thiserror::Error)]
41
enum WebLockError {
42
    #[error("this global scope has no Web Locks, they need a secure context")]
43
    Unavailable,
44
    #[error("requesting the Web Lock failed, {0}")]
45
    Failed(String),
46
}
47
48
#[cfg(feature = "indexeddb")]
49
impl StoreLock {
50
    /// `ifAvailable` answers at once. the lock is ours until the promise we hand back settles
51
    pub(crate) async fn web(name: &str) -> Result<Self, AccountError> {
52
        use futures_util::future::{Either, select};
53
        use js_sys::{Function, Object, Promise, Reflect};
54
        use tokio::sync::oneshot;
55
        use wasm_bindgen::{JsCast, JsValue, closure::Closure};
56
57
        let failed = |error: JsValue| {
58
            account_error(
59
                "locking the Matrix store",
60
                WebLockError::Failed(format!("{error:?}")),
61
            )
62
        };
63
        let locks = Reflect::get(&js_sys::global(), &JsValue::from_str("navigator"))
64
            .and_then(|navigator| Reflect::get(&navigator, &JsValue::from_str("locks")))
65
            .map_err(failed)?;
66
        if locks.is_undefined() {
67
            return Err(account_error(
68
                "locking the Matrix store",
69
                WebLockError::Unavailable,
70
            ));
71
        }
72
        let request: Function = Reflect::get(&locks, &JsValue::from_str("request"))
73
            .map_err(failed)?
74
            .dyn_into()
75
            .map_err(failed)?;
76
        let options = Object::new();
77
        Reflect::set(&options, &JsValue::from_str("ifAvailable"), &JsValue::TRUE)
78
            .map_err(failed)?;
79
80
        let (granted, grant) = oneshot::channel();
81
        let callback = Closure::once(move |lock: JsValue| -> JsValue {
82
            if lock.is_null() {
83
                let _ = granted.send(None);
84
                return JsValue::UNDEFINED;
85
            }
86
            let mut release = None;
87
            let held = Promise::new(&mut |resolve, _| release = Some(resolve));
88
            let _ = granted.send(release);
89
            held.into()
90
        });
91
        let outcome: Promise = request
92
            .call3(
93
                &locks,
94
                &JsValue::from_str(&format!("macltok:{name}")),
95
                &options,
96
                callback.as_ref(),
97
            )
98
            .map_err(failed)?
99
            .dyn_into()
100
            .map_err(failed)?;
101
        match select(grant, wasm_bindgen_futures::JsFuture::from(outcome)).await {
102
            Either::Left((Ok(Some(release)), _)) => Ok(Self { release }),
103
            Either::Left((Ok(None), _)) => Err(AccountError::StoreInUse),
104
            Either::Left((Err(_), _)) | Either::Right((Ok(_), _)) => {
105
                Err(failed(JsValue::from_str("the lock callback never ran")))
106
            }
107
            Either::Right((Err(error), _)) => Err(failed(error)),
108
        }
109
    }
110
}
111
112
#[cfg(feature = "indexeddb")]
113
impl Drop for StoreLock {
114
    fn drop(&mut self) {
115
        let _ = self.release.call0(&wasm_bindgen::JsValue::UNDEFINED);
116
    }
117
}