svelte_store/lib.rs
1//! # Svelte Store Bindings
2//!
3//! This crate is intended to make working with svelte stores
4//! easy and ergonomic. Specifically, the goal is to allow
5//! easier use of Rust as the backend of a svelte app where
6//! the UI can directly react to changes that happen with
7//! the Rust-WASM world.
8//!
9//! This crate exposes one struct, mainly [`Readable`], which
10//! allows seemless management of readable Svelte stores in JS.
11//! Despite it's name, [`Readable`] can be written to from Rust,
12//! but only yields a `Readable` store to JS, making sure that
13//! mutation can only happen within Rust's safety guarantees.
14//!
15//! These stores can additionally be annotated with Typescript types
16//! to provide better safety from the JS side. To see how, check out
17//! the [`Readable::get_store`] example. (Note: [`Readable::get_store`]
18//! fn and example is only available on `wasm32` targets)
19
20#[cfg(target_arch = "wasm32")]
21mod bindings;
22
23use std::{
24 cell::UnsafeCell,
25 fmt,
26 ops::{self, Deref},
27};
28use wasm_bindgen::prelude::*;
29
30/// Rust-managed `Readable` Svelte store.
31pub struct Readable<T> {
32 value: UnsafeCell<T>,
33 #[cfg(target_arch = "wasm32")]
34 writable_store: bindings::Writable,
35 #[cfg(target_arch = "wasm32")]
36 derived_store: bindings::Readable,
37 #[allow(clippy::type_complexity)]
38 #[cfg(target_arch = "wasm32")]
39 mapped_set_fn: Box<dyn FnMut(&T) -> JsValue>,
40 #[cfg(target_arch = "wasm32")]
41 _derived_store_map_fn: Closure<dyn FnMut(JsValue) -> JsValue>,
42}
43
44impl<T> fmt::Debug for Readable<T>
45where
46 T: fmt::Debug,
47{
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 f.debug_tuple("Readable").field(self.deref()).finish()
50 }
51}
52
53impl<T> fmt::Display for Readable<T>
54where
55 T: fmt::Display,
56{
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 self.deref().fmt(f)
59 }
60}
61
62impl<T> Default for Readable<T>
63where
64 T: Default + Clone + Into<JsValue> + 'static,
65{
66 fn default() -> Self {
67 Self::new(T::default())
68 }
69}
70
71/// [`Readable`] relies on the fact that only one instance
72/// can exist at a time to provide transparent dereferencing
73/// to the inner value. As a result, it is unsound to implement
74/// [`Clone`]. If you need shared mutability, try using
75/// [`Rc`](std::rc::Rc) and [`RefCell`](std::cell::RefCell).
76impl<T> ops::Deref for Readable<T> {
77 type Target = T;
78
79 fn deref(&self) -> &Self::Target {
80 // SAFETY:
81 // This is safe because the `set` fn is the only way to
82 // mutate T, which already requires an &mut Self, so the
83 // borrow checker will make sure no outstanding aliases
84 // are possible
85 unsafe { &*self.value.get() }
86 }
87}
88
89impl<T: 'static> Readable<T> {
90 #[allow(unused_variables)]
91 fn init_store<F>(initial_value: UnsafeCell<T>, mapping_fn: F) -> Self
92 where
93 F: FnMut(&T) -> JsValue + 'static,
94 {
95 #[cfg(target_arch = "wasm32")]
96 let this = {
97 let mut mapped_set_fn =
98 Box::new(mapping_fn) as Box<dyn FnMut(&T) -> JsValue>;
99
100 let writable_store = bindings::writable(mapped_set_fn(unsafe {
101 &*initial_value.get()
102 }));
103
104 let derived_store_map_fn = Closure::new(|v| v);
105
106 let derived_store =
107 bindings::derived(&writable_store, &derived_store_map_fn);
108
109 Self {
110 value: initial_value,
111 writable_store,
112 derived_store,
113 mapped_set_fn,
114 _derived_store_map_fn: derived_store_map_fn,
115 }
116 };
117
118 #[cfg(not(target_arch = "wasm32"))]
119 let this = {
120 Self {
121 value: initial_value,
122 }
123 };
124
125 this
126 }
127
128 /// Creates a `Readable` Svelte store.
129 ///
130 /// This function is only implemented for types that can be converted
131 /// into [`JsValue`]. This includes all types annotated with
132 /// `#[wasm_bindgen]`. If your type does not provide an [`Into<JsValue>`]
133 /// implementation, use [`Readable::new_mapped`] instead.
134 ///
135 /// # Examples
136 ///
137 /// Using a type that already provides an implementation of
138 /// [`Into<JsValue>`].
139 ///
140 /// ```
141 /// use svelte_store::Readable;
142 ///
143 /// let store = Readable::new(0u8);
144 /// ```
145 ///
146 /// Using a type annotated with `#[wasm_bindgen]`.
147 ///
148 /// ```
149 /// use svelte_store::Readable;
150 /// use wasm_bindgen::prelude::*;
151 ///
152 /// #[derive(Clone)]
153 /// #[wasm_bindgen]
154 /// pub struct MyStruct;
155 ///
156 /// let store = Readable::new(MyStruct);
157 /// ```
158 pub fn new(initial_value: T) -> Self
159 where
160 T: Clone + Into<JsValue>,
161 {
162 Self::init_store(UnsafeCell::new(initial_value), |v| v.clone().into())
163 }
164
165 /// Creates a new `Readable` Svelte store which calls its mapping fn each
166 /// time the store is set, to produce a [`JsValue`].
167 ///
168 /// This method should be used whenever [`Readable::new`] cannot be,
169 /// due to lacking trait compatibility.
170 ///
171 /// # Examples
172 ///
173 /// Creating a store of [`Vec<u8>`].
174 ///
175 /// ```
176 /// use svelte_store::Readable;
177 /// use wasm_bindgen::prelude::*;
178 ///
179 /// let values = vec![7u8; 7];
180 ///
181 /// let store = Readable::new_mapped(values, |values: &Vec<u8>| {
182 /// values
183 /// .iter()
184 /// .cloned()
185 /// .map(JsValue::from)
186 /// .collect::<js_sys::Array>()
187 /// .into()
188 /// });
189 /// ```
190 pub fn new_mapped<F>(initial_value: T, mapping_fn: F) -> Self
191 where
192 F: FnMut(&T) -> JsValue + 'static,
193 {
194 Self::init_store(UnsafeCell::new(initial_value), mapping_fn)
195 }
196
197 /// Sets the value of the store, notifying all store
198 /// listeners the value has changed.
199 pub fn set(&mut self, new_value: T) {
200 // SAFETY:
201 // This is safe because this function is the only way to
202 // mutate T, which already requires an &mut Self, so the
203 // borrow checker will make sure no outstanding aliases
204 // are possible
205 let value = unsafe { &mut *self.value.get() };
206
207 *value = new_value;
208
209 #[cfg(target_arch = "wasm32")]
210 self.writable_store.set((self.mapped_set_fn)(value));
211 }
212
213 /// Calls the provided `f` with a `&mut T`, returning
214 /// whatever `f` returns. After this function is called,
215 /// the store will be updated and all store listeners will
216 /// be notified.
217 pub fn set_with<F, O>(&mut self, f: F) -> O
218 where
219 F: FnOnce(&mut T) -> O,
220 {
221 // SAFETY:
222 // This is safe because this function is the only way to
223 // mutate T, which already requires an &mut Self, so the
224 // borrow checker will make sure no outstanding aliases
225 // are possible
226 let value = unsafe { &mut *self.value.get() };
227
228 #[allow(clippy::let_and_return)]
229 let o = f(value);
230
231 #[cfg(target_arch = "wasm32")]
232 {
233 self.writable_store.set((self.mapped_set_fn)(value));
234 }
235
236 o
237 }
238
239 /// Gets the store that can be directly passed to JS and subscribed
240 /// to.
241 ///
242 /// # Examples
243 ///
244 /// ```no_run
245 /// use wasm_bindgen::prelude::*;
246 /// use svelte_store::Readable;
247 ///
248 /// #[wasm_bindgen(typescript_custom_section)]
249 /// const TYPESCRIPT_TYPES: &str = r#"
250 /// import type { Readable } from "svelte/store";
251 ///
252 /// type ReadableNumber = Readable<number>;
253 /// "#;
254 ///
255 /// #[wasm_bindgen]
256 /// extern "C" {
257 /// #[wasm_bindgen(typescript_type = "ReadableNumber")]
258 /// type ReadableNumber;
259 /// }
260 ///
261 /// #[wasm_bindgen]
262 /// pub struct MyStruct {
263 /// my_number: Readable<u8>,
264 /// }
265 ///
266 /// #[wasm_bindgen]
267 /// impl MyStruct {
268 /// #[wasm_bindgen(getter)]
269 /// pub fn number(&self) -> ReadableNumber {
270 /// self.my_number.get_store().into()
271 /// }
272 /// }
273 /// ```
274 pub fn get_store(&self) -> JsValue {
275 #[cfg(not(target_arch = "wasm32"))]
276 panic!(
277 "`Readable::get_store()` can only be called \
278 within `wasm32` targets"
279 );
280
281 #[cfg(target_arch = "wasm32")]
282 return self.derived_store.clone();
283 }
284}