aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/snapshot.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/snapshot.rs')
-rw-r--r--src/remake/snapshot.rs72
1 files changed, 72 insertions, 0 deletions
diff --git a/src/remake/snapshot.rs b/src/remake/snapshot.rs
index e82a2ab..2bb5fbb 100644
--- a/src/remake/snapshot.rs
+++ b/src/remake/snapshot.rs
@@ -289,6 +289,31 @@ pub(crate) fn epoch_metadata_path(generation: u64) -> Vec<String> {
meta::path(&[EPOCHS, generation.to_string().as_str()])
}
+/// The ended sessions `doc` records (`_continuum/sessions`), by user, each session with
+/// until when it would have lasted (one that doesn't read: at once, so it goes).
+fn ended_sessions(
+ doc: &impl ReadDoc,
+) -> Result<BTreeMap<String, BTreeMap<String, u64>>, StoreError> {
+ let mut users = BTreeMap::new();
+ for user in store::keys_at(doc, &meta::path(&[meta::SESSIONS]))?.unwrap_or_default() {
+ let path = meta::path(&[meta::SESSIONS, &user]);
+ let sessions = store::keys_at(doc, &path)?
+ .unwrap_or_default()
+ .into_iter()
+ .map(|session| {
+ let until = store::read_plain(doc, &meta::path(&[meta::SESSIONS, &user, &session]))
+ .ok()
+ .flatten()
+ .and_then(|until| until.as_u64())
+ .unwrap_or(0);
+ (session, until)
+ })
+ .collect();
+ users.insert(user, sessions);
+ }
+ Ok(users)
+}
+
/// Of `earlier` epochs' metadata (by generation), the generations a snapshot of
/// `generation`, proposed at `proposed_at`, leaves out: those no rejoining node replays
/// through any more. A node on epoch P replays while the replay window of epoch P+1
@@ -377,6 +402,23 @@ pub(crate) fn build(
for g in left_out(&earlier, generation, params.proposed_at) {
store::delete_path(&mut target, &epoch_metadata_path(g), false)?;
}
+ // Ended sessions past when they would have lasted: their tokens are refused as
+ // expired anyway. A user's whole map where none is left.
+ for (user, ended) in ended_sessions(&base)? {
+ let gone: Vec<&String> = ended
+ .iter()
+ .filter(|(_, until)| **until <= params.proposed_at.as_u64())
+ .map(|(session, _)| session)
+ .collect();
+ if gone.len() == ended.len() {
+ store::delete_path(&mut target, &meta::path(&[meta::SESSIONS, &user]), false)?;
+ } else {
+ for session in gone {
+ let path = meta::path(&[meta::SESSIONS, &user, session]);
+ store::delete_path(&mut target, &path, false)?;
+ }
+ }
+ }
// What the kind does to it (a strict remake keeps only what its manifest names).
kind.protocol(&base, params.content, &mut target)?;
@@ -691,6 +733,36 @@ mod tests {
assert_eq!(epoch["baseHeads"].as_array().unwrap().len(), heads.len());
}
+ /// Ended sessions go once they would have ended anyway; a user's map with them.
+ #[test]
+ fn ended_sessions_left_out_once_past() {
+ let mut a = doc(b"actor-a");
+ change(&mut a, |d| {
+ for (user, session, until) in
+ [("alice", "s1", 5), ("alice", "s2", 50), ("bob", "s3", 5)]
+ {
+ store::write_path(
+ d,
+ &meta::path(&[meta::SESSIONS, user, session]),
+ &json!(until),
+ )
+ .unwrap();
+ }
+ });
+ let policy = json!({});
+ let heads = a.get_heads();
+ let mut at = params(&heads, &policy);
+ at.proposed_at = UnixMillis(10);
+ let built = build(&a, &at).unwrap();
+ let keys = |path: &[&str]| {
+ store::keys_at(&built.doc, &meta::path(path))
+ .unwrap()
+ .unwrap_or_default()
+ };
+ assert_eq!(keys(&[meta::SESSIONS]), vec!["alice"]);
+ assert_eq!(keys(&[meta::SESSIONS, "alice"]), vec!["s2"]);
+ }
+
/// Earlier epochs' metadata goes once no rejoining node replays through it, oldest
/// first, but for a strict remake's; what doesn't read stays.
#[test]