all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH yew-widget-toolkit 2/3] state: persistent state: allow listening for updates
Date: Thu, 20 Aug 2026 10:17:38 +0200	[thread overview]
Message-ID: <20260820081746.989972-3-d.csapak@proxmox.com> (raw)
In-Reply-To: <20260820081746.989972-1-d.csapak@proxmox.com>

A `PersistentState` only reads its storage location on construction, so
instances held by unrelated components go stale as soon as another one
writes. Components deep in different subtrees often share nothing but the
storage key, so they cannot be handed a common instance either.

Dispatch a per-location event whenever `store_state` succeeds and let
`PersistentState` subscribe to it. The event carries no payload, as the
callback cannot mutate the instance it belongs to, hence `load` is now
public so that the owner can pick up the new value.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
 src/state/mod.rs              | 26 +++++++++++++++++++-----
 src/state/persistent_state.rs | 37 ++++++++++++++++++++++++++++++++++-
 2 files changed, 57 insertions(+), 6 deletions(-)

diff --git a/src/state/mod.rs b/src/state/mod.rs
index 6f99c7e..87ff8a2 100644
--- a/src/state/mod.rs
+++ b/src/state/mod.rs
@@ -124,6 +124,15 @@ pub fn load_state<T: 'static + DeserializeOwned>(storage: &StorageLocation) -> O
     None
 }
 
+fn create_storage_event_name(location: &StorageLocation) -> String {
+    let (prefix, name) = match location {
+        StorageLocation::Local(name) => ("local", name),
+        StorageLocation::Session(name) => ("session", name),
+    };
+
+    format!("pwt-storage-{prefix}-{name}")
+}
+
 pub fn store_state<T: 'static + Serialize>(data: &T, storage: &StorageLocation) {
     let (store, state_id) = match storage {
         StorageLocation::Local(state_id) => (local_storage(), state_id),
@@ -131,11 +140,18 @@ pub fn store_state<T: 'static + Serialize>(data: &T, storage: &StorageLocation)
     };
     if let Some(store) = store {
         let item_str = serde_json::to_string(data).unwrap();
-        if let Err(err) = store.set_item(state_id, &item_str) {
-            log::error!(
-                "store persistent state {state_id} failed: {}",
-                crate::convert_js_error(err)
-            )
+        match store.set_item(state_id, &item_str) {
+            Ok(()) => {
+                if let Err(err) = dispatch_custom_event(&create_storage_event_name(storage)) {
+                    log::error!("failed to send storage event: {err}")
+                }
+            }
+            Err(err) => {
+                log::error!(
+                    "store persistent state {state_id} failed: {}",
+                    crate::convert_js_error(err)
+                )
+            }
         }
     }
 }
diff --git a/src/state/persistent_state.rs b/src/state/persistent_state.rs
index 90a54b7..672b0e5 100644
--- a/src/state/persistent_state.rs
+++ b/src/state/persistent_state.rs
@@ -1,7 +1,10 @@
+use gloo_events::EventListener;
 use serde::{Serialize, de::DeserializeOwned};
 use std::ops::Deref;
+use yew::Callback;
 
 use crate::props::StorageLocation;
+use crate::state::create_storage_event_name;
 
 /// Helper to store data persistently using window local [Storage](web_sys::Storage)
 ///
@@ -35,6 +38,7 @@ use crate::props::StorageLocation;
 pub struct PersistentState<T> {
     storage: StorageLocation,
     data: T,
+    listener: Option<EventListener>,
 }
 
 impl<T> Deref for PersistentState<T> {
@@ -51,12 +55,14 @@ impl<T: 'static + Default + Serialize + DeserializeOwned> PersistentState<T> {
         let mut me = Self {
             data: T::default(),
             storage: storage.into(),
+            listener: None,
         };
         me.load();
         me
     }
 
-    fn load(&mut self) {
+    /// Loads the (potentially updated) data again from the given storage
+    pub fn load(&mut self) {
         if let Some(data) = super::load_state(&self.storage) {
             self.data = data;
         }
@@ -80,4 +86,33 @@ impl<T: 'static + Default + Serialize + DeserializeOwned> PersistentState<T> {
     pub fn into_inner(self) -> T {
         self.data
     }
+
+    /// Calls the callback when the data has changed.
+    ///
+    /// CAUTION: this does not update the data, [Self::load] must be called after this event to get
+    /// the current value.
+    ///
+    /// The callback also fires for updates done through this instance, so it must not write back
+    /// unconditionally, otherwise it triggers itself in an endless loop. Only updates from the
+    /// current document are reported, changes made in other browser tabs are not.
+    pub fn set_on_update(&mut self, on_update: impl Into<Callback<()>>) {
+        let on_update = on_update.into();
+        let Some(document) = web_sys::window().and_then(|window| window.document()) else {
+            return;
+        };
+
+        self.listener = Some(EventListener::new(
+            &document,
+            create_storage_event_name(&self.storage),
+            move |_| on_update.emit(()),
+        ));
+    }
+
+    /// Builder style method to set an update callback [Self::set_on_update].
+    /// CAUTION: this does not update the data, [Self::load] must be called after this event to
+    /// get the current value
+    pub fn on_update(mut self, on_update: impl Into<Callback<()>>) -> Self {
+        self.set_on_update(on_update);
+        self
+    }
 }
-- 
2.47.3





  parent reply	other threads:[~2026-08-20  8:17 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-20  8:17 [PATCH datacenter-manager/yew-widget-toolkit 0/3] ui: make the default dashboard configurable Dominik Csapak
2026-08-20  8:17 ` [PATCH yew-widget-toolkit 1/3] state: event: add helpers for custom DOM events Dominik Csapak
2026-08-20  8:17 ` Dominik Csapak [this message]
2026-08-20  8:17 ` [PATCH datacenter-manager 3/3] ui: make the default dashboard page configurable Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260820081746.989972-3-d.csapak@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal