trz_gateway_common/
dynamic_config.rs

1//! Dynamic configuration.
2
3use std::collections::HashMap;
4use std::collections::hash_map;
5use std::convert::Infallible;
6use std::fmt::Debug;
7use std::marker::PhantomData;
8use std::sync::Arc;
9use std::sync::RwLock;
10
11use serde::Deserialize;
12use serde::Deserializer;
13use serde::Serialize;
14
15use self::has_diff::HasDiff;
16use crate::is_global::IsGlobal;
17use crate::unwrap_infallible::UnwrapInfallible as _;
18
19/// A struct that contains a piece of configuration that can change.
20#[derive(Default)]
21pub struct DynamicConfig<T, M = mode::RW> {
22    config: std::sync::RwLock<Option<T>>,
23    notify: Arc<Registry<Box<dyn Fn(&T) + Send + Sync + 'static>>>,
24    on_drop: std::sync::Mutex<Vec<Box<dyn FnOnce() + Send + Sync + 'static>>>,
25    _mode: PhantomData<M>,
26}
27
28pub mod mode {
29    pub use super::mode_impl::Mode;
30    pub use super::mode_impl::RO;
31    pub use super::mode_impl::RW;
32}
33
34mod mode_impl {
35    use crate::is_global::IsGlobal;
36
37    pub trait Mode: IsGlobal {
38        fn mode() -> ModeImpl;
39    }
40
41    /// Marker for dynamic configurations that can be modified.
42    pub enum RW {}
43
44    /// Marker for dynamic configurations that are readonly.
45    /// Such configuration can change only if the source they are derived from changes as well.
46    pub enum RO {}
47
48    #[derive(Debug, PartialEq, Eq)]
49    pub enum ModeImpl {
50        RW,
51        RO,
52    }
53
54    impl Mode for RW {
55        fn mode() -> ModeImpl {
56            ModeImpl::RW
57        }
58    }
59    impl Mode for RO {
60        fn mode() -> ModeImpl {
61            ModeImpl::RO
62        }
63    }
64}
65
66impl<T> From<T> for DynamicConfig<T, mode::RW> {
67    fn from(config: T) -> Self {
68        from_impl(config)
69    }
70}
71
72fn from_impl<T, M>(config: T) -> DynamicConfig<T, M>
73where
74    M: mode::Mode,
75{
76    DynamicConfig {
77        config: RwLock::new(Some(config)),
78        notify: Default::default(),
79        on_drop: Default::default(),
80        _mode: PhantomData,
81    }
82}
83
84impl<T> DynamicConfig<T, mode::RW> {
85    /// Registers a callback to be notified when the configuration changes.
86    ///
87    /// Prefer to use [derive()](DynamicConfig::derive) or [view()](DynamicConfig::view).
88    pub fn add_notify(
89        &self,
90        f: impl Fn(&T) + IsGlobal,
91    ) -> RegistryHandle<Box<dyn Fn(&T) + Send + Sync + 'static>> {
92        add_notify(self, f)
93    }
94}
95
96fn add_notify<T>(
97    this: &DynamicConfig<T, impl mode::Mode>,
98    f: impl Fn(&T) + IsGlobal,
99) -> RegistryHandle<Box<dyn Fn(&T) + Send + Sync + 'static>> {
100    this.notify.add(Box::new(f))
101}
102
103impl<T, M> DynamicConfig<T, M>
104where
105    M: mode::Mode,
106{
107    /// Returns the current value.
108    ///
109    /// Note: the returned value is not dynamic.
110    pub fn get(&self) -> T
111    where
112        T: Clone,
113    {
114        self.with(T::clone)
115    }
116
117    /// Computes a result based on the current configuration value.
118    ///
119    /// Useful when the configuration is not [Copy] and [get()](DynamicConfig::get) cannot be used.
120    pub fn with<R>(&self, f: impl FnOnce(&T) -> R) -> R {
121        f(self
122            .config
123            .read()
124            .expect("read lock")
125            .as_ref()
126            .expect("option not present"))
127    }
128
129    /// Returns a derived [mode::RO] dynamic configuration.
130    ///
131    /// The derived configuration is updated only if the value actually changes.
132    pub fn view_diff<U>(
133        self: &Arc<Self>,
134        to: impl Fn(&T) -> U + IsGlobal,
135    ) -> Arc<DynamicConfig<U, mode::RO>>
136    where
137        T: Clone + IsGlobal,
138        U: Clone + IsGlobal + HasDiff,
139    {
140        derive_impl(self.clone(), to, |_, _| Option::<T>::None, U::is_same)
141    }
142
143    /// Returns a derived [mode::RO] dynamic configuration.
144    ///
145    /// Use [view_diff()](DynamicConfig::view_diff) so the derived config doesn't notify unless the value actually changes.
146    pub fn view<U>(
147        self: &Arc<Self>,
148        to: impl Fn(&T) -> U + IsGlobal,
149    ) -> Arc<DynamicConfig<U, mode::RO>>
150    where
151        T: Clone + IsGlobal,
152        U: Clone + IsGlobal,
153    {
154        derive_impl(self.clone(), to, |_, _| Option::<T>::None, always_changed)
155    }
156
157    pub fn zip<T2, M2>(
158        self: &Arc<Self>,
159        right: &Arc<DynamicConfig<T2, M2>>,
160    ) -> Arc<DynamicConfig<(T, T2), mode::RO>>
161    where
162        T: Clone + IsGlobal,
163        T2: Clone + IsGlobal,
164        M2: mode::Mode,
165    {
166        let left = self;
167        let zipped: Arc<DynamicConfig<(T, T2), mode::RO>> =
168            Arc::new(from_impl((left.get(), right.get())));
169
170        {
171            let on_left_change = add_notify(left, {
172                let zipped_weak = Arc::downgrade(&zipped);
173                move |new_left| {
174                    if let Some(zipped_strong) = zipped_weak.upgrade() {
175                        zipped_strong
176                            .try_set_impl(
177                                |(_, right)| Ok((new_left.clone(), right.clone())),
178                                always_changed,
179                            )
180                            .unwrap_infallible();
181                    }
182                }
183            });
184            let left = left.clone();
185            zipped.on_drop.lock().unwrap().push(Box::new(move || {
186                drop(on_left_change);
187                drop(left);
188            }));
189        }
190        {
191            let on_right_change = add_notify(right, {
192                let zipped_weak = Arc::downgrade(&zipped);
193                move |new_right| {
194                    if let Some(zipped_strong) = zipped_weak.upgrade() {
195                        zipped_strong
196                            .try_set_impl(
197                                |(left, _)| Ok((left.clone(), new_right.clone())),
198                                always_changed,
199                            )
200                            .unwrap_infallible();
201                    }
202                }
203            });
204            let right = right.clone();
205            zipped.on_drop.lock().unwrap().push(Box::new(move || {
206                drop(on_right_change);
207                drop(right);
208            }));
209        }
210
211        return zipped;
212    }
213}
214
215impl<T> DynamicConfig<T, mode::RW> {
216    /// Modifies the current configuration.
217    ///
218    /// This will trigger all handlers registered with [add_notify()](DynamicConfig::add_notify), such as views or derived configurations.
219    pub fn set(&self, make_new_config: impl FnOnce(&T) -> T)
220    where
221        T: Clone,
222    {
223        self.try_set(|t| Ok::<_, Infallible>(make_new_config(t)))
224            .unwrap_infallible()
225    }
226
227    /// Modifies the current configuration.
228    ///
229    /// Same as [set()](DynamicConfig::set) but accepts a mutation that could return an error.
230    pub fn try_set<E>(&self, make_new_config: impl FnOnce(&T) -> Result<T, E>) -> Result<(), E>
231    where
232        T: Clone,
233    {
234        self.try_set_impl(make_new_config, always_changed)
235    }
236
237    /// Modifies the current configuration.
238    ///
239    /// Same as [set()](DynamicConfig::set) but will not trigger notify.
240    pub fn silent_set(&self, make_new_config: impl FnOnce(&T) -> T)
241    where
242        T: Clone,
243    {
244        self.silent_try_set(|t| Ok::<_, Infallible>(make_new_config(t)))
245            .unwrap_infallible()
246    }
247
248    /// Modifies the current configuration.
249    ///
250    /// Does not trigger notify and accepts a mutation that could return an error.
251    pub fn silent_try_set<E>(
252        &self,
253        make_new_config: impl FnOnce(&T) -> Result<T, E>,
254    ) -> Result<(), E>
255    where
256        T: Clone,
257    {
258        {
259            let mut lock = self.config.write().expect("write lock");
260            let new_config = make_new_config(lock.as_ref().expect("option not present"))?;
261            *lock = Some(new_config);
262        }
263
264        #[cfg(debug_assertions)]
265        let _ = self.get();
266
267        Ok(())
268    }
269
270    /// Returns a derived piece of configuration.
271    ///
272    /// Unlike [view()](DynamicConfig::view), changes made to the derived configuration are
273    /// applied back to the original configuration.
274    pub fn derive<U>(
275        self: &Arc<Self>,
276        to: impl Fn(&T) -> U + IsGlobal,
277        from: impl Fn(T, &U) -> Option<T> + IsGlobal,
278    ) -> Arc<DynamicConfig<U, mode::RW>>
279    where
280        T: Clone + IsGlobal,
281        U: Clone + IsGlobal + HasDiff,
282    {
283        derive_impl(self.clone(), to, from, U::is_same)
284    }
285}
286
287fn derive_impl<T, U, MU>(
288    main: Arc<DynamicConfig<T, impl mode::Mode>>,
289    to: impl Fn(&T) -> U + IsGlobal,
290    from: impl Fn(T, &U) -> Option<T> + IsGlobal,
291    is_same: impl FnOnce(&U, &U) -> bool + IsGlobal + Copy,
292) -> Arc<DynamicConfig<U, MU>>
293where
294    T: Clone + IsGlobal,
295    U: Clone + IsGlobal,
296    MU: mode::Mode,
297{
298    let derived: Arc<DynamicConfig<U, MU>> = Arc::new(from_impl(main.with(|m| to(m))));
299    if MU::mode() == mode_impl::ModeImpl::RW {
300        let on_derived_change = add_notify(&derived, {
301            let main_weak = Arc::downgrade(&main);
302            move |d| {
303                if let Some(main) = main_weak.upgrade()
304                    && let Some(m) = from(main.get(), d)
305                {
306                    main.try_set_impl(|_| Ok(m), always_changed)
307                        .unwrap_infallible();
308                }
309            }
310        });
311        main.on_drop
312            .lock()
313            .unwrap()
314            .push(Box::new(move || drop(on_derived_change)));
315    }
316    let on_main_change = add_notify(&main, {
317        let derived_weak = Arc::downgrade(&derived);
318        move |m| {
319            if let Some(derived) = derived_weak.upgrade() {
320                derived
321                    .try_set_impl(|_old| Ok(to(m)), is_same)
322                    .unwrap_infallible()
323            }
324        }
325    });
326    derived.on_drop.lock().unwrap().push(Box::new(move || {
327        drop(on_main_change);
328        drop(main);
329    }));
330    return derived;
331}
332
333impl<T> DynamicConfig<T> {
334    pub fn if_change<U>(
335        from: impl Fn(&T, &U) -> T + 'static,
336    ) -> impl Fn(T, &U) -> Option<T> + 'static
337    where
338        T: Eq,
339    {
340        move |old_t, u| {
341            let new_t = from(&old_t, u);
342            if new_t != old_t { Some(new_t) } else { None }
343        }
344    }
345
346    pub fn if_ptr_change<U>(
347        from: impl Fn(&Arc<T>, &U) -> Arc<T> + 'static,
348    ) -> impl Fn(Arc<T>, &U) -> Option<Arc<T>> + 'static {
349        move |old_t, u| {
350            let new_t = from(&old_t, u);
351            if !Arc::ptr_eq(&new_t, &old_t) {
352                Some(new_t)
353            } else {
354                None
355            }
356        }
357    }
358}
359
360impl<T, M> DynamicConfig<T, M>
361where
362    M: mode::Mode,
363{
364    fn try_set_impl<E>(
365        &self,
366        make_new_config: impl FnOnce(&T) -> Result<T, E>,
367        is_same: impl FnOnce(&T, &T) -> bool,
368    ) -> Result<(), E>
369    where
370        T: Clone,
371    {
372        let new_config;
373        {
374            let mut lock = self.config.write().expect("write lock");
375            let old_config = lock.as_ref().expect("option not present");
376            new_config = make_new_config(old_config)?;
377            if is_same(old_config, &new_config) {
378                return Ok(());
379            }
380            *lock = Some(new_config.clone());
381        }
382
383        #[cfg(debug_assertions)]
384        let _ = self.get();
385
386        self.notify.with(|notify| {
387            for notify in notify {
388                notify(&new_config);
389            }
390        });
391        Ok(())
392    }
393}
394
395impl<T: Debug, M: mode::Mode> std::fmt::Debug for DynamicConfig<T, M> {
396    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397        self.with(|config| {
398            f.debug_struct("DynamicConfig")
399                .field("config", config)
400                .finish()
401        })
402    }
403}
404
405impl<T: Serialize> Serialize for DynamicConfig<T> {
406    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
407    where
408        S: serde::Serializer,
409    {
410        self.with(|v| v.serialize(serializer))
411    }
412}
413
414impl<'de, T: Deserialize<'de>> Deserialize<'de> for DynamicConfig<T> {
415    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
416    where
417        D: Deserializer<'de>,
418    {
419        Ok(T::deserialize(deserializer)?.into())
420    }
421}
422
423struct Registry<T>(std::sync::RwLock<RegistryInner<T>>);
424
425struct RegistryInner<T> {
426    next: i32,
427    table: HashMap<i32, T>,
428}
429
430#[must_use]
431pub struct RegistryHandle<T> {
432    registry: Arc<Registry<T>>,
433    key: i32,
434}
435
436impl<T> Registry<T> {
437    fn add(self: &Arc<Self>, value: T) -> RegistryHandle<T> {
438        let mut lock = self.write();
439        let RegistryInner { next, table } = &mut *lock;
440        *next += 1;
441        let prev = table.insert(*next, value);
442        assert!(prev.is_none());
443        return RegistryHandle {
444            registry: self.clone(),
445            key: *next,
446        };
447    }
448
449    fn read(&self) -> std::sync::RwLockReadGuard<'_, RegistryInner<T>> {
450        self.0.read().expect("registry")
451    }
452
453    fn write(&self) -> std::sync::RwLockWriteGuard<'_, RegistryInner<T>> {
454        self.0.write().expect("registry")
455    }
456}
457
458impl<T> Registry<T> {
459    pub fn with<R>(&self, f: impl Fn(hash_map::Values<'_, i32, T>) -> R) -> R {
460        f(self.read().table.values())
461    }
462}
463
464impl<T> Default for Registry<T> {
465    fn default() -> Self {
466        Self(RwLock::new(RegistryInner {
467            next: 0,
468            table: HashMap::new(),
469        }))
470    }
471}
472
473impl<T> Drop for RegistryHandle<T> {
474    fn drop(&mut self) {
475        let mut lock = self.registry.write();
476        let removed = lock.table.remove(&self.key);
477        debug_assert!(removed.is_some());
478    }
479}
480
481pub mod has_diff {
482    use std::ops::Deref;
483    use std::sync::Arc;
484
485    use serde::Deserialize;
486    use serde::Serialize;
487
488    pub trait HasDiff {
489        #[expect(unused)]
490        fn is_same(lhs: &Self, rhs: &Self) -> bool {
491            false
492        }
493
494        fn is_diff(lhs: &Self, rhs: &Self) -> bool {
495            !Self::is_same(lhs, rhs)
496        }
497    }
498
499    impl<T: Eq> HasDiff for T {
500        fn is_same(lhs: &Self, rhs: &Self) -> bool {
501            PartialEq::eq(lhs, rhs)
502        }
503    }
504
505    #[derive(Default, Serialize, Deserialize)]
506    #[serde(transparent)]
507    pub struct DiffArc<T>(Arc<T>);
508
509    impl<T> HasDiff for DiffArc<T> {
510        fn is_same(lhs: &Self, rhs: &Self) -> bool {
511            Arc::ptr_eq(&lhs.0, &rhs.0)
512        }
513    }
514
515    impl<T> From<T> for DiffArc<T> {
516        fn from(value: T) -> Self {
517            Self(value.into())
518        }
519    }
520
521    impl<T> From<Arc<T>> for DiffArc<T> {
522        fn from(value: Arc<T>) -> Self {
523            Self(value)
524        }
525    }
526
527    impl<T> Deref for DiffArc<T> {
528        type Target = T;
529
530        fn deref(&self) -> &Self::Target {
531            &self.0
532        }
533    }
534
535    impl<T> Clone for DiffArc<T> {
536        fn clone(&self) -> Self {
537            Self(self.0.clone())
538        }
539    }
540
541    impl<T: std::fmt::Debug> std::fmt::Debug for DiffArc<T> {
542        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
543            std::fmt::Debug::fmt(&self.0, f)
544        }
545    }
546
547    #[derive(Clone, Serialize, Deserialize)]
548    #[serde(transparent)]
549    pub struct DiffOption<T>(Option<T>);
550
551    impl<T: HasDiff> HasDiff for DiffOption<T> {
552        fn is_same(lhs: &Self, rhs: &Self) -> bool {
553            match (&lhs.0, &rhs.0) {
554                (None, None) => true,
555                (None, Some(_)) => false,
556                (Some(_), None) => false,
557                (Some(lhs), Some(rhs)) => T::is_same(lhs, rhs),
558            }
559        }
560    }
561
562    impl<T> From<T> for DiffOption<T> {
563        fn from(value: T) -> Self {
564            Self(value.into())
565        }
566    }
567
568    impl<T> Default for DiffOption<T> {
569        fn default() -> Self {
570            Self(Option::default())
571        }
572    }
573
574    impl<T> From<Option<T>> for DiffOption<T> {
575        fn from(value: Option<T>) -> Self {
576            Self(value)
577        }
578    }
579
580    impl<T> Deref for DiffOption<T> {
581        type Target = Option<T>;
582
583        fn deref(&self) -> &Self::Target {
584            &self.0
585        }
586    }
587
588    impl<T: std::fmt::Debug> std::fmt::Debug for DiffOption<T> {
589        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590            std::fmt::Debug::fmt(&self.0, f)
591        }
592    }
593
594    #[derive(Clone, Default, Serialize, Deserialize)]
595    #[serde(transparent)]
596    pub struct DiffItem<T>(T);
597
598    impl<T> HasDiff for DiffItem<T> {}
599
600    impl<T> From<T> for DiffItem<T> {
601        fn from(value: T) -> Self {
602            Self(value)
603        }
604    }
605
606    impl<T> Deref for DiffItem<T> {
607        type Target = T;
608
609        fn deref(&self) -> &Self::Target {
610            &self.0
611        }
612    }
613
614    impl<T: std::fmt::Debug> std::fmt::Debug for DiffItem<T> {
615        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
616            std::fmt::Debug::fmt(&self.0, f)
617        }
618    }
619}
620
621fn always_changed<T>(_: &T, _: &T) -> bool {
622    false
623}
624
625#[cfg(test)]
626mod tests {
627    use std::sync::Arc;
628    use std::sync::Mutex;
629
630    use super::DynamicConfig;
631
632    #[test]
633    fn set() {
634        let cfg = DynamicConfig::from("hello".to_owned());
635        let () = cfg.set(|old| format!("{old} world"));
636        assert_eq!("hello world", cfg.get());
637    }
638
639    #[test]
640    fn try_set() {
641        let cfg = DynamicConfig::from("hello".to_owned());
642        let Ok(()): Result<(), ()> = cfg.try_set(|old| Ok(format!("{old} world"))) else {
643            panic!();
644        };
645        assert_eq!("hello world", cfg.get());
646
647        let Err(()): Result<(), ()> = cfg.try_set(|_| Err(())) else {
648            panic!();
649        };
650        assert_eq!("hello world", cfg.get());
651    }
652
653    #[test]
654    fn add_notify() {
655        let cfg = DynamicConfig::from("hello".to_owned());
656        let last = Arc::new(Mutex::new(None));
657        let last2 = last.clone();
658        let notify =
659            cfg.add_notify(move |current| *last2.lock().unwrap() = Some(current.to_owned()));
660
661        let () = cfg.set(|old| format!("{old} world"));
662        assert_eq!(Some("hello world"), last.lock().unwrap().as_deref());
663        assert_eq!("hello world", cfg.get());
664
665        let () = cfg.set(|old| format!("{old}!"));
666        assert_eq!(Some("hello world!"), last.lock().unwrap().as_deref());
667        assert_eq!("hello world!", cfg.get());
668
669        drop(notify);
670        let () = cfg.set(|old| format!("{old}!!"));
671        assert_eq!(Some("hello world!"), last.lock().unwrap().as_deref());
672        assert_eq!("hello world!!!", cfg.get());
673    }
674
675    #[test]
676    fn derive() {
677        let main = Arc::new(DynamicConfig::from("hello".to_owned()));
678        let derived = main.derive(
679            |main| Box::new(main.to_uppercase()),
680            |m, d| {
681                let new_main = d.to_lowercase();
682                if new_main != m { Some(new_main) } else { None }
683            },
684        );
685        assert_eq!("hello", main.get());
686        assert_eq!("HELLO", *derived.get());
687
688        derived.set(|_| Box::new("HELLO_WORLD".to_owned()));
689        assert_eq!("hello_world", main.get());
690        assert_eq!("HELLO_WORLD", *derived.get());
691
692        assert_eq!(1, main.notify.read().table.len());
693        assert_eq!(1, derived.notify.read().table.len());
694
695        drop(derived);
696        assert_eq!(0, main.notify.read().table.len());
697    }
698
699    #[test]
700    fn view() {
701        let main = Arc::new(DynamicConfig::from("hello".to_owned()));
702        let view = main.view(|main| Box::new(main.to_uppercase()));
703        assert_eq!("hello", main.get());
704        assert_eq!("HELLO", *view.get());
705
706        main.set(|_| "Hello World".to_owned());
707        assert_eq!("Hello World", main.get());
708        assert_eq!("HELLO WORLD", *view.get());
709    }
710
711    #[test]
712    fn derive_eq() {
713        let main = Arc::new(DynamicConfig::from("hello".to_owned()));
714        let derived = main.derive(
715            |main| Box::new(main.to_uppercase()),
716            DynamicConfig::if_change(|_m, d: &Box<String>| d.to_lowercase()),
717        );
718        assert_eq!("hello", main.get());
719        assert_eq!("HELLO", *derived.get());
720
721        derived.set(|_| Box::new("HELLO_WORLD".to_owned()));
722        assert_eq!("hello_world", main.get());
723        assert_eq!("HELLO_WORLD", *derived.get());
724    }
725
726    #[test]
727    fn derive_ptr() {
728        let main = Arc::new(DynamicConfig::from(Arc::new("hello".to_string())));
729        let derived = main.derive(
730            |main| Arc::new(main.to_uppercase()),
731            DynamicConfig::if_ptr_change(|m: &Arc<String>, _| m.clone()),
732        );
733        assert_eq!("hello", *main.get());
734        assert_eq!("HELLO", *derived.get());
735
736        derived.set(|_| Arc::new("HELLO_WORLD".to_owned()));
737        assert_eq!("hello", *main.get());
738        assert_eq!("HELLO_WORLD", *derived.get());
739
740        main.set(|_| Arc::new("hello2".to_owned()));
741        assert_eq!("hello2", *main.get());
742        assert_eq!("HELLO2", *derived.get());
743    }
744
745    #[test]
746    fn zip() {
747        let left = Arc::new(DynamicConfig::from("left"));
748        let right = Arc::new(DynamicConfig::from(22));
749        let zipped = left.zip(&right);
750        assert_eq!(("left", 22), zipped.get());
751
752        left.set(|_| "left2");
753        assert_eq!(("left2", 22), zipped.get());
754
755        right.set(|_| 33);
756        assert_eq!(("left2", 33), zipped.get());
757
758        let concat = zipped.view(|(left, right)| format!("left:{left} right:{right}"));
759        assert_eq!("left:left2 right:33", concat.get());
760
761        left.set(|_| "left3");
762        right.set(|i| i + 1);
763        assert_eq!("left:left3 right:34", concat.get());
764
765        drop(zipped);
766        left.set(|_| "left4");
767        right.set(|i| i + 1);
768        assert_eq!("left:left4 right:35", concat.get());
769
770        let other = Arc::new(DynamicConfig::from("other"));
771        let zipped = concat.zip(&other.view(|other| other.to_uppercase()));
772        assert_eq!(("left:left4 right:35".into(), "OTHER".into()), zipped.get());
773
774        right.set(|_| 40);
775        assert_eq!(("left:left4 right:40".into(), "OTHER".into()), zipped.get());
776
777        other.set(|_| "update");
778        drop(concat);
779        drop(other);
780        right.set(|_| 41);
781        assert_eq!(
782            ("left:left4 right:41".into(), "UPDATE".into()),
783            zipped.get()
784        );
785    }
786}