mirror of
https://github.com/leptos-rs/leptos.git
synced 2025-12-27 09:54:41 -05:00
Merge remote-tracking branch 'origin' into leptos_0.8
This commit is contained in:
4
.github/workflows/ci-semver.yml
vendored
4
.github/workflows/ci-semver.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
test:
|
||||
needs: [get-leptos-changed]
|
||||
if: needs.get-leptos-changed.outputs.leptos_changed == 'true' && github.event.pull_request.labels[0].name != 'breaking'
|
||||
name: Run semver check (nightly-2025-02-19)
|
||||
name: Run semver check (nightly-2025-03-05)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Glib
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
- name: Semver Checks
|
||||
uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
with:
|
||||
rust-toolchain: nightly-2025-02-19
|
||||
rust-toolchain: nightly-2025-03-05
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -27,4 +27,4 @@ jobs:
|
||||
directory: ${{ matrix.directory }}
|
||||
erased_mode: ${{ matrix.erased_mode }}
|
||||
cargo_make_task: "ci"
|
||||
toolchain: nightly-2025-02-19
|
||||
toolchain: nightly-2025-03-05
|
||||
|
||||
@@ -103,7 +103,9 @@ pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
|
||||
|
||||
/// Sets the current thread-local error hook, which will be invoked when [`throw`] is called.
|
||||
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
|
||||
ResetErrorHookOnDrop(ERROR_HOOK.with_borrow_mut(|this| this.replace(hook)))
|
||||
ResetErrorHookOnDrop(
|
||||
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Invokes the error hook set by [`set_error_hook`] with the given error.
|
||||
|
||||
@@ -22,20 +22,20 @@ log = "0.4.22"
|
||||
simple_logger = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
axum = { version = "0.8.1", optional = true }
|
||||
tower = { version = "0.4.13", optional = true }
|
||||
tower-http = { version = "0.5.2", features = [
|
||||
tower = { version = "0.5.2", optional = true }
|
||||
tower-http = { version = "0.6.2", features = [
|
||||
"fs",
|
||||
"tracing",
|
||||
"trace",
|
||||
], optional = true }
|
||||
tokio = { version = "1.39", features = ["full"], optional = true }
|
||||
thiserror = "1.0"
|
||||
thiserror = "2.0.11"
|
||||
wasm-bindgen = "0.2.93"
|
||||
serde_toml = "0.0.1"
|
||||
toml = "0.8.19"
|
||||
web-sys = { version = "0.3.70", features = ["FileList", "File"] }
|
||||
strum = { version = "0.26.3", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "6.1", optional = true }
|
||||
strum = { version = "0.27.1", features = ["strum_macros", "derive"] }
|
||||
notify = { version = "8.0", optional = true }
|
||||
pin-project-lite = "0.2.14"
|
||||
dashmap = { version = "6.0", optional = true }
|
||||
once_cell = { version = "1.19", optional = true }
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "nightly-2025-02-19"
|
||||
channel = "nightly-2025-03-05"
|
||||
|
||||
@@ -11,13 +11,13 @@ dependencies = [
|
||||
[tasks.test-leptos_macro-example]
|
||||
description = "Tests the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2025-02-19", "test", "--doc"]
|
||||
args = ["+nightly-2025-03-05", "test", "--doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
[tasks.doc-leptos_macro-example]
|
||||
description = "Docs the leptos_macro/example to check if macro handles doc comments correctly"
|
||||
command = "cargo"
|
||||
args = ["+nightly-2025-02-19", "doc"]
|
||||
args = ["+nightly-2025-03-05", "doc"]
|
||||
cwd = "example"
|
||||
install_crate = false
|
||||
|
||||
@@ -1175,8 +1175,14 @@ pub(crate) fn two_way_binding_to_tokens(
|
||||
let ident =
|
||||
format_ident!("{}", name.to_case(UpperCamel), span = node.key.span());
|
||||
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
if name == "group" {
|
||||
quote! {
|
||||
.bind(leptos::tachys::reactive_graph::bind::#ident, #value)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
.bind(::leptos::attr::#ident, #value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ use reactive_graph::{
|
||||
guards::{AsyncPlain, ReadGuard},
|
||||
ArcRwSignal, RwSignal,
|
||||
},
|
||||
traits::{DefinedAt, IsDisposed, ReadUntracked, Track, Update, Write},
|
||||
traits::{
|
||||
DefinedAt, IsDisposed, ReadUntracked, Track, Update, With, Write,
|
||||
},
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
use std::{
|
||||
@@ -91,6 +93,34 @@ impl<T> ArcLocalResource<T> {
|
||||
pub fn refetch(&self) {
|
||||
*self.refetch.write() += 1;
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&SendWrapper<T>) -> U) -> Option<U>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
self.data.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> ArcLocalResource<Result<T, E>>
|
||||
where
|
||||
T: 'static,
|
||||
E: Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoFuture for ArcLocalResource<T>
|
||||
|
||||
@@ -168,6 +168,41 @@ where
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
#[track_caller]
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U>
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
self.try_with(|n| n.as_ref().map(f))?
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> ArcOnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> ArcOnceResource<T, Ser> {
|
||||
@@ -534,6 +569,37 @@ where
|
||||
defined_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronously, reactively reads the current value of the resource and applies the function
|
||||
/// `f` to its value if it is `Some(_)`.
|
||||
pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> Option<U> {
|
||||
self.try_with(|n| n.as_ref().map(|n| Some(f(n))))?.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, Ser> OnceResource<Result<T, E>, Ser>
|
||||
where
|
||||
Ser: Encoder<Result<T, E>> + Decoder<Result<T, E>>,
|
||||
<Ser as Encoder<Result<T, E>>>::Error: Debug,
|
||||
<Ser as Decoder<Result<T, E>>>::Error: Debug,
|
||||
<<Ser as Decoder<Result<T, E>>>::Encoded as FromEncodedStr>::DecodingError:
|
||||
Debug,
|
||||
<Ser as Encoder<Result<T, E>>>::Encoded: IntoEncodedString,
|
||||
<Ser as Decoder<Result<T, E>>>::Encoded: FromEncodedStr,
|
||||
T: Send + Sync + 'static,
|
||||
E: Send + Sync + Clone + 'static,
|
||||
{
|
||||
/// Applies the given function when a resource that returns `Result<T, E>`
|
||||
/// has resolved and loaded an `Ok(_)`, rather than requiring nested `.map()`
|
||||
/// calls over the `Option<Result<_, _>>` returned by the resource.
|
||||
///
|
||||
/// This is useful when used with features like server functions, in conjunction
|
||||
/// with `<ErrorBoundary/>` and `<Suspense/>`, when these other components are
|
||||
/// left to handle the `None` and `Err(_)` states.
|
||||
#[track_caller]
|
||||
pub fn and_then<U>(&self, f: impl FnOnce(&T) -> U) -> Option<Result<U, E>> {
|
||||
self.map(|data| data.as_ref().map(f).map_err(|e| e.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Ser> OnceResource<T, Ser>
|
||||
|
||||
@@ -215,16 +215,11 @@ where
|
||||
None
|
||||
}
|
||||
Ok(encoded) => {
|
||||
match Ser::decode(encoded.borrow()) {
|
||||
#[allow(unused_variables)]
|
||||
// used in tracing
|
||||
Err(e) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::error!("{e:?}");
|
||||
None
|
||||
}
|
||||
Ok(value) => Some(value),
|
||||
}
|
||||
let decoded = Ser::decode(encoded.borrow());
|
||||
#[cfg(feature = "tracing")]
|
||||
let decoded = decoded
|
||||
.inspect_err(|e| tracing::error!("{e:?}"));
|
||||
decoded.ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ pub trait OrPoisoned {
|
||||
fn or_poisoned(self) -> Self::Inner;
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockReadGuard<'a, T>, PoisonError<RwLockReadGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockReadGuard<'a, T>;
|
||||
@@ -45,7 +45,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned
|
||||
impl<'a, T: ?Sized> OrPoisoned
|
||||
for Result<RwLockWriteGuard<'a, T>, PoisonError<RwLockWriteGuard<'a, T>>>
|
||||
{
|
||||
type Inner = RwLockWriteGuard<'a, T>;
|
||||
@@ -55,7 +55,7 @@ impl<'a, T> OrPoisoned
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
impl<'a, T: ?Sized> OrPoisoned for LockResult<MutexGuard<'a, T>> {
|
||||
type Inner = MutexGuard<'a, T>;
|
||||
|
||||
fn or_poisoned(self) -> Self::Inner {
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod effect;
|
||||
mod effect_function;
|
||||
mod immediate;
|
||||
mod inner;
|
||||
mod render_effect;
|
||||
|
||||
pub use effect::*;
|
||||
pub use effect_function::*;
|
||||
pub use immediate::*;
|
||||
pub use render_effect::*;
|
||||
|
||||
/// Creates a new render effect, which immediately runs `fun`.
|
||||
|
||||
@@ -374,47 +374,16 @@ impl Effect<SyncStorage> {
|
||||
/// This spawns a task that can be run on any thread. For an effect that will be spawned on
|
||||
/// the current thread, use [`new`](Effect::new).
|
||||
pub fn new_sync<T, M>(
|
||||
mut fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
fun: impl EffectFunction<T, M> + Send + Sync + 'static,
|
||||
) -> Self
|
||||
where
|
||||
T: Send + Sync + 'static,
|
||||
{
|
||||
let inner = cfg!(feature = "effects").then(|| {
|
||||
let (mut rx, owner, inner) = effect_base();
|
||||
let mut first_run = true;
|
||||
let value = Arc::new(RwLock::new(None::<T>));
|
||||
if !cfg!(feature = "effects") {
|
||||
return Self { inner: None };
|
||||
}
|
||||
|
||||
crate::spawn({
|
||||
let value = Arc::clone(&value);
|
||||
let subscriber = inner.to_any_subscriber();
|
||||
|
||||
async move {
|
||||
while rx.next().await.is_some() {
|
||||
if !owner.paused()
|
||||
&& (subscriber.with_observer(|| {
|
||||
subscriber.update_if_necessary()
|
||||
}) || first_run)
|
||||
{
|
||||
first_run = false;
|
||||
subscriber.clear_sources(&subscriber);
|
||||
|
||||
let old_value =
|
||||
mem::take(&mut *value.write().or_poisoned());
|
||||
let new_value = owner.with_cleanup(|| {
|
||||
subscriber.with_observer(|| {
|
||||
run_in_effect_scope(|| fun.run(old_value))
|
||||
})
|
||||
});
|
||||
*value.write().or_poisoned() = Some(new_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
ArenaItem::new_with_storage(Some(inner))
|
||||
});
|
||||
|
||||
Self { inner }
|
||||
Self::new_isomorphic(fun)
|
||||
}
|
||||
|
||||
/// Creates a new effect, which runs once on the next “tick”, and then runs again when reactive values
|
||||
|
||||
379
reactive_graph/src/effect/immediate.rs
Normal file
379
reactive_graph/src/effect/immediate.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
use crate::{
|
||||
graph::{AnySubscriber, ReactiveNode, ToAnySubscriber},
|
||||
owner::on_cleanup,
|
||||
traits::{DefinedAt, Dispose},
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
panic::Location,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
|
||||
/// Effects run a certain chunk of code whenever the signals they depend on change.
|
||||
///
|
||||
/// The effect runs on creation and again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: you probably want use [`Effect`](super::Effect) instead.
|
||||
/// This is for the few cases where it's important to execute effects immediately and in order.
|
||||
///
|
||||
/// [ImmediateEffect]s stop running when dropped.
|
||||
///
|
||||
/// NOTE: since effects are executed immediately, they might recurse.
|
||||
/// Under recursion or parallelism only the last run to start is tracked.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use reactive_graph::computed::*;
|
||||
/// # use reactive_graph::signal::*; let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// # use reactive_graph::prelude::*;
|
||||
/// # use reactive_graph::effect::ImmediateEffect;
|
||||
/// # use reactive_graph::owner::ArenaItem;
|
||||
/// # let owner = reactive_graph::owner::Owner::new(); owner.set();
|
||||
/// let a = RwSignal::new(0);
|
||||
/// let b = RwSignal::new(0);
|
||||
///
|
||||
/// // ✅ use effects to interact between reactive state and the outside world
|
||||
/// let _drop_guard = ImmediateEffect::new(move || {
|
||||
/// // on the next “tick” prints "Value: 0" and subscribes to `a`
|
||||
/// println!("Value: {}", a.get());
|
||||
/// });
|
||||
///
|
||||
/// // The effect runs immediately and subscribes to `a`, in the process it prints "Value: 0"
|
||||
/// # assert_eq!(a.get(), 0);
|
||||
/// a.set(1);
|
||||
/// # assert_eq!(a.get(), 1);
|
||||
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
|
||||
/// ```
|
||||
/// ## Notes
|
||||
///
|
||||
/// 1. **Scheduling**: Effects run immediately, as soon as any tracked signal changes.
|
||||
/// 2. By default, effects do not run unless the `effects` feature is enabled. If you are using
|
||||
/// this with a web framework, this generally means that effects **do not run on the server**.
|
||||
/// and you can call browser-specific APIs within the effect function without causing issues.
|
||||
/// If you need an effect to run on the server, use [`ImmediateEffect::new_isomorphic`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ImmediateEffect {
|
||||
inner: StoredEffect,
|
||||
}
|
||||
|
||||
type StoredEffect = Option<Arc<RwLock<inner::EffectInner>>>;
|
||||
|
||||
impl Dispose for ImmediateEffect {
|
||||
fn dispose(self) {}
|
||||
}
|
||||
|
||||
impl ImmediateEffect {
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: this requires a `Fn` function because it might recurse.
|
||||
/// Use [Self::new_mut] to pass a `FnMut` function, it'll panic on recursion.
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new(fun: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
if !cfg!(feature = "effects") {
|
||||
return Self { inner: None };
|
||||
}
|
||||
|
||||
let inner = inner::EffectInner::new(fun);
|
||||
|
||||
inner.update_if_necessary();
|
||||
|
||||
Self { inner: Some(inner) }
|
||||
}
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics on recursion or if triggered in parallel. Also see [Self::new]
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new_mut(fun: impl FnMut() + Send + Sync + 'static) -> Self {
|
||||
const MSG: &str = "The effect recursed or its function panicked.";
|
||||
let fun = Mutex::new(fun);
|
||||
Self::new(move || fun.try_lock().expect(MSG)())
|
||||
}
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// NOTE: this requires a `Fn` function because it might recurse.
|
||||
/// NOTE: this effect is automatically cleaned up when the current owner is cleared or disposed.
|
||||
#[track_caller]
|
||||
pub fn new_scoped(fun: impl Fn() + Send + Sync + 'static) {
|
||||
let effect = Self::new(fun);
|
||||
|
||||
on_cleanup(move || effect.dispose());
|
||||
}
|
||||
|
||||
/// Creates a new effect which runs immediately, then again as soon as any tracked signal changes.
|
||||
///
|
||||
/// This will run whether the `effects` feature is enabled or not.
|
||||
#[track_caller]
|
||||
#[must_use]
|
||||
pub fn new_isomorphic(fun: impl Fn() + Send + Sync + 'static) -> Self {
|
||||
let inner = inner::EffectInner::new(fun);
|
||||
|
||||
inner.update_if_necessary();
|
||||
|
||||
Self { inner: Some(inner) }
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAnySubscriber for ImmediateEffect {
|
||||
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||
const MSG: &str = "tried to set effect that has been stopped";
|
||||
self.inner.as_ref().expect(MSG).to_any_subscriber()
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinedAt for ImmediateEffect {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
self.inner.as_ref()?.read().or_poisoned().defined_at()
|
||||
}
|
||||
}
|
||||
|
||||
mod inner {
|
||||
use crate::{
|
||||
graph::{
|
||||
AnySource, AnySubscriber, ReactiveNode, ReactiveNodeState,
|
||||
SourceSet, Subscriber, ToAnySubscriber, WithObserver,
|
||||
},
|
||||
log_warning,
|
||||
owner::Owner,
|
||||
traits::DefinedAt,
|
||||
};
|
||||
use or_poisoned::OrPoisoned;
|
||||
use std::{
|
||||
panic::Location,
|
||||
sync::{Arc, RwLock, Weak},
|
||||
thread::{self, ThreadId},
|
||||
};
|
||||
|
||||
/// Handles subscription logic for effects.
|
||||
///
|
||||
/// To handle parallelism and recursion we assign ordered (1..) ids to each run.
|
||||
/// We only keep the sources tracked by the run with the highest id (the last one).
|
||||
///
|
||||
/// We do this by:
|
||||
/// - Clearing the sources before every run, so the last one clears anything before it.
|
||||
/// - We stop tracking sources after the last run has completed.
|
||||
/// (A parent run will start before and end after a recursive child run.)
|
||||
/// - To handle parallelism with the last run, we only allow sources to be added by its thread.
|
||||
pub(super) struct EffectInner {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: &'static Location<'static>,
|
||||
owner: Owner,
|
||||
state: ReactiveNodeState,
|
||||
/// The number of effect runs in this 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to assign ordered ids to each run, and to know when we can clear these values.
|
||||
run_count_start: usize,
|
||||
/// The number of effect runs that have completed in the current 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to know when we can clear these values.
|
||||
run_done_count: usize,
|
||||
/// Given ordered ids (1..), the run with the highest id that has completed in this 'batch'.
|
||||
/// Cleared when no runs are *ongoing* anymore.
|
||||
/// Used to know whether the current run is the latest one.
|
||||
run_done_max: usize,
|
||||
/// The [ThreadId] of the run with the highest id.
|
||||
/// Used to prevent over-subscribing during parallel execution with the last run.
|
||||
///
|
||||
/// ```text
|
||||
/// Thread 1:
|
||||
/// -------------------------
|
||||
/// --- --- =======
|
||||
///
|
||||
/// Thread 2:
|
||||
/// -------------------------
|
||||
/// -----------
|
||||
/// ```
|
||||
///
|
||||
/// In the parallel example above, we can see why we need this.
|
||||
/// The last run is marked using `=`, but another run in the other thread might
|
||||
/// also be gathering sources. So we only allow the run from the correct [ThreadId] to push sources.
|
||||
last_run_thread_id: ThreadId,
|
||||
fun: Arc<dyn Fn() + Send + Sync>,
|
||||
sources: SourceSet,
|
||||
any_subscriber: AnySubscriber,
|
||||
}
|
||||
|
||||
impl EffectInner {
|
||||
#[track_caller]
|
||||
pub fn new(
|
||||
fun: impl Fn() + Send + Sync + 'static,
|
||||
) -> Arc<RwLock<EffectInner>> {
|
||||
let owner = Owner::new();
|
||||
|
||||
Arc::new_cyclic(|weak| {
|
||||
let any_subscriber = AnySubscriber(
|
||||
weak.as_ptr() as usize,
|
||||
Weak::clone(weak) as Weak<dyn Subscriber + Send + Sync>,
|
||||
);
|
||||
|
||||
RwLock::new(EffectInner {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
defined_at: Location::caller(),
|
||||
owner,
|
||||
state: ReactiveNodeState::Dirty,
|
||||
run_count_start: 0,
|
||||
run_done_count: 0,
|
||||
run_done_max: 0,
|
||||
last_run_thread_id: thread::current().id(),
|
||||
fun: Arc::new(fun),
|
||||
sources: SourceSet::new(),
|
||||
any_subscriber,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToAnySubscriber for Arc<RwLock<EffectInner>> {
|
||||
fn to_any_subscriber(&self) -> AnySubscriber {
|
||||
AnySubscriber(
|
||||
Arc::as_ptr(self) as usize,
|
||||
Arc::downgrade(self) as Weak<dyn Subscriber + Send + Sync>,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReactiveNode for RwLock<EffectInner> {
|
||||
fn mark_subscribers_check(&self) {}
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let state = {
|
||||
let guard = self.read().or_poisoned();
|
||||
|
||||
if guard.owner.paused() {
|
||||
return false;
|
||||
}
|
||||
|
||||
guard.state
|
||||
};
|
||||
|
||||
let needs_update = match state {
|
||||
ReactiveNodeState::Clean => false,
|
||||
ReactiveNodeState::Check => {
|
||||
let sources = self.read().or_poisoned().sources.clone();
|
||||
sources
|
||||
.into_iter()
|
||||
.any(|source| source.update_if_necessary())
|
||||
}
|
||||
ReactiveNodeState::Dirty => true,
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
|
||||
let owner = guard.owner.clone();
|
||||
let any_subscriber = guard.any_subscriber.clone();
|
||||
let fun = guard.fun.clone();
|
||||
|
||||
// New run has started.
|
||||
guard.run_count_start += 1;
|
||||
// We get a value for this run, the highest value will be what we keep the sources from.
|
||||
let recursion_count = guard.run_count_start;
|
||||
// We clear the sources before running the effect.
|
||||
// Note that this is tied to the ordering of the initial write lock acquisition
|
||||
// to ensure the last run is also the last to clear them.
|
||||
guard.sources.clear_sources(&any_subscriber);
|
||||
// Only this thread will be able to subscribe.
|
||||
guard.last_run_thread_id = thread::current().id();
|
||||
|
||||
if recursion_count > 2 {
|
||||
warn_excessive_recursion(&guard);
|
||||
}
|
||||
|
||||
drop(guard);
|
||||
|
||||
// We execute the effect.
|
||||
// Note that *this could happen in parallel across threads*.
|
||||
owner.with_cleanup(|| any_subscriber.with_observer(|| fun()));
|
||||
|
||||
let mut guard = self.write().or_poisoned();
|
||||
|
||||
// This run has completed.
|
||||
guard.run_done_count += 1;
|
||||
|
||||
// We update the done count.
|
||||
// Sources will only be added if recursion_done_max < recursion_count_start.
|
||||
// (Meaning the last run is not done yet.)
|
||||
guard.run_done_max =
|
||||
Ord::max(recursion_count, guard.run_done_max);
|
||||
|
||||
// The same amount of runs has started and completed,
|
||||
// so we can clear everything up for next time.
|
||||
if guard.run_count_start == guard.run_done_count {
|
||||
guard.run_count_start = 0;
|
||||
guard.run_done_count = 0;
|
||||
guard.run_done_max = 0;
|
||||
// Can be left unchanged, it'll be set again next time.
|
||||
// guard.last_run_thread_id = thread::current().id();
|
||||
}
|
||||
|
||||
guard.state = ReactiveNodeState::Clean;
|
||||
}
|
||||
|
||||
needs_update
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
self.write().or_poisoned().state = ReactiveNodeState::Check;
|
||||
self.update_if_necessary();
|
||||
}
|
||||
|
||||
fn mark_dirty(&self) {
|
||||
self.write().or_poisoned().state = ReactiveNodeState::Dirty;
|
||||
self.update_if_necessary();
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber for RwLock<EffectInner> {
|
||||
fn add_source(&self, source: AnySource) {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
if guard.run_done_max < guard.run_count_start
|
||||
&& guard.last_run_thread_id == thread::current().id()
|
||||
{
|
||||
guard.sources.insert(source);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_sources(&self, subscriber: &AnySubscriber) {
|
||||
self.write().or_poisoned().sources.clear_sources(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
impl DefinedAt for EffectInner {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
{
|
||||
Some(self.defined_at)
|
||||
}
|
||||
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EffectInner {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("EffectInner")
|
||||
.field("owner", &self.owner)
|
||||
.field("state", &self.state)
|
||||
.field("sources", &self.sources)
|
||||
.field("any_subscriber", &self.any_subscriber)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn warn_excessive_recursion(effect: &EffectInner) {
|
||||
const MSG: &str = "ImmediateEffect recursed more than once.";
|
||||
match effect.defined_at() {
|
||||
Some(defined_at) => {
|
||||
log_warning(format_args!("{MSG} Defined at: {}", defined_at));
|
||||
}
|
||||
None => {
|
||||
log_warning(format_args!("{MSG}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,21 +30,19 @@ impl ReactiveNode for RwLock<EffectInner> {
|
||||
|
||||
fn update_if_necessary(&self) -> bool {
|
||||
let mut guard = self.write().or_poisoned();
|
||||
let (is_dirty, sources) =
|
||||
(guard.dirty, (!guard.dirty).then(|| guard.sources.clone()));
|
||||
|
||||
if is_dirty {
|
||||
if guard.dirty {
|
||||
guard.dirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
let sources = guard.sources.clone();
|
||||
|
||||
drop(guard);
|
||||
for source in sources.into_iter().flatten() {
|
||||
if source.update_if_necessary() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
|
||||
sources
|
||||
.into_iter()
|
||||
.any(|source| source.update_if_necessary())
|
||||
}
|
||||
|
||||
fn mark_check(&self) {
|
||||
|
||||
@@ -37,8 +37,10 @@ impl AsyncTransition {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let global_transition = global_transition();
|
||||
let inner = TransitionInner { tx };
|
||||
let prev =
|
||||
(*global_transition.write().or_poisoned()).replace(inner.clone());
|
||||
let prev = Option::replace(
|
||||
&mut *global_transition.write().or_poisoned(),
|
||||
inner.clone(),
|
||||
);
|
||||
let value = action().await;
|
||||
_ = std::mem::replace(
|
||||
&mut *global_transition.write().or_poisoned(),
|
||||
|
||||
227
reactive_graph/tests/effect_immediate.rs
Normal file
227
reactive_graph/tests/effect_immediate.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
#[cfg(feature = "effects")]
|
||||
pub mod imports {
|
||||
pub use any_spawner::Executor;
|
||||
pub use reactive_graph::{
|
||||
effect::ImmediateEffect, owner::Owner, prelude::*, signal::RwSignal,
|
||||
};
|
||||
pub use std::sync::{Arc, RwLock};
|
||||
pub use tokio::task;
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn effect_runs() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let a = RwSignal::new(-1);
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let b = Arc::new(RwLock::new(String::new()));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let b = b.clone();
|
||||
move || {
|
||||
let formatted = format!("Value is {}", a.get());
|
||||
*b.write().unwrap() = formatted;
|
||||
}
|
||||
});
|
||||
assert_eq!(b.read().unwrap().as_str(), "Value is -1");
|
||||
|
||||
println!("setting to 1");
|
||||
a.set(1);
|
||||
assert_eq!(b.read().unwrap().as_str(), "Value is 1");
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn dynamic_dependencies() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let first = RwSignal::new("Greg");
|
||||
let last = RwSignal::new("Johnston");
|
||||
let use_last = RwSignal::new(true);
|
||||
|
||||
let combined_count = Arc::new(RwLock::new(0));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let combined_count = Arc::clone(&combined_count);
|
||||
move || {
|
||||
*combined_count.write().unwrap() += 1;
|
||||
if use_last.get() {
|
||||
println!("{} {}", first.get(), last.get());
|
||||
} else {
|
||||
println!("{}", first.get());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(*combined_count.read().unwrap(), 1);
|
||||
|
||||
println!("\nsetting `first` to Bob");
|
||||
first.set("Bob");
|
||||
assert_eq!(*combined_count.read().unwrap(), 2);
|
||||
|
||||
println!("\nsetting `last` to Bob");
|
||||
last.set("Thompson");
|
||||
assert_eq!(*combined_count.read().unwrap(), 3);
|
||||
|
||||
println!("\nsetting `use_last` to false");
|
||||
use_last.set(false);
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Jones");
|
||||
last.set("Jones");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Jones");
|
||||
last.set("Smith");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `last` to Stevens");
|
||||
last.set("Stevens");
|
||||
assert_eq!(*combined_count.read().unwrap(), 4);
|
||||
|
||||
println!("\nsetting `use_last` to true");
|
||||
use_last.set(true);
|
||||
assert_eq!(*combined_count.read().unwrap(), 5);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn recursive_effect_runs_recursively() {
|
||||
use imports::*;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let s = RwSignal::new(0);
|
||||
|
||||
let logged_values = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let logged_values = Arc::clone(&logged_values);
|
||||
move || {
|
||||
let a = s.get();
|
||||
println!("a = {a}");
|
||||
logged_values.write().unwrap().push(a);
|
||||
if a == 0 {
|
||||
return;
|
||||
}
|
||||
s.set(0);
|
||||
}
|
||||
});
|
||||
|
||||
s.set(1);
|
||||
s.set(2);
|
||||
s.set(3);
|
||||
|
||||
assert_eq!(0, s.get_untracked());
|
||||
assert_eq!(&*logged_values.read().unwrap(), &[0, 1, 0, 2, 0, 3, 0]);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
fn paused_effect_pauses() {
|
||||
use imports::*;
|
||||
use reactive_graph::owner::StoredValue;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let a = RwSignal::new(-1);
|
||||
|
||||
// simulate an arbitrary side effect
|
||||
let runs = StoredValue::new(0);
|
||||
|
||||
let owner = StoredValue::new(None);
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
move || {
|
||||
*owner.write_value() = Owner::current();
|
||||
|
||||
let _ = a.get();
|
||||
*runs.write_value() += 1;
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(runs.get_value(), 1);
|
||||
|
||||
println!("setting to 1");
|
||||
a.set(1);
|
||||
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("pausing");
|
||||
owner.get_value().unwrap().pause();
|
||||
|
||||
println!("setting to 2");
|
||||
a.set(2);
|
||||
|
||||
assert_eq!(runs.get_value(), 2);
|
||||
|
||||
println!("resuming");
|
||||
owner.get_value().unwrap().resume();
|
||||
|
||||
println!("setting to 3");
|
||||
a.set(3);
|
||||
|
||||
println!("checking value");
|
||||
assert_eq!(runs.get_value(), 3);
|
||||
}
|
||||
|
||||
#[cfg(feature = "effects")]
|
||||
#[test]
|
||||
#[ignore = "Parallel signal access can panic."]
|
||||
fn threaded_chaos_effect() {
|
||||
use imports::*;
|
||||
use reactive_graph::owner::StoredValue;
|
||||
|
||||
const SIGNAL_COUNT: usize = 5;
|
||||
const THREAD_COUNT: usize = 10;
|
||||
|
||||
let owner = Owner::new();
|
||||
owner.set();
|
||||
|
||||
let signals = vec![RwSignal::new(0); SIGNAL_COUNT];
|
||||
|
||||
let runs = StoredValue::new(0);
|
||||
|
||||
let _guard = ImmediateEffect::new({
|
||||
let signals = signals.clone();
|
||||
move || {
|
||||
*runs.write_value() += 1;
|
||||
|
||||
let mut values = vec![];
|
||||
for s in &signals {
|
||||
let v = s.get();
|
||||
values.push(v);
|
||||
if v != 0 {
|
||||
s.set(v - 1);
|
||||
}
|
||||
}
|
||||
println!("{values:?}");
|
||||
}
|
||||
});
|
||||
|
||||
std::thread::scope(|s| {
|
||||
for _ in 0..THREAD_COUNT {
|
||||
let signals = signals.clone();
|
||||
s.spawn(move || {
|
||||
for s in &signals {
|
||||
s.set(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(runs.get_value(), 1 + THREAD_COUNT * SIGNAL_COUNT);
|
||||
|
||||
let values: Vec<_> = signals.iter().map(|s| s.get_untracked()).collect();
|
||||
println!("FINAL: {values:?}");
|
||||
}
|
||||
@@ -38,6 +38,20 @@ where
|
||||
track_field: Arc<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T> Debug for ArcField<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_struct("ArcField");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("path", &self.path)
|
||||
.field("trigger", &self.trigger)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StoreFieldReader<T>(Box<dyn Deref<Target = T>>);
|
||||
|
||||
impl<T> StoreFieldReader<T> {
|
||||
|
||||
@@ -31,6 +31,19 @@ where
|
||||
inner: ArenaItem<ArcField<T>, S>,
|
||||
}
|
||||
|
||||
impl<T, S> Debug for Field<T, S>
|
||||
where
|
||||
T: 'static,
|
||||
S: Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_struct("Field");
|
||||
#[cfg(any(debug_assertions, leptos_debuginfo))]
|
||||
let f = f.field("defined_at", &self.defined_at);
|
||||
f.field("inner", &self.inner).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> StoreField for Field<T, S>
|
||||
where
|
||||
S: Storage<ArcField<T>>,
|
||||
|
||||
@@ -79,7 +79,7 @@ impl Parse for Model {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum SubfieldMode {
|
||||
Keyed(ExprClosure, Type),
|
||||
Keyed(ExprClosure, Box<Type>),
|
||||
Skip,
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ impl Parse for SubfieldMode {
|
||||
let ty: Type = input.parse()?;
|
||||
let _eq: Token![=] = input.parse()?;
|
||||
let closure: ExprClosure = input.parse()?;
|
||||
Ok(SubfieldMode::Keyed(closure, ty))
|
||||
Ok(SubfieldMode::Keyed(closure, Box::new(ty)))
|
||||
} else if mode == "skip" {
|
||||
Ok(SubfieldMode::Skip)
|
||||
} else {
|
||||
|
||||
@@ -91,7 +91,9 @@ impl Url {
|
||||
path.push_str(&self.search);
|
||||
}
|
||||
if !self.hash.is_empty() {
|
||||
path.push('#');
|
||||
if !self.hash.starts_with('#') {
|
||||
path.push('#');
|
||||
}
|
||||
path.push_str(&self.hash);
|
||||
}
|
||||
path
|
||||
|
||||
@@ -62,7 +62,7 @@ impl PossibleRouteMatch for ParamSegment {
|
||||
}
|
||||
}
|
||||
|
||||
if matched_len == 0 {
|
||||
if matched_len == 0 || (matched_len == 1 && path.starts_with('/')) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
type OutletViewFn = Box<dyn Fn() -> Suspend<AnyView> + Send>;
|
||||
type OutletViewFn = Box<dyn FnMut() -> Suspend<AnyView> + Send>;
|
||||
|
||||
pub(crate) struct RouteContext {
|
||||
id: RouteMatchId,
|
||||
@@ -774,8 +774,8 @@ where
|
||||
|
||||
// assign a new owner, so that contexts and signals owned by the previous route
|
||||
// in this outlet can be dropped
|
||||
let old_owner =
|
||||
mem::replace(&mut current.owner, parent.child());
|
||||
let mut old_owner =
|
||||
Some(mem::replace(&mut current.owner, parent.child()));
|
||||
let owner = current.owner.clone();
|
||||
let (full_tx, full_rx) = oneshot::channel();
|
||||
let full_tx = Mutex::new(Some(full_tx));
|
||||
@@ -802,6 +802,7 @@ where
|
||||
let view = view.clone();
|
||||
let full_tx =
|
||||
full_tx.lock().or_poisoned().take();
|
||||
let old_owner = old_owner.take();
|
||||
Suspend::new(Box::pin(async move {
|
||||
let view = SendWrapper::new(
|
||||
owner.with(|| {
|
||||
@@ -817,6 +818,10 @@ where
|
||||
}),
|
||||
);
|
||||
let view = view.await;
|
||||
if let Some(old_owner) = old_owner {
|
||||
old_owner.cleanup();
|
||||
}
|
||||
|
||||
if let Some(tx) = full_tx {
|
||||
_ = tx.send(());
|
||||
}
|
||||
@@ -825,7 +830,7 @@ where
|
||||
})
|
||||
}))
|
||||
});
|
||||
drop(old_owner);
|
||||
|
||||
drop(old_params);
|
||||
drop(old_url);
|
||||
drop(old_matched);
|
||||
@@ -913,7 +918,7 @@ where
|
||||
trigger, view_fn, ..
|
||||
} = ctx;
|
||||
trigger.track();
|
||||
let view_fn = view_fn.lock().or_poisoned();
|
||||
let mut view_fn = view_fn.lock().or_poisoned();
|
||||
view_fn()
|
||||
}
|
||||
}
|
||||
|
||||
45
scripts/update_nightly.sh
Executable file
45
scripts/update_nightly.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
exit_error() {
|
||||
echo "ERROR: $1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
current_dir=$(pwd)
|
||||
|
||||
if [[ "$current_dir" != */leptos ]]; then
|
||||
exit_error "Current directory does not end with leptos"
|
||||
fi
|
||||
|
||||
# Check if a date is provided as an argument
|
||||
if [ $# -eq 1 ]; then
|
||||
NEW_DATE=$1
|
||||
echo -n "Will use the provided date: "
|
||||
else
|
||||
# Use current date if no date is provided
|
||||
NEW_DATE=$(date +"%Y-%m-%d")
|
||||
echo -n "Will use the current date: "
|
||||
fi
|
||||
|
||||
echo "$NEW_DATE"
|
||||
|
||||
# Detect if it is darwin sed
|
||||
if sed --version 2>/dev/null | grep -q GNU; then
|
||||
SED_COMMAND="sed -i"
|
||||
else
|
||||
SED_COMMAND='sed -i ""'
|
||||
fi
|
||||
|
||||
MATCH="nightly-[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
|
||||
REPLACE="nightly-$NEW_DATE"
|
||||
|
||||
# Find all occurrences of the pattern
|
||||
git grep -l "$MATCH" | while read -r file; do
|
||||
# Replace the pattern in each file
|
||||
$SED_COMMAND "s/$MATCH/$REPLACE/g" "$file"
|
||||
echo "Updated $file"
|
||||
done
|
||||
|
||||
echo "All occurrences of 'nightly-XXXX-XX-XX' have been replaced with 'nightly-$NEW_DATE'"
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::html::{element::ElementType, node_ref::NodeRefContainer};
|
||||
use reactive_graph::{
|
||||
effect::Effect,
|
||||
graph::untrack,
|
||||
signal::{
|
||||
guards::{Derefable, ReadGuard},
|
||||
RwSignal,
|
||||
@@ -46,7 +47,10 @@ where
|
||||
|
||||
Effect::new(move |_| {
|
||||
if let Some(node_ref) = self.get() {
|
||||
f.take().unwrap()(node_ref);
|
||||
let f = f.take().unwrap();
|
||||
untrack(move || {
|
||||
f(node_ref);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user