From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 91A481FF0BA for ; Thu, 20 Aug 2026 10:17:59 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id 5E57F21587; Thu, 20 Aug 2026 10:17:59 +0200 (CEST) From: Dominik Csapak 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 Message-ID: <20260820081746.989972-3-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260820081746.989972-1-d.csapak@proxmox.com> References: <20260820081746.989972-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -1.143 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record URI_PHISH 3.999 Phishing using web form Message-ID-Hash: R4MTSPPIQINZ2X3M5MUAMB3WZ6O4PKQV X-Message-ID-Hash: R4MTSPPIQINZ2X3M5MUAMB3WZ6O4PKQV X-MailFrom: d.csapak@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: 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 --- 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(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(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(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 { storage: StorageLocation, data: T, + listener: Option, } impl Deref for PersistentState { @@ -51,12 +55,14 @@ impl PersistentState { 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 PersistentState { 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>) { + 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>) -> Self { + self.set_on_update(on_update); + self + } } -- 2.47.3