Skip to content

Commit 6b36158

Browse files
authored
release 0.4.1, fix double emit for initially empty reactive collections (#18)
1 parent 15d0807 commit 6b36158

File tree

5 files changed

+244
-32
lines changed

5 files changed

+244
-32
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ the format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
44

55
## unreleased
66

7+
# 0.4.1 (2025-11-27)
8+
9+
### fixed
10+
11+
- initially empty `MutableVec`s and `MutableBTreeMap`s won't double emit their first additions
12+
13+
# 0.3.1 (2025-11-27)
14+
15+
### fixed
16+
17+
- initially empty `MutableVec`s and `MutableBTreeMap`s won't double emit their first additions
18+
719
# 0.4.0 (2025-11-20)
820

921
### changed

Cargo.lock

Lines changed: 26 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "jonmo"
3-
version = "0.4.0"
3+
version = "0.4.1"
44
edition = "2024"
55
categories = ["asynchronous", "gui", "game-development"]
66
description = "ergonomic Bevy-native reactivity powered by FRP signals"

src/signal_map.rs

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,14 +1121,18 @@ impl<K, V> MutableBTreeMap<K, V> {
11211121
let replay_lazy_signal = LazySignal::new(clone!((self => self_) move |world: &mut World| {
11221122
let broadcaster_system = world.get::<MutableBTreeMapData<K, V>>(self_.entity).unwrap().broadcaster.clone().register(world);
11231123

1124+
let was_initially_empty = self_.read(&*world).is_empty();
1125+
11241126
let replay_entity = LazyEntity::new();
11251127
let replay_system = clone!((self_, replay_entity) move |In(upstream_diffs): In<Vec<MapDiff<K, V>>>, replay_onces: Query<&ReplayOnce, Allow<Internal>>, mutable_btree_map_datas: Query<&MutableBTreeMapData<K, V>>| {
11261128
if replay_onces.contains(*replay_entity) {
1127-
let initial_map = self_.read(&mutable_btree_map_datas);
1128-
if !initial_map.is_empty() {
1129+
if !was_initially_empty {
1130+
let initial_map = self_.read(&mutable_btree_map_datas);
11291131
Some(vec![MapDiff::Replace { entries: initial_map.iter().map(|(k, v)| (k.clone(), v.clone())).collect() }])
1130-
} else {
1132+
} else if upstream_diffs.is_empty() {
11311133
None
1134+
} else {
1135+
Some(upstream_diffs)
11321136
}
11331137
} else if upstream_diffs.is_empty() { None } else { Some(upstream_diffs) }
11341138
});
@@ -2172,4 +2176,111 @@ pub(crate) mod tests {
21722176

21732177
cleanup(true);
21742178
}
2179+
2180+
#[test]
2181+
fn test_empty_map_first_insert() {
2182+
{
2183+
// Test that when a MutableBTreeMap starts empty and we insert into it,
2184+
// we only get one Insert diff (not a duplicate with Replace)
2185+
2186+
let mut app = create_test_app();
2187+
app.init_resource::<SignalMapOutput<String, i32>>();
2188+
2189+
// Start with an empty map
2190+
let source_map = MutableBTreeMap::from(app.world_mut());
2191+
2192+
// Create a signal_map and register it
2193+
let signal = source_map.signal_map();
2194+
let handle = signal.for_each(capture_map_output).register(app.world_mut());
2195+
2196+
// Insert the first entry
2197+
source_map.write(app.world_mut()).insert("a".to_string(), 42);
2198+
app.update();
2199+
2200+
// Should get exactly one Insert diff, not [Replace, Insert]
2201+
let diffs = get_and_clear_map_output::<String, i32>(app.world_mut());
2202+
assert_eq!(
2203+
diffs.len(),
2204+
1,
2205+
"Expected exactly one diff for first insert to empty map"
2206+
);
2207+
assert_eq!(
2208+
diffs[0],
2209+
MapDiff::Insert {
2210+
key: "a".to_string(),
2211+
value: 42
2212+
},
2213+
"Expected an Insert diff, not a Replace"
2214+
);
2215+
2216+
// Insert another entry to verify normal operation continues
2217+
source_map.write(app.world_mut()).insert("b".to_string(), 99);
2218+
app.update();
2219+
2220+
let diffs = get_and_clear_map_output::<String, i32>(app.world_mut());
2221+
assert_eq!(diffs.len(), 1);
2222+
assert_eq!(
2223+
diffs[0],
2224+
MapDiff::Insert {
2225+
key: "b".to_string(),
2226+
value: 99
2227+
}
2228+
);
2229+
2230+
handle.cleanup(app.world_mut());
2231+
}
2232+
2233+
cleanup(true);
2234+
}
2235+
2236+
#[test]
2237+
fn test_nonempty_map_initial_replace() {
2238+
{
2239+
// Test that when a MutableBTreeMap starts with entries,
2240+
// we get an initial Replace diff
2241+
2242+
let mut app = create_test_app();
2243+
app.init_resource::<SignalMapOutput<String, i32>>();
2244+
2245+
// Start with a map containing initial entries
2246+
let source_map =
2247+
MutableBTreeMapBuilder::from([("x".to_string(), 1), ("y".to_string(), 2), ("z".to_string(), 3)])
2248+
.spawn(app.world_mut());
2249+
2250+
// Create a signal_map and register it
2251+
let signal = source_map.signal_map();
2252+
let handle = signal.for_each(capture_map_output).register(app.world_mut());
2253+
2254+
// First update should produce a Replace with initial entries
2255+
app.update();
2256+
2257+
let diffs = get_and_clear_map_output::<String, i32>(app.world_mut());
2258+
assert_eq!(diffs.len(), 1, "Expected exactly one diff for initial state");
2259+
assert_eq!(
2260+
diffs[0],
2261+
MapDiff::Replace {
2262+
entries: vec![("x".to_string(), 1), ("y".to_string(), 2), ("z".to_string(), 3),]
2263+
},
2264+
"Expected a Replace diff with initial entries"
2265+
);
2266+
2267+
// Insert another entry to verify normal operation continues
2268+
source_map.write(app.world_mut()).insert("w".to_string(), 4);
2269+
app.update();
2270+
2271+
let diffs = get_and_clear_map_output::<String, i32>(app.world_mut());
2272+
assert_eq!(diffs.len(), 1);
2273+
assert_eq!(
2274+
diffs[0],
2275+
MapDiff::Insert {
2276+
key: "w".to_string(),
2277+
value: 4
2278+
}
2279+
);
2280+
2281+
handle.cleanup(app.world_mut());
2282+
}
2283+
2284+
cleanup(true);
2285+
}
21752286
}

0 commit comments

Comments
 (0)