Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 13 additions & 85 deletions src-tauri/src/notices/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,11 @@ const FLAP_TIMES: usize = 3;
/// 撑过这么久的 critical,恢复时值得说一句;短的静默撤回
const SAY_RECOVERED_AFTER: Duration = Duration::from_secs(300);

/// 一次性的事([`Signal::event`])弹过一次之后,这么久里再发生只记下、不再弹;冷却结束时
/// 还有没说的,合成一条说。
/// 一次性的事([`Signal::event`])弹过一次之后,这么久里再发生只记下、不再弹。
///
/// **冷却结束时不补发**:冷却期间又发生的,列表里那一条累计次数、重新算没看过,通知中心里
/// 那一份原地改掉(不打断)。事后补的一条说的是几分钟前的事,那时那次请求、那段会话多半
/// 已经结束了,用户对不上;冷却过后再发生,照常当场弹。
///
/// **5 分钟**:一次拦截发生时客户端正在弹批准提示,第一条必须立刻说;之后几分钟里同一家
/// 再拦下的,几乎总是同一件事的后续(代理在重试同一个调用、同一段会话接着往下走),用户
Expand Down Expand Up @@ -229,8 +232,8 @@ impl Signal {
/// 铃铛、菜单栏、系统通知都不再有动静 —— 而记录是落盘的,重启之后也一样。所以:
///
/// - 每发生一次,列表里那一条都**重新算没看过**,次数和内容换成最近的这一次
/// - 系统通知按 [`COOLDOWN`] 限着:弹过一次之后,冷却期间再发生只记下;冷却结束时
/// 还有没说的、用户也还没看过,合成一条再说一次
/// - 系统通知按 [`COOLDOWN`] 限着:弹过一次之后,冷却期间再发生只记下、不再弹,冷却
/// 结束时也不补;之后再发生照常当场弹
/// - 不去抖:它不会自己好
pub fn event(mut self) -> Self {
self.event = true;
Expand Down Expand Up @@ -311,14 +314,11 @@ struct Open {
muted_until: Option<Instant>,
/// 这一条是抖动之后留下的那句「(时断时续)」说明,不是一件还开着的事
intermittent: bool,
/// 一次性的事上次弹出之后,冷却到什么时候([`COOLDOWN`])。用 tokio 的钟:和冷却结束
/// 时的那个计时器是同一个钟,测试里拨得动。
/// 一次性的事上次弹出之后,冷却到什么时候([`COOLDOWN`])。用 tokio 的钟:测试里拨得动。
///
/// **只在内存里,不落盘**:重启之后头一次再发生照常弹。宁可重启之后多说一次,也不能
/// 让存下来的状态把以后的每一次都压住
cool_until: Option<tokio::time::Instant>,
/// 冷却期间又发生、还没为它弹过的次数。冷却结束时合成一条说
unsaid: u32,
/// 要弹时和同时要弹的合成一条([`Signal::merged`]),这是它那一行
merge: Option<String>,
}
Expand Down Expand Up @@ -386,7 +386,6 @@ impl Notices {
muted_until: None,
intermittent: false,
cool_until: None,
unsaid: 0,
merge: None,
},
)
Expand Down Expand Up @@ -651,17 +650,15 @@ impl Notices {
}
let now = Instant::now();
let tick = tokio::time::Instant::now();
let (notice, deliver, due, sum_up) = {
let (notice, deliver, due) = {
let mut g = self.state.lock().expect("锁未中毒");
let existing = g.open.get(&signal.key).cloned();
let muted = existing.as_ref().and_then(|o| o.muted_until);
let hush = hushed(&g, &signal.key, muted, now);
let escalated = existing
.as_ref()
.is_some_and(|o| signal.level > o.notice.level);
let (mut cool_until, mut unsaid) = existing
.as_ref()
.map_or((None, 0), |o| (o.cool_until, o.unsaid));
let mut cool_until = existing.as_ref().and_then(|o| o.cool_until);
let cooling = signal.event && cool_until.is_some_and(|t| tick < t);
let notice = match existing {
// 一次性的事又发生了一次:**是新的一件**,重新算没看过
Expand Down Expand Up @@ -715,18 +712,8 @@ impl Notices {
let merged = signal.merge.is_some();
let deliver =
wants && !signal.hold && (merged || take_token(&mut g, &signal.key, signal.level));
let mut sum_up = None;
if signal.event {
if deliver {
cool_until = Some(tick + COOLDOWN);
unsaid = 0;
} else if interrupts && !hush && cooling {
// 只差冷却这一条:记下,冷却结束时合成一条说。头一次记下时排上那个时刻
if unsaid == 0 {
sum_up = cool_until;
}
unsaid = unsaid.saturating_add(signal.times);
}
if signal.event && deliver {
cool_until = Some(tick + COOLDOWN);
}
g.open.insert(
signal.key.clone(),
Expand All @@ -736,11 +723,10 @@ impl Notices {
muted_until: muted,
intermittent: false,
cool_until,
unsaid,
merge: signal.merge.clone(),
},
);
(notice, deliver, due, sum_up)
(notice, deliver, due)
};
if deliver {
self.deliver(&notice);
Expand All @@ -753,9 +739,6 @@ impl Notices {
if let Some(at) = due {
self.schedule(signal.key.clone(), at);
}
if let Some(at) = sum_up {
self.sum_up(signal.key.clone(), at);
}
self.persist();
self.changed();
}
Expand Down Expand Up @@ -804,48 +787,6 @@ impl Notices {
});
}

/// 一次性的事冷却结束(`at`)。**期间又发生过、用户还没看的,合成一条说**,说了就再
/// 冷却一段。用户已经看过了、换了一档、这会儿被更要紧的事压着的,就不说了 —— 列表里
/// 那一条一直在,次数也一直在涨
fn sum_up(self: &Arc<Self>, key: String, at: tokio::time::Instant) {
let me = self.clone();
tokio::spawn(async move {
tokio::time::sleep_until(at).await;
let wants = me.mode() == Mode::System;
let shown = {
let mut g = me.state.lock().expect("锁未中毒");
// 还开着、冷却的还是这一段、期间有没说的
let pending = g
.open
.get(&key)
.filter(|o| o.cool_until == Some(at) && o.unsaid > 0)
.map(|o| (o.notice.clone(), o.unsaid, o.muted_until, o.merge.is_some()));
let Some((notice, unsaid, muted, merged)) = pending else {
return;
};
let say = wants
&& !notice.read
&& !hushed(&g, &key, muted, Instant::now())
&& (merged || take_token(&mut g, &key, notice.level));
if let Some(o) = g.open.get_mut(&key) {
o.unsaid = 0;
if say {
o.cool_until = Some(tokio::time::Instant::now() + COOLDOWN);
}
}
say.then(|| Notice {
body: summed_up(&notice.body, unsaid),
..notice
})
};
if let Some(n) = shown {
me.deliver(&n);
me.persist();
me.changed();
}
});
}

fn deliver(self: &Arc<Self>, notice: &Notice) {
if self.merges(&notice.key) {
self.merge_later(&notice.key);
Expand Down Expand Up @@ -1012,7 +953,6 @@ impl Notices {
muted_until: Some(now + FLAP_WINDOW),
intermittent: true,
cool_until: None,
unsaid: 0,
merge: None,
},
);
Expand Down Expand Up @@ -1127,18 +1067,6 @@ fn newest_first(state: &State) -> Vec<Notice> {
out
}

/// 冷却结束时合成的那一条的正文:最近那一次的内容,再带一句上次弹出之后又发生了几次
fn summed_up(body: &str, n: u32) -> String {
tr!(
format!("{body}(上次通知后又发生 {n} 次)"),
if n == 1 {
format!("{body} (1 more time since the last notification)")
} else {
format!("{body} ({n} more times since the last notification)")
}
)
}

/// 此刻不该为 `key` 打断用户:它在抖(`muted_until` 之前),或者有更要紧的事压着它。
/// 新来的时候判一次,去抖到点时再判一次([`Notices::schedule`])—— 两处必须是同一套
fn hushed(state: &State, key: &str, muted_until: Option<Instant>, now: Instant) -> bool {
Expand Down
66 changes: 34 additions & 32 deletions src-tauri/src/notices/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ async fn wait(d: Duration) {
}

/// 每拦下一次都是一件新的事。**看过之后再拦下,铃铛和菜单栏立刻重新数它**(它们只数没
/// 看过的);冷却期间不再弹,冷却结束时合成一条说。以前它按「同一件事」去重:看过一次
/// 看过的);冷却期间不再弹,冷却结束时也不补。以前它按「同一件事」去重:看过一次
/// 之后,再拦下多少次都只是悄悄涨个次数
#[tokio::test(start_paused = true)]
async fn a_block_after_the_last_one_was_read_counts_again_at_once() {
Expand All @@ -459,7 +459,7 @@ async fn a_block_after_the_last_one_was_read_counts_again_at_once() {
assert_eq!(n.count, 2);
assert_eq!(b.titles().len(), 1, "冷却期间不再弹:{:?}", b.titles());
wait(COOLDOWN).await;
assert_eq!(b.titles().len(), 2, "冷却结束时要说:{:?}", b.titles());
assert_eq!(b.titles().len(), 1, "冷却结束时不补:{:?}", b.titles());
}

#[tokio::test(start_paused = true)]
Expand All @@ -474,10 +474,11 @@ async fn once_the_cooldown_is_over_the_next_block_pops_right_away() {
assert_eq!(b.bus.list()[0].count, 2);
}

/// 一阵子里接连拦下好多次:**只弹一条**,其余的进列表、累计次数,不刷屏;冷却结束时还有
/// 没说的,合成一条再说一次,之后没有再拦下就安静。另一家不受这一家的冷却影响
/// 一阵子里接连拦下好多次:**只弹一条**,其余的进列表、累计次数,不刷屏。**冷却结束时
/// 不补发**:几分钟后补的一条说的是那次会话早就过去的事。另一家不受这一家的冷却影响;
/// 冷却过后再拦下,当场弹
#[tokio::test(start_paused = true)]
async fn a_burst_of_blocks_pops_once_and_is_summed_up_when_the_cooldown_ends() {
async fn a_burst_of_blocks_pops_once_and_nothing_comes_late() {
let b = bed();
for i in 0..10 {
b.bus.ingest(blocked("relay"), T0 + i * 1_000);
Expand All @@ -488,20 +489,28 @@ async fn a_burst_of_blocks_pops_once_and_is_summed_up_when_the_cooldown_ends() {
b.bus.ingest(blocked("other"), T0 + 10_000);
assert_eq!(b.titles().len(), 2, "另一家照常说:{:?}", b.titles());

wait(COOLDOWN).await;
assert_eq!(b.titles().len(), 3, "{:?}", b.titles());
let last = b.bodies().last().cloned().unwrap_or_default();
assert!(
last.contains("relay") && last.contains("又发生 9 次"),
"{last}"
);
wait(COOLDOWN * 3).await;
assert_eq!(b.titles().len(), 3, "之后没有再拦下,不再说");
assert_eq!(b.titles().len(), 2, "冷却结束时不补:{:?}", b.titles());
assert_eq!(
b.bus
.list()
.iter()
.find(|n| n.key == "toolwall:relay")
.map(|n| n.count),
Some(10)
);
b.bus.ingest(blocked("relay"), T0 + 1_000_000);
assert_eq!(
b.titles().len(),
3,
"冷却过后再拦下,当场弹:{:?}",
b.titles()
);
}

/// 这一阵子拦下的,用户在冷却结束之前都看过了:没有要合成的
/// 这一阵子拦下的,用户在冷却结束之前都看过了:之后什么都不说
#[tokio::test(start_paused = true)]
async fn a_burst_that_was_read_before_the_cooldown_ended_is_not_summed_up() {
async fn a_burst_that_was_read_before_the_cooldown_ended_stays_quiet() {
let b = bed();
for i in 0..3 {
b.bus.ingest(blocked("relay"), T0 + i * 1_000);
Expand Down Expand Up @@ -820,11 +829,10 @@ async fn a_plugin_failing_on_every_answer_is_gathered_not_flooded() {
assert_eq!(bus.list()[0].count, 52);
assert_eq!(pushes(), after_first + 2);

// 冷却结束时合成一条说,带上这期间又发生的次数
// 冷却结束时不补发:次数在列表里
wait(COOLDOWN).await;
let all = shown.lock().unwrap().clone();
assert_eq!(all.len(), 2, "{all:?}");
assert!(all[1].contains("51"), "{}", all[1]);
assert_eq!(all.len(), 1, "{all:?}");
}

/// 两个插件各攒各的:一个在攒着,另一个的头一次照样立刻说
Expand Down Expand Up @@ -1058,7 +1066,7 @@ async fn hits_that_arrive_together_are_told_in_one_notification() {
}
}

/// 同一条规则 5 分钟内至多通知一次;冷却结束时还有没说的,合成一条再说
/// 同一条规则 5 分钟内至多通知一次;冷却结束时不补发
#[tokio::test(start_paused = true)]
async fn the_same_rule_is_told_at_most_once_in_five_minutes() {
let b = bed();
Expand All @@ -1072,14 +1080,18 @@ async fn the_same_rule_is_told_at_most_once_in_five_minutes() {
wait(MERGE).await;
assert_eq!(b.titles().len(), 1, "冷却期间不再弹:{:?}", b.titles());
assert_eq!(b.bus.list()[0].count, 3, "次数照记");
wait(COOLDOWN).await;
wait(MERGE).await;
assert_eq!(b.titles().len(), 2, "冷却结束时说一次:{:?}", b.titles());
// 另一条规则不受这一条的冷却影响
for s in rules::from_event(&secrets("relay", &[("email", true)])) {
b.bus.ingest(s, T0 + 130_000);
}
wait(MERGE).await;
assert_eq!(b.titles().len(), 2, "{:?}", b.titles());
wait(COOLDOWN).await;
wait(MERGE).await;
assert_eq!(b.titles().len(), 2, "冷却结束时不补:{:?}", b.titles());
// 冷却过后再命中,当场弹
b.bus.ingest(hit(), T0 + 600_000);
wait(MERGE).await;
assert_eq!(b.titles().len(), 3, "{:?}", b.titles());
}

Expand Down Expand Up @@ -1537,16 +1549,6 @@ fn the_bus_adds_its_own_words_in_english_too() {
b.titles().last().unwrap(),
"Resolved: Gateway Not Forwarding"
);

// 冷却结束时合成的那一条
assert_eq!(
summed_up("It matched the rule.", 1),
"It matched the rule. (1 more time since the last notification)"
);
assert_eq!(
summed_up("It matched the rule.", 9),
"It matched the rule. (9 more times since the last notification)"
);
});
}

Expand Down
Loading