Skip to main content

haematite_cli/serve/
changes.rs

1//! `changes.list`: the documents added, changed or removed between two kept
2//! versions of one branch, in (collection, id) order, page by page.
3//!
4//! The changes come from the engine's diff of each shard's two roots over the
5//! revision entries (`m\0`) alone, so no body is read unless values are asked,
6//! and a page costs in proportion to the changes, never to the store. Each
7//! shard's diff resumes after the last key the answer passed, and the shards'
8//! changes are merged in key order, so the sealed `next` holds one key.
9//!
10//! A row's `before` and `after` are the versions its revisions were written
11//! at. A record merged in from another branch keeps the version it was written
12//! at on its own side; the version the change belongs to is the answer's `to`.
13//!
14//! A change to a record the caller may not read is absent and not counted, but
15//! it is taken against the page's limit: a page that reaches its limit inside
16//! a run of such changes gives no rows and a `next`. A caller can learn from
17//! such pages that changes lie there which it was not given, and their number
18//! bounds how many; never which records, or where.
19//!
20//! `to: {branch}` with no version is the head, captured on the first page;
21//! the answer's `to` names it, and every later page reads at that version from
22//! the token, so a writer between pages changes nothing. A live view asks again
23//! with `from` set to the last `to` it was given. `from: {"fork": branch}` is
24//! the moment that branch forked at, from its fork provenance; `to` must then
25//! be on that branch.
26//!
27//! `from` may be later than `to`: the answer is then the reverse, what would
28//! be undone going back, with added and removed exchanged and each row's
29//! `before` and `after` exchanged.
30//!
31//! With values, each body is JSON as in query.run's rows: every number's text
32//! as written, object keys in sorted order. The bytes exactly as stored are
33//! `documents.get`'s `raw_json` at that version.
34
35#[cfg(test)]
36use std::cell::Cell;
37use std::collections::{BTreeMap, VecDeque};
38use std::num::{NonZeroU32, NonZeroU64, NonZeroUsize};
39
40use haematite::db::{ObservedChange, ObserverError, ReadOnlyDatabase};
41use haematite::tree::{DiffBudget, DiffError, DiffInterval};
42use haematite::{Hash, TreeError};
43use haematite_query::page::{Allowance, Origin, Resume};
44use serde::{Deserialize, Serialize};
45use serde_json::value::RawValue;
46use serde_json::{Value, json};
47
48use super::access::Access;
49use super::contracts::Caller;
50use super::protocol::{QueryPage, Selection, Version};
51use super::query::{Binding, Continuation, Engine, Rows, Seal};
52use super::storage::{
53    Captured, Catalogue, DocumentRevision, Failure, ForkProvenance, Held, document_key, reads,
54    revision_key,
55};
56
57const REVISIONS: &[u8] = b"m\0";
58const REVISIONS_END: &[u8] = b"m\x01";
59
60#[cfg(test)]
61thread_local! {
62    /// Diff calls and body reads made on this thread, for tests.
63    static DIFFS: Cell<u64> = const { Cell::new(0) };
64    static BODIES: Cell<u64> = const { Cell::new(0) };
65}
66
67/// The most changes one shard's diff gives at once.
68const BATCH: usize = 64;
69
70/// The most bytes a collection name or record id may have.
71const ID_BYTES: usize = 512;
72
73/// The named limits of `changes.list`, each set in serve.json or defaulted.
74#[derive(Clone, Copy, Debug)]
75pub struct Settings {
76    /// Changes one page may take, denied ones included; at least 2.
77    pub scan_records: NonZeroU64,
78    /// Tree nodes one shard's diff may load for one call.
79    pub node_budget: NonZeroUsize,
80    /// The most rows a page may ask for.
81    pub max_page: NonZeroU32,
82}
83
84impl Default for Settings {
85    fn default() -> Self {
86        Self {
87            scan_records: NonZeroU64::MIN.saturating_add(9_999),
88            // As `branch.review`'s node budget (storage/review.rs).
89            node_budget: NonZeroUsize::MIN.saturating_add(65_535),
90            max_page: NonZeroU32::MIN.saturating_add(999),
91        }
92    }
93}
94
95/// A moment a caller names: a branch at a version, or its head (`to` only),
96/// or the moment a branch forked at (`from` only).
97#[derive(Debug, Serialize, Deserialize)]
98#[serde(untagged)]
99pub enum Moment {
100    Fork(Fork),
101    At(Selection),
102}
103
104/// The moment `fork` forked at.
105#[derive(Debug, Serialize, Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct Fork {
108    pub fork: String,
109}
110
111/// The two moments, the page and the row form a caller asks for.
112#[derive(Debug, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct ChangesRequest {
115    pub from: Moment,
116    pub to: Moment,
117    /// `"origins"` (the default) or `"values"`.
118    #[serde(default = "origins")]
119    pub rows: Rows,
120    pub page: QueryPage,
121    #[serde(default)]
122    pub continuation: Option<String>,
123}
124
125const fn origins() -> Rows {
126    Rows::Origins
127}
128
129/// What one call of `changes.list` reads with, all owned by the service.
130pub struct Context<'a> {
131    pub access: &'a Access,
132    pub reader: &'a ReadOnlyDatabase,
133    pub catalogue: &'a Catalogue,
134    pub settings: &'a Settings,
135    pub seal: &'a Seal,
136}
137
138/// One page of the changes `request` asks for.
139///
140/// # Errors
141///
142/// A named `Failure` for every refusal; never a shortened answer.
143pub fn list(
144    context: &Context<'_>,
145    request: &ChangesRequest,
146    response_byte_limit: Option<NonZeroUsize>,
147) -> Result<Box<RawValue>, Failure> {
148    let asked = checked(request, context.settings)?;
149    let token = request
150        .continuation
151        .as_deref()
152        .map(|encoded| Continuation::decode(encoded, context.seal))
153        .transpose()?;
154    if let Some(token) = &token {
155        token.check_moment(&asked.branch, asked.version)?;
156    }
157    // Both moments are held until this answer is built, on every path
158    // (STOR-003 H1b).
159    let mut from = match &request.from {
160        Moment::At(selection) => context.catalogue.capture_held(selection)?,
161        Moment::Fork(fork) => fork_moment(context, &fork.fork)?,
162    };
163    // The head is captured on the first page; a later page reads at the
164    // version its token carries.
165    let mut to = context.catalogue.capture_held(&Selection {
166        branch: asked.branch.clone(),
167        version: token.as_ref().map(|token| token.version).or(asked.version),
168    })?;
169    from.shards.sort_unstable_by_key(|shard| shard.shard_id);
170    to.shards.sort_unstable_by_key(|shard| shard.shard_id);
171    let binding = bind(request, from.version, context.settings)?;
172    if let Some(token) = &token {
173        token.check_binding(&binding)?;
174    }
175    let budget = budget(request.page.bytes, response_byte_limit)?;
176    let widest = widest_next(&to, &binding, context.seal)?;
177    let envelope = answer(&from, &to, &json!([]), Some(&widest), u64::MAX);
178    let allowance = Allowance {
179        rows: request.page.rows,
180        bytes: rows_bytes(budget, &envelope)?,
181        scan_records: context.settings.scan_records.get(),
182    };
183    let resume = token.as_ref().map(|token| &token.resume);
184    let after = resume
185        .map(|resume| revision_key(&resume.pending.collection, &resume.pending.id))
186        .map(|mut key| {
187            key.push(0); // Exclusive of the key given; ids never contain NUL.
188            key
189        });
190    let read_before = resume.map_or(0, |resume| resume.records_read);
191    let mut source = Changes::new(
192        context.reader,
193        &from,
194        &to,
195        after.as_deref(),
196        context.settings,
197    )?;
198    drop(after);
199    let reading = Reading {
200        context,
201        from: &from,
202        to: &to,
203        form: request.rows,
204    };
205    let page = page(&mut source, &reading, &allowance, read_before)?;
206    let next = page
207        .next
208        .map(|place| {
209            let resume = Resume {
210                records_read: page.given,
211                pending: place,
212                discard_outputs: 0,
213                passed: true,
214                passed_counts: Vec::new(),
215            };
216            let now = raw("null")?;
217            Continuation::new(to.branch.clone(), to.version, resume, now, binding)?
218                .encode(context.seal)
219        })
220        .transpose()?;
221    // NB1: the rows are written as text, each body as it is stored, and
222    // never decoded into a value and encoded again.
223    let rows = serde_json::value::to_raw_value(&page.rows).map_err(Failure::storage)?;
224    let envelope = answer(&from, &to, &json!([]), next.as_deref(), page.given);
225    let answer = with_rows(envelope, &rows)?;
226    if answer.get().len() > budget.get() {
227        return Err(Failure::new(
228            "response_limit",
229            "the answer exceeds its requested bytes",
230        ));
231    }
232    Ok(answer)
233}
234
235/// An answer object's members beside a `rows` member written as text (NB1).
236#[derive(Serialize)]
237struct WithRows<'a> {
238    #[serde(flatten)]
239    envelope: &'a serde_json::Map<String, Value>,
240    rows: &'a RawValue,
241}
242
243/// `envelope`, an object, as text with `rows` in place of its own `rows`
244/// member: every body inside `rows` goes out as the text it is.
245///
246/// # Errors
247/// `storage` when `envelope` is not an object or cannot be written.
248pub fn with_rows(envelope: Value, rows: &RawValue) -> Result<Box<RawValue>, Failure> {
249    let Value::Object(mut envelope) = envelope else {
250        return Err(Failure::new(
251            "storage",
252            "an answer envelope is not an object",
253        ));
254    };
255    envelope.remove("rows");
256    serde_json::value::to_raw_value(&WithRows {
257        envelope: &envelope,
258        rows,
259    })
260    .map_err(Failure::storage)
261}
262
263/// `to` as asked, once `to` is no fork, `from` names a version or a fork on
264/// `to`'s branch, and the page is within the service's.
265fn checked<'a>(request: &'a ChangesRequest, settings: &Settings) -> Result<&'a Selection, Failure> {
266    if request.page.rows.get() > u64::from(settings.max_page.get()) {
267        return Err(Failure::new(
268            "page_limit",
269            format!("maximum page size is {}", settings.max_page),
270        ));
271    }
272    let Moment::At(to) = &request.to else {
273        return Err(Failure::new(
274            "changes_fork_as_to",
275            "to names a branch and perhaps a version, never a fork",
276        ));
277    };
278    let branch = match &request.from {
279        Moment::At(from) if from.version.is_none() => {
280            return Err(Failure::new(
281                "changes_version_required",
282                "from names a version or a fork",
283            ));
284        }
285        Moment::At(from) => &from.branch,
286        Moment::Fork(fork) => &fork.fork,
287    };
288    if *branch != to.branch {
289        return Err(Failure::new(
290            "changes_branch_mismatch",
291            "from and to must be on the same branch",
292        ));
293    }
294    Ok(to)
295}
296
297/// Where a branch's fork provenance is kept (storage/branches.rs).
298const FORK_PROVENANCE: &[u8] = b"b\0fork";
299
300/// The moment `branch` forked at: its parent at the version it forked from.
301fn fork_moment(context: &Context<'_>, branch: &str) -> Result<Held, Failure> {
302    let head = context.catalogue.capture_held(&Selection {
303        branch: branch.to_owned(),
304        version: None,
305    })?;
306    let bytes = reads::get_bytes(context.reader, &head, FORK_PROVENANCE)?.ok_or_else(|| {
307        Failure::new(
308            "changes_fork_missing",
309            format!("branch {branch} has no recorded fork origin"),
310        )
311    })?;
312    let fork: ForkProvenance = serde_json::from_slice(&bytes).map_err(Failure::storage)?;
313    context.catalogue.capture_held(&Selection {
314        branch: fork.branch,
315        version: Some(fork.version),
316    })
317}
318
319/// What a continuation is bound to: both moments as asked and the version
320/// `from` was found at, the caller, the page's limits and the row form.
321fn bind(request: &ChangesRequest, from: Version, settings: &Settings) -> Result<Binding, Failure> {
322    let asked = (&request.from, &request.to, from);
323    let moments = serde_json::to_string(&asked).map_err(Failure::storage)?;
324    let engine = Engine {
325        protocol: 1,
326        worker: "haem".to_owned(),
327        engine: "changes".to_owned(),
328        jaq_core: String::new(),
329        jaq_std: String::new(),
330        jaq_json: String::new(),
331    };
332    let limits = Allowance {
333        rows: request.page.rows,
334        bytes: request.page.bytes,
335        scan_records: settings.scan_records.get(),
336    };
337    Binding::new(
338        &moments,
339        &BTreeMap::new(),
340        &engine,
341        &Caller::local("standalone")?,
342        &limits,
343        request.rows,
344    )
345}
346
347fn raw(json: &str) -> Result<Box<RawValue>, Failure> {
348    RawValue::from_string(json.to_owned()).map_err(Failure::storage)
349}
350
351/// The longest `next` a page could give: a place with the longest collection
352/// and id, every byte escaped, and the largest count.
353fn widest_next(to: &Captured, binding: &Binding, seal: &Seal) -> Result<String, Failure> {
354    let resume = Resume {
355        records_read: u64::MAX,
356        pending: Origin {
357            collection: "\u{1}".repeat(ID_BYTES),
358            id: "\u{1}".repeat(ID_BYTES),
359            version: raw("null")?,
360            score: None,
361        },
362        discard_outputs: u64::MAX,
363        passed: false,
364        passed_counts: Vec::new(),
365    };
366    let now = raw("null")?;
367    Continuation::new(to.branch.clone(), to.version, resume, now, binding.clone())?.encode(seal)
368}
369
370/// The smaller of the request's bytes and the transport's limit.
371fn budget(
372    requested: NonZeroU64,
373    response_byte_limit: Option<NonZeroUsize>,
374) -> Result<NonZeroUsize, Failure> {
375    let requested = NonZeroUsize::try_from(requested)
376        .map_err(|_| Failure::new("page_limit", "the requested bytes do not fit this machine"))?;
377    let bytes = response_byte_limit
378        .map_or_else(|| requested.get(), |limit| limit.get().min(requested.get()));
379    NonZeroUsize::new(bytes)
380        .ok_or_else(|| Failure::new("page_limit", "selected byte limit is zero"))
381}
382
383/// The bytes left for the rows once the answer's `envelope` is counted.
384fn rows_bytes(budget: NonZeroUsize, envelope: &Value) -> Result<NonZeroU64, Failure> {
385    let envelope = serde_json::to_vec(envelope)
386        .map_err(Failure::storage)?
387        .len();
388    // The envelope's empty `[]` is counted again by the page as its rows.
389    // It holds room for the widest continuation the page could need, so the
390    // refusal names the size: a caller asks once more, with enough.
391    let needed = envelope - 2;
392    u64::try_from(budget.get())
393        .ok()
394        .and_then(|budget| budget.checked_sub(u64::try_from(needed).ok()?))
395        .and_then(NonZeroU64::new)
396        .ok_or_else(|| {
397            Failure::new(
398                "response_limit",
399                format!(
400                    "the answer's envelope takes {needed} bytes before any row; ask for more than that"
401                ),
402            )
403        })
404}
405
406fn moment(at: &Captured) -> Value {
407    json!({"branch": at.branch, "version": at.version})
408}
409
410fn answer(from: &Captured, to: &Captured, rows: &Value, next: Option<&str>, read: u64) -> Value {
411    json!({
412        "from": moment(from),
413        "to": moment(to),
414        "rows": rows,
415        "next": next,
416        "changes_read": read,
417    })
418}
419
420/// The moments and row form one page is read with.
421struct Reading<'a> {
422    context: &'a Context<'a>,
423    from: &'a Captured,
424    to: &'a Captured,
425    form: Rows,
426}
427
428/// One page's rows, how many changes have been given in all, and where the
429/// next page starts after.
430struct Page {
431    rows: Vec<Row>,
432    given: u64,
433    next: Option<Origin>,
434}
435
436/// The rows of one page: changes the caller may read, until the rows, the
437/// bytes or the source's limit are reached.
438fn page(
439    source: &mut Changes<'_>,
440    reading: &Reading<'_>,
441    allowance: &Allowance,
442    read_before: u64,
443) -> Result<Page, Failure> {
444    let mut page = Page {
445        rows: Vec::new(),
446        given: read_before,
447        next: None,
448    };
449    // The rows array's brackets.
450    let mut bytes = 2_u64;
451    // The last key this page finished with, given or passed.
452    let mut done: Option<Vec<u8>> = None;
453    loop {
454        let given = u64::try_from(page.rows.len()).map_err(Failure::storage)?;
455        if given == allowance.rows.get() {
456            page.next = done.as_deref().map(place).transpose()?;
457            return Ok(page);
458        }
459        let change = match source.next()? {
460            Next::Change(change) => change,
461            Next::Stopped(key) => {
462                page.next = Some(place(&key)?);
463                return Ok(page);
464            }
465            Next::End => return Ok(page),
466        };
467        let key = changed_key(&change).to_vec();
468        let Some(row) = row(reading, change)? else {
469            done = Some(key);
470            continue;
471        };
472        let size = u64::try_from(serde_json::to_vec(&row).map_err(Failure::storage)?.len())
473            .map_err(Failure::storage)?;
474        if size.saturating_add(2) > allowance.bytes.get() {
475            return Err(Failure::new(
476                "changes_row_too_large",
477                format!("a change needs more than {} bytes", allowance.bytes),
478            ));
479        }
480        let grown = bytes
481            .saturating_add(u64::from(given > 0))
482            .saturating_add(size);
483        if grown > allowance.bytes.get() {
484            // Not given: the next page reads it again.
485            page.next = done.as_deref().map(place).transpose()?;
486            return Ok(page);
487        }
488        bytes = grown;
489        page.given = page.given.saturating_add(1);
490        page.rows.push(row);
491        done = Some(key);
492    }
493}
494
495fn changed_key(change: &ObservedChange) -> &[u8] {
496    match change {
497        ObservedChange::Added { key, .. }
498        | ObservedChange::Removed { key, .. }
499        | ObservedChange::Modified { key, .. } => key,
500    }
501}
502
503/// A revision key's collection and id.
504fn parts(key: &[u8]) -> Result<(&str, &str), Failure> {
505    key.strip_prefix(REVISIONS)
506        .and_then(|rest| std::str::from_utf8(rest).ok())
507        .and_then(|rest| rest.split_once('\0'))
508        .ok_or_else(|| Failure::new("changes_key_corrupt", "a revision key names no record"))
509}
510
511/// The place after which the next page starts.
512fn place(key: &[u8]) -> Result<Origin, Failure> {
513    let (collection, id) = parts(key)?;
514    Ok(Origin {
515        collection: collection.to_owned(),
516        id: id.to_owned(),
517        version: raw("null")?,
518        score: None,
519    })
520}
521
522fn written_at(revision: &[u8]) -> Result<Version, Failure> {
523    serde_json::from_slice::<DocumentRevision>(revision)
524        .map(|revision| revision.written_at)
525        .map_err(|error| Failure::new("changes_revision_corrupt", error.to_string()))
526}
527
528/// A changed record's name.
529#[derive(Serialize)]
530struct Named {
531    collection: String,
532    id: String,
533}
534
535/// One change. Without values, `value_before` and `value_after` are absent;
536/// with them, each is the body at its moment, or null where there is none.
537#[derive(Serialize)]
538struct Row {
539    kind: &'static str,
540    origin: Named,
541    before: Option<Version>,
542    after: Option<Version>,
543    #[serde(skip_serializing_if = "Option::is_none")]
544    value_before: Option<Box<RawValue>>,
545    #[serde(skip_serializing_if = "Option::is_none")]
546    value_after: Option<Box<RawValue>>,
547}
548
549/// The row for `change`, or none when the caller may not read its record.
550fn row(reading: &Reading<'_>, change: ObservedChange) -> Result<Option<Row>, Failure> {
551    let (key, kind, before, after) = match change {
552        ObservedChange::Added { key, value } => (key, "added", None, Some(value)),
553        ObservedChange::Removed { key, value } => (key, "removed", Some(value), None),
554        ObservedChange::Modified {
555            key,
556            old_value,
557            new_value,
558        } => (key, "changed", Some(old_value), Some(new_value)),
559    };
560    let (collection, id) = parts(&key)?;
561    if !reading.context.access.can_read_document(collection, id) {
562        return Ok(None);
563    }
564    let before = before.as_deref().map(written_at).transpose()?;
565    let after = after.as_deref().map(written_at).transpose()?;
566    let mut row = Row {
567        kind,
568        origin: Named {
569            collection: collection.to_owned(),
570            id: id.to_owned(),
571        },
572        before,
573        after,
574        value_before: None,
575        value_after: None,
576    };
577    if reading.form == Rows::Values {
578        let body = document_key(collection, id);
579        row.value_before = Some(body_at(reading, reading.from, &body, before.is_some())?);
580        row.value_after = Some(body_at(reading, reading.to, &body, after.is_some())?);
581    }
582    Ok(Some(row))
583}
584
585/// The body at `at` of a record whose revision is there (`present`), else
586/// null: the stored text as it is (NB1), checked to be JSON and never
587/// decoded into a value, so every number keeps the text it was written with.
588fn body_at(
589    reading: &Reading<'_>,
590    at: &Captured,
591    key: &[u8],
592    present: bool,
593) -> Result<Box<RawValue>, Failure> {
594    if !present {
595        return raw("null");
596    }
597    #[cfg(test)]
598    BODIES.with(|count| count.set(count.get() + 1));
599    let bytes = reads::get_bytes(reading.context.reader, at, key)?
600        .ok_or_else(|| Failure::new("changes_body_missing", "a revision has no body"))?;
601    let text = std::str::from_utf8(&bytes).map_err(Failure::storage)?;
602    RawValue::from_string(text.to_owned()).map_err(Failure::storage)
603}
604
605/// What the source gives next.
606enum Next {
607    Change(ObservedChange),
608    /// The limit is reached with changes left: the last key taken.
609    Stopped(Vec<u8>),
610    End,
611}
612
613/// One shard's diff, read ahead after its last key.
614struct Shard {
615    id: usize,
616    from: Hash,
617    to: Hash,
618    after: Option<Vec<u8>>,
619    ahead: VecDeque<ObservedChange>,
620    ended: bool,
621}
622
623/// The revision changes of every shard between two captures, in key order.
624struct Changes<'a> {
625    reader: &'a ReadOnlyDatabase,
626    shards: Vec<Shard>,
627    node_budget: NonZeroUsize,
628    limit: u64,
629    taken: u64,
630    last: Option<Vec<u8>>,
631}
632
633impl<'a> Changes<'a> {
634    fn new(
635        reader: &'a ReadOnlyDatabase,
636        from: &Captured,
637        to: &Captured,
638        after: Option<&[u8]>,
639        settings: &Settings,
640    ) -> Result<Self, Failure> {
641        if from.shards.len() != to.shards.len() {
642            return Err(Failure::new(
643                "changes_shards_differ",
644                "the two moments do not have the same shards",
645            ));
646        }
647        let mut shards = Vec::with_capacity(to.shards.len());
648        for (before, now) in from.shards.iter().zip(&to.shards) {
649            if before.shard_id != now.shard_id {
650                return Err(Failure::new(
651                    "changes_shards_differ",
652                    "the two moments do not name the same shards",
653                ));
654            }
655            shards.push(Shard {
656                id: now.shard_id,
657                from: before.head,
658                to: now.head,
659                after: after.map(<[u8]>::to_vec),
660                ahead: VecDeque::new(),
661                // Equal roots have no changes: nothing is read.
662                ended: before.head == now.head,
663            });
664        }
665        Ok(Self {
666            reader,
667            shards,
668            node_budget: settings.node_budget,
669            limit: settings.scan_records.get(),
670            taken: 0,
671            last: None,
672        })
673    }
674
675    /// As many changes as this page may still take, at most `BATCH`, at least one.
676    fn batch(&self) -> NonZeroUsize {
677        let left = usize::try_from(self.limit.saturating_sub(self.taken)).unwrap_or(usize::MAX);
678        NonZeroUsize::new(left.min(BATCH)).unwrap_or(NonZeroUsize::MIN)
679    }
680
681    /// Reads on every shard whose read-ahead is empty until it has a change or
682    /// its diff is done; a diff page may be empty with more to come.
683    fn fill(&mut self) -> Result<(), Failure> {
684        let budget = DiffBudget {
685            max_changes: self.batch(),
686            max_nodes: self.node_budget,
687        };
688        let interval = DiffInterval {
689            lower: Some(REVISIONS),
690            upper: Some(REVISIONS_END),
691        };
692        for shard in &mut self.shards {
693            while shard.ahead.is_empty() && !shard.ended {
694                #[cfg(test)]
695                DIFFS.with(|count| count.set(count.get() + 1));
696                let page = self
697                    .reader
698                    .changes_page_in_range(
699                        shard.id,
700                        shard.from,
701                        shard.to,
702                        interval,
703                        shard.after.as_deref(),
704                        budget,
705                    )
706                    .map_err(diff_failure)?;
707                shard.ended = page.next_after.is_none();
708                shard.after = page.next_after;
709                shard.ahead.extend(page.entries);
710            }
711        }
712        Ok(())
713    }
714
715    fn next(&mut self) -> Result<Next, Failure> {
716        if self.taken >= self.limit {
717            if self.shards.iter().all(|shard| shard.ahead.is_empty()) {
718                self.fill()?;
719            }
720            if self.shards.iter().all(|shard| shard.ahead.is_empty()) {
721                return Ok(Next::End);
722            }
723            let last = self.last.clone().ok_or_else(|| {
724                Failure::new(
725                    "changes_scan_limit",
726                    "a page that may take no changes moves nowhere",
727                )
728            })?;
729            return Ok(Next::Stopped(last));
730        }
731        self.fill()?;
732        let Some(shard) = self
733            .shards
734            .iter_mut()
735            .filter(|shard| !shard.ahead.is_empty())
736            .min_by(|left, right| {
737                let left = left.ahead.front().map(changed_key);
738                let right = right.ahead.front().map(changed_key);
739                left.cmp(&right)
740            })
741        else {
742            return Ok(Next::End);
743        };
744        let Some(change) = shard.ahead.pop_front() else {
745            return Ok(Next::End);
746        };
747        self.taken += 1;
748        self.last = Some(changed_key(&change).to_vec());
749        Ok(Next::Change(change))
750    }
751}
752
753/// A moment whose nodes are gone is named, never read around.
754fn diff_failure(error: ObserverError) -> Failure {
755    let classification = match &error {
756        ObserverError::Tree(TreeError::MissingNode { hash }) => Failure::new(
757            "changes_moment_gone",
758            format!("the moment no longer holds node {hash}"),
759        ),
760        ObserverError::RootUnavailable { shard_id, root } => Failure::new(
761            "changes_moment_gone",
762            format!("shard {shard_id} cannot read root {root}"),
763        ),
764        ObserverError::Diff(error @ DiffError::NodeBudgetTooSmall { .. }) => {
765            Failure::new("changes_node_budget", error.to_string())
766        }
767        other => Failure::storage(other),
768    };
769    let mut failure = Failure::observer(error);
770    failure.code = classification.code;
771    failure.message = classification.message;
772    failure.path = classification.path;
773    failure
774}
775
776#[cfg(test)]
777#[path = "changes_tests.rs"]
778mod tests;
779
780#[cfg(test)]
781#[path = "held_reads_tests.rs"]
782mod held_reads_tests;