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}