| 88 |
88 |
|
/// One entry in the hold, as listed for a human.
|
| 89 |
89 |
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
| 90 |
90 |
|
pub struct HeldEntry {
|
|
91 |
+ |
/// Scope the change belongs to: `""` for personal, otherwise the group id.
|
|
92 |
+ |
pub scope: String,
|
| 91 |
93 |
|
/// Table the change targets.
|
| 92 |
94 |
|
pub table: String,
|
| 93 |
95 |
|
/// Wire row id of the change.
|
| 100 |
102 |
|
pub attempts: i64,
|
| 101 |
103 |
|
/// When the entry was first held (RFC 3339, UTC).
|
| 102 |
104 |
|
pub first_seen: String,
|
|
105 |
+ |
/// The held change's row payload, so a consumer can name the row in terms
|
|
106 |
+ |
/// its user recognises. `None` for a delete, or a payload that no longer
|
|
107 |
+ |
/// parses.
|
|
108 |
+ |
pub payload: Option<serde_json::Value>,
|
| 103 |
109 |
|
}
|
| 104 |
110 |
|
|
| 105 |
111 |
|
/// How much is being held for a scope, for a sync-status surface.
|
| 172 |
178 |
|
Ok(out)
|
| 173 |
179 |
|
}
|
| 174 |
180 |
|
|
|
181 |
+ |
const LIST_COLUMNS: &str =
|
|
182 |
+ |
"scope, table_name, row_id, cause, state, attempts, first_seen, entry FROM sync_deferred";
|
|
183 |
+ |
|
|
184 |
+ |
fn read_held(r: &rusqlite::Row<'_>) -> rusqlite::Result<HeldEntry> {
|
|
185 |
+ |
let state: String = r.get(4)?;
|
|
186 |
+ |
Ok(HeldEntry {
|
|
187 |
+ |
scope: r.get(0)?,
|
|
188 |
+ |
table: r.get(1)?,
|
|
189 |
+ |
row_id: r.get(2)?,
|
|
190 |
+ |
cause: r.get(3)?,
|
|
191 |
+ |
state: if state == "rejected" {
|
|
192 |
+ |
HoldState::Rejected
|
|
193 |
+ |
} else {
|
|
194 |
+ |
HoldState::Deferred
|
|
195 |
+ |
},
|
|
196 |
+ |
attempts: r.get(5)?,
|
|
197 |
+ |
first_seen: r.get(6)?,
|
|
198 |
+ |
// The payload is handed back so a consumer can label the row with
|
|
199 |
+ |
// something a person recognises (a task's title) instead of a wire row
|
|
200 |
+ |
// id. A payload that will not parse degrades to None, never an error.
|
|
201 |
+ |
payload: serde_json::from_str::<ChangeEntry>(&r.get::<_, String>(7)?)
|
|
202 |
+ |
.ok()
|
|
203 |
+ |
.and_then(|e| e.data),
|
|
204 |
+ |
})
|
|
205 |
+ |
}
|
|
206 |
+ |
|
| 175 |
207 |
|
/// List everything held for a scope, newest first, for a UI surface.
|
| 176 |
208 |
|
pub fn list(conn: &Connection, scope: &str) -> Result<Vec<HeldEntry>> {
|
| 177 |
|
- |
let mut stmt = conn.prepare(
|
| 178 |
|
- |
"SELECT table_name, row_id, cause, state, attempts, first_seen \
|
| 179 |
|
- |
FROM sync_deferred WHERE scope = ?1 ORDER BY last_seen DESC",
|
| 180 |
|
- |
)?;
|
| 181 |
|
- |
let rows = stmt.query_map([scope], |r| {
|
| 182 |
|
- |
let state: String = r.get(3)?;
|
| 183 |
|
- |
Ok(HeldEntry {
|
| 184 |
|
- |
table: r.get(0)?,
|
| 185 |
|
- |
row_id: r.get(1)?,
|
| 186 |
|
- |
cause: r.get(2)?,
|
| 187 |
|
- |
state: if state == "rejected" {
|
| 188 |
|
- |
HoldState::Rejected
|
| 189 |
|
- |
} else {
|
| 190 |
|
- |
HoldState::Deferred
|
| 191 |
|
- |
},
|
| 192 |
|
- |
attempts: r.get(4)?,
|
| 193 |
|
- |
first_seen: r.get(5)?,
|
| 194 |
|
- |
})
|
| 195 |
|
- |
})?;
|
|
209 |
+ |
let mut stmt = conn.prepare(&format!(
|
|
210 |
+ |
"SELECT {LIST_COLUMNS} WHERE scope = ?1 ORDER BY last_seen DESC"
|
|
211 |
+ |
))?;
|
|
212 |
+ |
collect(stmt.query_map([scope], read_held)?)
|
|
213 |
+ |
}
|
|
214 |
+ |
|
|
215 |
+ |
/// List everything held across every scope, newest first.
|
|
216 |
+ |
///
|
|
217 |
+ |
/// A device syncs its personal scope plus one per group, and a row held in a
|
|
218 |
+ |
/// group scope is just as lost as one held in the personal scope, so a status
|
|
219 |
+ |
/// surface that covered only personal would hide the failures this exists to
|
|
220 |
+ |
/// show.
|
|
221 |
+ |
pub fn list_all(conn: &Connection) -> Result<Vec<HeldEntry>> {
|
|
222 |
+ |
let mut stmt = conn.prepare(&format!("SELECT {LIST_COLUMNS} ORDER BY last_seen DESC"))?;
|
|
223 |
+ |
collect(stmt.query_map([], read_held)?)
|
|
224 |
+ |
}
|
|
225 |
+ |
|
|
226 |
+ |
fn collect<I>(rows: I) -> Result<Vec<HeldEntry>>
|
|
227 |
+ |
where
|
|
228 |
+ |
I: Iterator<Item = rusqlite::Result<HeldEntry>>,
|
|
229 |
+ |
{
|
| 196 |
230 |
|
let mut out = Vec::new();
|
| 197 |
231 |
|
for row in rows {
|
| 198 |
232 |
|
out.push(row?);
|
| 200 |
234 |
|
Ok(out)
|
| 201 |
235 |
|
}
|
| 202 |
236 |
|
|
|
237 |
+ |
fn read_tally(r: &rusqlite::Row<'_>) -> rusqlite::Result<(String, i64)> {
|
|
238 |
+ |
Ok((r.get(0)?, r.get(1)?))
|
|
239 |
+ |
}
|
|
240 |
+ |
|
| 203 |
241 |
|
/// Held counts for a scope.
|
| 204 |
242 |
|
pub fn counts(conn: &Connection, scope: &str) -> Result<HoldCounts> {
|
| 205 |
|
- |
let mut out = HoldCounts::default();
|
| 206 |
243 |
|
let mut stmt =
|
| 207 |
244 |
|
conn.prepare("SELECT state, COUNT(*) FROM sync_deferred WHERE scope = ?1 GROUP BY state")?;
|
| 208 |
|
- |
let rows = stmt.query_map([scope], |r| {
|
| 209 |
|
- |
Ok((r.get::<_, String>(0)?, r.get::<_, i64>(1)?))
|
| 210 |
|
- |
})?;
|
|
245 |
+ |
tally(stmt.query_map([scope], read_tally)?)
|
|
246 |
+ |
}
|
|
247 |
+ |
|
|
248 |
+ |
/// Held counts across every scope. See [`list_all`].
|
|
249 |
+ |
pub fn counts_all(conn: &Connection) -> Result<HoldCounts> {
|
|
250 |
+ |
let mut stmt = conn.prepare("SELECT state, COUNT(*) FROM sync_deferred GROUP BY state")?;
|
|
251 |
+ |
tally(stmt.query_map([], read_tally)?)
|
|
252 |
+ |
}
|
|
253 |
+ |
|
|
254 |
+ |
fn tally<I>(rows: I) -> Result<HoldCounts>
|
|
255 |
+ |
where
|
|
256 |
+ |
I: Iterator<Item = rusqlite::Result<(String, i64)>>,
|
|
257 |
+ |
{
|
|
258 |
+ |
let mut out = HoldCounts::default();
|
| 211 |
259 |
|
for row in rows {
|
| 212 |
260 |
|
let (state, n) = row?;
|
| 213 |
261 |
|
let n = u64::try_from(n).unwrap_or(0);
|