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}