lmjtfy.git / apps / lmjtfy / src / past.rs
past.rsannotatedpast.rssource90 lines · 3.9 KB · raw
1//! What an object's storage can do that workers-rs has no binding for:
2//! go back to an earlier moment, and do several statements as one.
3//!
4//! Cloudflare keeps thirty days of an SQLite object's history. A moment in
5//! it is named by a bookmark; told to restore to one, the object has that
6//! moment's storage the next time it starts. That rewinds everything the
7//! object keeps: it is for a migration that went wrong minutes ago, not
8//! for taking back one row.
9
10use js_sys::{Function, Promise, Reflect};
11use wasm_bindgen::closure::Closure;
12use wasm_bindgen::{JsCast, JsValue};
13use wasm_bindgen_futures::JsFuture;
14use worker::State;
15
16fn message(error: JsValue) -> String {
17    error
18        .dyn_ref::<js_sys::Error>()
19        .map(|error| String::from(error.message()))
20        .or_else(|| error.as_string())
21        .unwrap_or_else(|| format!("{error:?}"))
22}
23
24/// The object's state as the runtime gave it, for the calls below.
25#[derive(Clone)]
26pub struct Past(JsValue);
27
28impl Past {
29    /// Takes a second hold on `state`, and gives it back.
30    pub fn of(state: State) -> (Past, State) {
31        let inner = state._inner();
32        let held: &JsValue = inner.as_ref();
33        (Past(held.clone()), State::from(inner))
34    }
35
36    /// Calls `storage.<name>(argument)`.
37    fn storage(&self, name: &str, argument: &JsValue) -> Result<JsValue, String> {
38        let storage = Reflect::get(&self.0, &"storage".into()).map_err(message)?;
39        let method: Function = Reflect::get(&storage, &name.into()).map_err(message)?.dyn_into().map_err(|_| format!("the storage has no {name}"))?;
40        method.call1(&storage, argument).map_err(message)
41    }
42
43    async fn text(&self, name: &str, argument: &JsValue) -> Result<String, String> {
44        let promise: Promise = self.storage(name, argument)?.dyn_into().map_err(|_| format!("{name} did not return a promise"))?;
45        JsFuture::from(promise).await.map_err(message)?.as_string().ok_or_else(|| format!("{name} did not answer with a bookmark"))
46    }
47
48    /// The bookmark for `at_ms`, which must be in the last thirty days, or
49    /// for now. Asking changes nothing.
50    pub async fn bookmark(&self, at_ms: Option<f64>) -> Result<String, String> {
51        match at_ms {
52            Some(at_ms) => self.text("getBookmarkForTime", &at_ms.into()).await,
53            None => self.text("getCurrentBookmark", &JsValue::UNDEFINED).await,
54        }
55    }
56
57    /// Has the object's storage be as it was at `bookmark` when it next
58    /// starts, and answers with the bookmark of the moment before that, to
59    /// undo it by. `restart` is what makes it happen.
60    pub async fn restore(&self, bookmark: &str) -> Result<String, String> {
61        self.text("onNextSessionRestoreBookmark", &bookmark.into()).await
62    }
63
64    /// Ends the object where it stands; the next request starts it again.
65    /// Whatever is waiting on it fails.
66    pub fn restart(&self) {
67        if let Ok(abort) = Reflect::get(&self.0, &"abort".into()).and_then(|abort| abort.dyn_into::<Function>()) {
68            let _ = abort.call1(&self.0, &"restoring an earlier moment".into());
69        }
70    }
71
72    /// Runs `all` as one: if it fails, nothing it wrote is kept.
73    pub fn atomically(&self, all: impl FnOnce() -> worker::Result<()> + 'static) -> Result<(), String> {
74        let failed = std::rc::Rc::new(std::cell::RefCell::new(None));
75        let said = failed.clone();
76        // A callback that throws is what rolls the transaction back.
77        let callback = Closure::once_into_js(move || -> Result<JsValue, JsValue> {
78            all().map(|()| JsValue::UNDEFINED).map_err(|error| {
79                let error = error.to_string();
80                *said.borrow_mut() = Some(error.clone());
81                JsValue::from(js_sys::Error::new(&error))
82            })
83        });
84        let done = self.storage("transactionSync", &callback).map(|_| ());
85        match failed.borrow_mut().take() {
86            Some(error) => Err(error),
87            None => done,
88        }
89    }
90}