Skip to main content

long_screen/
store.rs

1//! The row store: every row a run persists, by key, in key order — the native stand-in for the page's IndexedDB
2//! namespace `run/<id>/` (plus the `project/<id>` row).
3//!
4//! Rows live in memory. Encoded tile images can move to a spill directory once they take more than the store's
5//! budget, so a long recording's print does not have to fit in RAM twice (decoded in the tile cache and encoded
6//! here); they are read back from there when a pass or the output writer asks for them.
7
8use crate::value::{Js, Obj};
9use std::collections::BTreeMap;
10use std::fs;
11use std::io;
12use std::path::PathBuf;
13use std::sync::Arc;
14
15/// Where a blob's bytes are.
16#[derive(Clone, Debug)]
17enum Spilled {
18    File(PathBuf),
19}
20
21/// Rows by key, iterated in key order.
22pub struct Store {
23    rows: BTreeMap<String, Js>,
24    /// Bytes of blobs held in memory, and the budget past which new blobs go to `spill`.
25    blob_bytes: usize,
26    budget: Option<usize>,
27    spill: Option<PathBuf>,
28    spilled: BTreeMap<String, Spilled>,
29    spill_count: u64,
30}
31
32impl Store {
33    /// A store that keeps everything in memory.
34    pub fn in_memory() -> Store {
35        Store {
36            rows: BTreeMap::new(),
37            blob_bytes: 0,
38            budget: None,
39            spill: None,
40            spilled: BTreeMap::new(),
41            spill_count: 0,
42        }
43    }
44
45    /// A store that writes blobs into `dir` once those in memory exceed `budget` bytes.
46    pub fn spilling(dir: PathBuf, budget: usize) -> io::Result<Store> {
47        fs::create_dir_all(&dir)?;
48        let mut store = Store::in_memory();
49        store.budget = Some(budget);
50        store.spill = Some(dir);
51        Ok(store)
52    }
53
54    pub fn len(&self) -> usize {
55        self.rows.len()
56    }
57
58    pub fn is_empty(&self) -> bool {
59        self.rows.is_empty()
60    }
61
62    /// The row under `key`, with any spilled blob read back.
63    pub fn get(&self, key: &str) -> Option<Js> {
64        let row = self.rows.get(key)?;
65        if let Some(Spilled::File(path)) = self.spilled.get(key) {
66            let mut row = row.clone();
67            if let Js::Obj(o) = &mut row {
68                if let Some(Js::Blob(kind, _)) = o.get("blob").cloned() {
69                    let bytes = fs::read(path).unwrap_or_default();
70                    o.set("blob", Js::Blob(kind, Arc::new(bytes)));
71                }
72            }
73            return Some(row);
74        }
75        Some(row.clone())
76    }
77
78    /// A reference to the row as stored (a spilled blob reads as empty).
79    pub fn peek(&self, key: &str) -> Option<&Js> {
80        self.rows.get(key)
81    }
82
83    pub fn contains(&self, key: &str) -> bool {
84        self.rows.contains_key(key)
85    }
86
87    pub fn put(&mut self, key: &str, value: Js) {
88        self.forget_blob(key);
89        let value = self.maybe_spill(key, value);
90        self.rows.insert(key.to_string(), value);
91    }
92
93    pub fn delete(&mut self, key: &str) {
94        self.forget_blob(key);
95        self.rows.remove(key);
96    }
97
98    /// Keys under `prefix`, in key order.
99    pub fn keys(&self, prefix: &str) -> Vec<String> {
100        self.rows
101            .range(prefix.to_string()..)
102            .take_while(|(k, _)| k.starts_with(prefix))
103            .map(|(k, _)| k.clone())
104            .collect()
105    }
106
107    pub fn delete_prefix(&mut self, prefix: &str) {
108        for key in self.keys(prefix) {
109            self.delete(&key);
110        }
111    }
112
113    /// Every row, in key order (spilled blobs read as empty; see [`Store::get`]).
114    pub fn iter(&self) -> impl Iterator<Item = (&String, &Js)> {
115        self.rows.iter()
116    }
117
118    fn blob_len(value: &Js) -> usize {
119        match value {
120            Js::Obj(o) => match o.get("blob") {
121                Some(Js::Blob(_, b)) => b.len(),
122                _ => 0,
123            },
124            _ => 0,
125        }
126    }
127
128    fn forget_blob(&mut self, key: &str) {
129        if let Some(Spilled::File(path)) = self.spilled.remove(key) {
130            let _ = fs::remove_file(path);
131        } else if let Some(old) = self.rows.get(key) {
132            self.blob_bytes -= Self::blob_len(old);
133        }
134    }
135
136    fn maybe_spill(&mut self, key: &str, value: Js) -> Js {
137        let len = Self::blob_len(&value);
138        if len == 0 {
139            return value;
140        }
141        let (Some(budget), Some(dir)) = (self.budget, &self.spill) else {
142            self.blob_bytes += len;
143            return value;
144        };
145        if self.blob_bytes + len <= budget {
146            self.blob_bytes += len;
147            return value;
148        }
149        self.spill_count += 1;
150        let path = dir.join(format!("{}.bin", self.spill_count));
151        let Js::Obj(mut o) = value else {
152            unreachable!()
153        };
154        let Some(Js::Blob(kind, bytes)) = o.get("blob").cloned() else {
155            unreachable!()
156        };
157        if fs::write(&path, bytes.as_slice()).is_err() {
158            // The spill directory failed: keep the blob in memory rather than lose it.
159            self.blob_bytes += len;
160            return Js::Obj(o);
161        }
162        o.set("blob", Js::Blob(kind, Arc::new(Vec::new())));
163        self.spilled.insert(key.to_string(), Spilled::File(path));
164        Js::Obj(o)
165    }
166
167    /// Blobs held in memory, in bytes.
168    pub fn blob_bytes(&self) -> usize {
169        self.blob_bytes
170    }
171
172    /// Rows whose blob lives in the spill directory.
173    pub fn spilled(&self) -> usize {
174        self.spilled.len()
175    }
176}
177
178impl Drop for Store {
179    fn drop(&mut self) {
180        for (_, Spilled::File(path)) in std::mem::take(&mut self.spilled) {
181            let _ = fs::remove_file(path);
182        }
183    }
184}
185
186/// An object row's field, for readers that take a stored row apart.
187pub fn obj(value: &Js) -> Option<&Obj> {
188    value.as_obj()
189}