markread: persist account-keyed read markers to disk and restore them at startup
This commit is contained in:
parent
3f9aa8c6eb
commit
0f28083855
3 changed files with 97 additions and 0 deletions
|
|
@ -25,6 +25,70 @@ pub fn marker_id(s: &Server, uid: Uid) -> String {
|
|||
.unwrap_or_else(|| format!("~{uid}"))
|
||||
}
|
||||
|
||||
/// The read-marker database path: the `markread_database` conf key, or `<conf>.markread`.
|
||||
fn db_path(s: &Server) -> String {
|
||||
match s.conf("markread_database") {
|
||||
Some(p) if !p.is_empty() => p.to_string(),
|
||||
_ => format!("{}.markread", s.conf_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Serialise the durable (account-keyed) markers as `identity target ts` lines.
|
||||
/// Session `~uid` keys are dropped — a uid doesn't outlive the connection, let
|
||||
/// alone a restart. Output is sorted so it's stable across coalesced writes.
|
||||
fn dump_markers(m: &ReadMarkers) -> String {
|
||||
let mut out = String::from("# echoircd read markers — auto-generated; account-keyed only\n");
|
||||
let mut ids: Vec<&String> = m.0.keys().filter(|k| !k.starts_with('~')).collect();
|
||||
ids.sort();
|
||||
for id in ids {
|
||||
if let Some(targets) = m.0.get(id) {
|
||||
let mut tk: Vec<&String> = targets.keys().collect();
|
||||
tk.sort();
|
||||
for t in tk {
|
||||
out.push_str(&format!("{id} {t} {}\n", targets[t]));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Parse the on-disk format back into a store (identity/target names carry no
|
||||
/// spaces, so a 3-way split is unambiguous).
|
||||
fn load_str(store: &mut ReadMarkers, text: &str) {
|
||||
for line in text.lines() {
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let mut it = line.splitn(3, ' ');
|
||||
if let (Some(id), Some(target), Some(ts)) = (it.next(), it.next(), it.next()) {
|
||||
if let Ok(ts) = ts.parse::<u64>() {
|
||||
store
|
||||
.0
|
||||
.entry(id.to_string())
|
||||
.or_default()
|
||||
.insert(target.to_string(), ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist the account-keyed markers. Off-core via `disk_write`, which coalesces
|
||||
/// repeated writes to the same path, so frequent MARKREADs stay cheap.
|
||||
pub fn save(s: &Server) {
|
||||
if let Some(m) = s.ext.get::<ReadMarkers>() {
|
||||
s.disk_write(db_path(s), dump_markers(m));
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore account-keyed markers at startup so read positions survive a restart.
|
||||
pub fn load(s: &mut Server) {
|
||||
let Ok(text) = std::fs::read_to_string(db_path(s)) else {
|
||||
return;
|
||||
};
|
||||
let store = s.ext.get_or_insert_with::<ReadMarkers>(ReadMarkers::default);
|
||||
load_str(store, &text);
|
||||
}
|
||||
|
||||
/// Cleanup hook: drop a user's session markers on disconnect (account-keyed
|
||||
/// markers are intentionally kept so they persist across reconnects).
|
||||
pub struct MarkRead;
|
||||
|
|
@ -85,6 +149,9 @@ impl Command for MarkReadCmd {
|
|||
for p in recips {
|
||||
s.send(p, line.clone());
|
||||
}
|
||||
if !id.starts_with('~') {
|
||||
save(s); // account markers are durable — persist across restarts
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let val = s
|
||||
|
|
@ -101,3 +168,28 @@ impl Command for MarkReadCmd {
|
|||
CmdResult::Ok
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn markers_round_trip_and_drop_session_keys() {
|
||||
let mut m = ReadMarkers::default();
|
||||
m.0.entry("alice".into()).or_default().insert("#chan".into(), 1700);
|
||||
m.0.entry("alice".into()).or_default().insert("bob".into(), 42);
|
||||
m.0.entry("~7".into()).or_default().insert("#chan".into(), 9999); // session: ephemeral
|
||||
|
||||
let text = dump_markers(&m);
|
||||
assert!(text.contains("alice #chan 1700"));
|
||||
assert!(text.contains("alice bob 42"));
|
||||
assert!(!text.contains("~7"), "session keys must not be persisted");
|
||||
|
||||
// reload into a fresh store — the account markers come back, the session one doesn't
|
||||
let mut restored = ReadMarkers::default();
|
||||
load_str(&mut restored, &text);
|
||||
assert_eq!(restored.0.get("alice").and_then(|t| t.get("#chan")), Some(&1700));
|
||||
assert_eq!(restored.0.get("alice").and_then(|t| t.get("bob")), Some(&42));
|
||||
assert!(!restored.0.contains_key("~7"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue