Skip to main content

acyclic_stream/
memory.rs

1//! Single-lock deterministic reference state machine; no durability or availability claim.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    sync::Arc,
6};
7
8use async_trait::async_trait;
9use bytes::Bytes;
10use futures::{StreamExt as _, stream};
11use sha2::{Digest, Sha256};
12use tokio::sync::{RwLock, watch};
13
14use crate::{
15    AppendOutcome, AppendReceipt, AppendRequest, Child, ChildStream, ChildrenRequest,
16    CommitCondition, CommitConflict, CommitId, CommitMutation, CommitOutcome, CommitRequest,
17    CommittedAppend, CommittedDelete, CommittedEnvelope, CommittedFork, CommittedMutation,
18    CommittedTrim, DeleteReceipt, ForkReceipt, ForkRequest, IdempotencyKey, IdempotencyObservation,
19    IdempotencyOutcome, MAX_COMMAND_BYTES, MAX_ITEMS, MAX_RECORD_BYTES, ReadRequest, Record,
20    RecordStream, StreamError, StreamPath, StreamProvider, SystemUnixMillisClock, TrimReceipt,
21    UnixMillisClock,
22};
23
24/// Explicit fail-closed process-memory ceilings.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct MemoryLimits {
27    /// Maximum created paths, including implicit parents.
28    pub paths: usize,
29    /// Maximum unique retained path bytes.
30    pub path_bytes: usize,
31    /// Maximum unique committed records.
32    pub records: usize,
33    /// Maximum unique retained payload bytes.
34    pub payload_bytes: usize,
35    /// Maximum immutable commit envelopes.
36    pub commits: usize,
37    /// Maximum stable replay identities retained indefinitely.
38    pub idempotency_results: usize,
39}
40
41impl Default for MemoryLimits {
42    fn default() -> Self {
43        Self {
44            paths: 100_000,
45            path_bytes: 64 * 1024 * 1024,
46            records: 1_000_000,
47            payload_bytes: 256 * 1024 * 1024,
48            commits: 1_000_000,
49            idempotency_results: 1_000_000,
50        }
51    }
52}
53
54/// Deterministic process-local provider. It retains idempotency results indefinitely, satisfying
55/// the minimum 24-hour contract while the process exists; it deliberately claims no crash recovery.
56#[derive(Clone)]
57pub struct MemoryStream {
58    state: Arc<RwLock<State>>,
59    limits: MemoryLimits,
60    clock: Arc<dyn UnixMillisClock>,
61}
62
63impl Default for MemoryStream {
64    fn default() -> Self {
65        Self::new(MemoryLimits::default())
66    }
67}
68
69impl MemoryStream {
70    /// Constructs one bounded independent provider.
71    #[must_use]
72    pub fn new(limits: MemoryLimits) -> Self {
73        Self::new_with_clock(limits, Arc::new(SystemUnixMillisClock))
74    }
75
76    /// Constructs a provider with an injected trusted clock.
77    #[must_use]
78    pub fn new_with_clock(limits: MemoryLimits, clock: Arc<dyn UnixMillisClock>) -> Self {
79        Self {
80            state: Arc::new(RwLock::new(State::default())),
81            limits,
82            clock,
83        }
84    }
85
86    async fn commit_inner(
87        &self,
88        mut request: CommitRequest,
89        deadline_unix_millis: Option<u64>,
90    ) -> Result<CommitOutcome, StreamError> {
91        normalize_commit(&mut request)?;
92        validate_commit_shape(&request)?;
93        let digest = commit_digest(&request);
94        let mut state = self.state.write().await;
95        if let Some(result) = replay_commit(&state, &request.idempotency_key, digest)? {
96            return Ok(result);
97        }
98        if deadline_unix_millis.is_some_and(|deadline| self.clock.now_unix_millis() >= deadline) {
99            return Err(StreamError::DeadlineElapsed);
100        }
101        admit_replay(&state, Some(&request.idempotency_key), self.limits)?;
102        let conflicts = commit_conflicts(&state, &request.conditions);
103        if !conflicts.is_empty() {
104            let result = CommitOutcome::Conflict(conflicts);
105            retain_replay(
106                &mut state,
107                Some(request.idempotency_key),
108                digest,
109                IdempotencyOutcome::Commit(result.clone()),
110            );
111            return Ok(result);
112        }
113        validate_commit_authority(&state, &request)?;
114        reserve_coordinated(&state, &request, self.limits)?;
115        let commit_id = next_commit_id(&mut state, digest)?;
116        let before = request
117            .mutations
118            .iter()
119            .filter_map(|mutation| match mutation {
120                CommitMutation::Fork { source, .. } => state
121                    .paths
122                    .get(source)
123                    .map(|stream| (source.clone(), (stream.history.clone(), stream.tail))),
124                CommitMutation::Append { .. }
125                | CommitMutation::Trim { .. }
126                | CommitMutation::Delete { .. } => None,
127            })
128            .collect::<BTreeMap<_, _>>();
129        let mutations = apply_coordinated(&mut state, request.mutations, &before, commit_id)?;
130        let envelope = CommittedEnvelope {
131            commit_id,
132            mutations,
133        };
134        state.commits.insert(commit_id, envelope.clone());
135        let result = CommitOutcome::Committed(envelope);
136        retain_replay(
137            &mut state,
138            Some(request.idempotency_key),
139            digest,
140            IdempotencyOutcome::Commit(result.clone()),
141        );
142        Ok(result)
143    }
144}
145
146#[derive(Default)]
147struct State {
148    paths: BTreeMap<StreamPath, PathState>,
149    retired: BTreeSet<StreamPath>,
150    commits: BTreeMap<CommitId, CommittedEnvelope>,
151    replays: BTreeMap<Bytes, Replay>,
152    path_bytes: usize,
153    record_count: usize,
154    payload_bytes: usize,
155    decision: u64,
156}
157
158struct PathState {
159    history: Option<Arc<History>>,
160    tail: u64,
161    trim_point: u64,
162    changed: watch::Sender<u64>,
163}
164
165enum History {
166    Batch {
167        parent: Option<Arc<Self>>,
168        records: Arc<[Record]>,
169    },
170    Prefix {
171        source: Option<Arc<Self>>,
172        tail: u64,
173    },
174}
175
176#[derive(Clone)]
177struct Replay {
178    digest: [u8; 32],
179    result: IdempotencyOutcome,
180}
181
182#[async_trait]
183impl StreamProvider for MemoryStream {
184    async fn inspect_idempotency(
185        &self,
186        idempotency_key: IdempotencyKey,
187    ) -> Result<Option<IdempotencyObservation>, StreamError> {
188        let state = self.state.read().await;
189        Ok(state
190            .replays
191            .get(idempotency_key.as_bytes())
192            .map(|replay| IdempotencyObservation {
193                idempotency_key,
194                request_digest: replay.digest,
195                outcome: replay.result.clone(),
196            }))
197    }
198
199    async fn tail(&self, path: StreamPath) -> Result<u64, StreamError> {
200        let state = self.state.read().await;
201        reject_retired(&state, &path)?;
202        state
203            .paths
204            .get(&path)
205            .map(|stream| stream.tail)
206            .ok_or(StreamError::NotFound)
207    }
208
209    async fn append(&self, request: AppendRequest) -> Result<AppendOutcome, StreamError> {
210        validate_records(&request.records)?;
211        validate_append_size(&request)?;
212        let digest = append_digest(&request);
213        let mut state = self.state.write().await;
214        if let Some(result) = replay_append(&state, request.idempotency_key.as_ref(), digest)? {
215            return Ok(result);
216        }
217        admit_replay(&state, request.idempotency_key.as_ref(), self.limits)?;
218        reject_retired(&state, &request.path)?;
219        let actual_tail = state.paths.get(&request.path).map_or(0, |path| path.tail);
220        if request
221            .if_tail
222            .is_some_and(|expected| expected != actual_tail)
223        {
224            let result = AppendOutcome::TailConflict { actual_tail };
225            retain_replay(
226                &mut state,
227                request.idempotency_key,
228                digest,
229                IdempotencyOutcome::Append(result.clone()),
230            );
231            return Ok(result);
232        }
233        reserve_append(&state, &request.path, &request.records, self.limits)?;
234        let commit_id = next_commit_id(&mut state, digest)?;
235        let records = build_records(actual_tail, request.records, commit_id)?;
236        ensure_path(&mut state, &request.path);
237        let resulting_tail = {
238            let stream = state
239                .paths
240                .get_mut(&request.path)
241                .ok_or(StreamError::Unavailable)?;
242            stream.history = Some(Arc::new(History::Batch {
243                parent: stream.history.clone(),
244                records: Arc::from(records.clone()),
245            }));
246            stream.tail = records
247                .last()
248                .map_or(actual_tail, |record| record.sequence + 1);
249            stream.changed.send_replace(stream.tail);
250            stream.tail
251        };
252        state.record_count += records.len();
253        state.payload_bytes += records
254            .iter()
255            .map(|record| record.value.len())
256            .sum::<usize>();
257        let receipt = AppendReceipt {
258            start: actual_tail,
259            end: resulting_tail,
260            tail: resulting_tail,
261            commit_id,
262        };
263        let envelope = CommittedEnvelope {
264            commit_id,
265            mutations: vec![CommittedMutation::Append(CommittedAppend {
266                path: request.path,
267                start: receipt.start,
268                end: receipt.end,
269                tail: receipt.tail,
270                records,
271            })],
272        };
273        state.commits.insert(commit_id, envelope);
274        let result = AppendOutcome::Committed(receipt);
275        retain_replay(
276            &mut state,
277            request.idempotency_key,
278            digest,
279            IdempotencyOutcome::Append(result.clone()),
280        );
281        Ok(result)
282    }
283
284    async fn fork(&self, request: ForkRequest) -> Result<ForkReceipt, StreamError> {
285        if request.source == request.destination {
286            return Err(StreamError::InvalidArgument);
287        }
288        validate_fork_size(&request)?;
289        let digest = fork_digest(&request);
290        let mut state = self.state.write().await;
291        if let Some(result) = replay_fork(&state, request.idempotency_key.as_ref(), digest)? {
292            return Ok(result);
293        }
294        admit_replay(&state, request.idempotency_key.as_ref(), self.limits)?;
295        reject_retired(&state, &request.destination)?;
296        if state.paths.contains_key(&request.destination) {
297            return Err(StreamError::AlreadyExists);
298        }
299        let source = state
300            .paths
301            .get(&request.source)
302            .ok_or(StreamError::NotFound)?;
303        let forked_at = request.at_tail.unwrap_or(source.tail);
304        if forked_at > source.tail {
305            return Err(StreamError::PrefixNotRetained);
306        }
307        let source_history = source.history.clone();
308        reserve_paths(&state, &request.destination, self.limits)?;
309        reserve_commit(&state, self.limits)?;
310        let commit_id = next_commit_id(&mut state, digest)?;
311        ensure_path(&mut state, &request.destination);
312        let destination = state
313            .paths
314            .get_mut(&request.destination)
315            .ok_or(StreamError::Unavailable)?;
316        destination.history = Some(Arc::new(History::Prefix {
317            source: source_history,
318            tail: forked_at,
319        }));
320        destination.tail = forked_at;
321        destination.changed.send_replace(forked_at);
322        let receipt = ForkReceipt {
323            source: request.source,
324            destination: request.destination,
325            forked_at,
326            tail: forked_at,
327            commit_id,
328        };
329        state.commits.insert(
330            commit_id,
331            CommittedEnvelope {
332                commit_id,
333                mutations: vec![CommittedMutation::Fork(CommittedFork {
334                    source: receipt.source.clone(),
335                    destination: receipt.destination.clone(),
336                    forked_at,
337                    tail: forked_at,
338                })],
339            },
340        );
341        retain_replay(
342            &mut state,
343            request.idempotency_key,
344            digest,
345            IdempotencyOutcome::Fork(receipt.clone()),
346        );
347        Ok(receipt)
348    }
349
350    async fn trim(
351        &self,
352        path: StreamPath,
353        before: u64,
354        idempotency_key: IdempotencyKey,
355    ) -> Result<TrimReceipt, StreamError> {
356        let digest = trim_digest(&path, before);
357        let mut state = self.state.write().await;
358        if let Some(result) = replay_trim(&state, &idempotency_key, digest)? {
359            return Ok(result);
360        }
361        admit_replay(&state, Some(&idempotency_key), self.limits)?;
362        reserve_commit(&state, self.limits)?;
363        let current = state.paths.get(&path).ok_or(StreamError::NotFound)?;
364        if before > current.tail {
365            return Err(StreamError::OutOfRange);
366        }
367        let trim_point = current.trim_point.max(before);
368        let commit_id = next_commit_id(&mut state, digest)?;
369        let stream = state.paths.get_mut(&path).ok_or(StreamError::Unavailable)?;
370        stream.trim_point = trim_point;
371        stream.changed.send_replace(stream.tail);
372        let receipt = TrimReceipt {
373            path: path.clone(),
374            trim_point,
375            commit_id,
376        };
377        state.commits.insert(
378            commit_id,
379            CommittedEnvelope {
380                commit_id,
381                mutations: vec![CommittedMutation::Trim(CommittedTrim { path, trim_point })],
382            },
383        );
384        retain_replay(
385            &mut state,
386            Some(idempotency_key),
387            digest,
388            IdempotencyOutcome::Trim(receipt.clone()),
389        );
390        Ok(receipt)
391    }
392
393    async fn delete(
394        &self,
395        path: StreamPath,
396        idempotency_key: IdempotencyKey,
397    ) -> Result<DeleteReceipt, StreamError> {
398        let digest = delete_digest(&path);
399        let mut state = self.state.write().await;
400        if let Some(result) = replay_delete(&state, &idempotency_key, digest)? {
401            return Ok(result);
402        }
403        admit_replay(&state, Some(&idempotency_key), self.limits)?;
404        reserve_commit(&state, self.limits)?;
405        if !state.paths.contains_key(&path) {
406            return Err(StreamError::NotFound);
407        }
408        if state
409            .paths
410            .keys()
411            .any(|candidate| is_descendant(&path, candidate))
412        {
413            return Err(StreamError::InvalidArgument);
414        }
415        let commit_id = next_commit_id(&mut state, digest)?;
416        state.paths.remove(&path);
417        state.retired.insert(path.clone());
418        let receipt = DeleteReceipt {
419            path: path.clone(),
420            commit_id,
421        };
422        state.commits.insert(
423            commit_id,
424            CommittedEnvelope {
425                commit_id,
426                mutations: vec![CommittedMutation::Delete(CommittedDelete { path })],
427            },
428        );
429        retain_replay(
430            &mut state,
431            Some(idempotency_key),
432            digest,
433            IdempotencyOutcome::Delete(receipt.clone()),
434        );
435        Ok(receipt)
436    }
437
438    async fn read(&self, request: ReadRequest) -> Result<RecordStream, StreamError> {
439        validate_limit(request.limit)?;
440        let state = self.state.read().await;
441        reject_retired(&state, &request.path)?;
442        let path = state
443            .paths
444            .get(&request.path)
445            .ok_or(StreamError::NotFound)?;
446        if request.from > path.tail {
447            return Err(StreamError::OutOfRange);
448        }
449        if request.from < path.trim_point {
450            return Err(StreamError::OutOfRange);
451        }
452        let records = read_history(
453            path.history.as_ref(),
454            path.tail,
455            request.from,
456            usize::try_from(request.limit).map_err(|_| StreamError::LimitExceeded)?,
457        );
458        Ok(stream::iter(records.into_iter().map(Ok)).boxed())
459    }
460
461    async fn follow(&self, path: StreamPath, from: u64) -> Result<RecordStream, StreamError> {
462        let (receiver, cursor) = {
463            let state = self.state.read().await;
464            reject_retired(&state, &path)?;
465            let stream = state.paths.get(&path).ok_or(StreamError::NotFound)?;
466            if from > stream.tail {
467                return Err(StreamError::OutOfRange);
468            }
469            if from < stream.trim_point {
470                return Err(StreamError::OutOfRange);
471            }
472            (
473                stream.changed.subscribe(),
474                HistoryCursor::new(stream.history.as_ref(), stream.tail, from),
475            )
476        };
477        let state = Arc::clone(&self.state);
478        Ok(stream::unfold(
479            (state, path, receiver, cursor),
480            |(state, path, mut receiver, mut cursor)| async move {
481                loop {
482                    let record = {
483                        let guard = state.read().await;
484                        if reject_retired(&guard, &path).is_err() {
485                            Err(StreamError::Retired)
486                        } else if let Some(current) = guard.paths.get(&path) {
487                            if cursor.next < current.trim_point {
488                                Err(StreamError::OutOfRange)
489                            } else {
490                                cursor.sync(current.history.as_ref(), current.tail);
491                                Ok(cursor.next_record())
492                            }
493                        } else {
494                            Err(StreamError::NotFound)
495                        }
496                    };
497                    let Some(record) = (match record {
498                        Ok(record) => record,
499                        Err(error) => {
500                            return Some((Err(error), (state, path, receiver, cursor)));
501                        }
502                    }) else {
503                        if receiver.changed().await.is_err() {
504                            return Some((
505                                Err(StreamError::Unavailable),
506                                (state, path, receiver, cursor),
507                            ));
508                        }
509                        continue;
510                    };
511                    return Some((Ok(record), (state, path, receiver, cursor)));
512                }
513            },
514        )
515        .boxed())
516    }
517
518    async fn children(&self, request: ChildrenRequest) -> Result<ChildStream, StreamError> {
519        validate_limit(request.limit)?;
520        let state = self.state.read().await;
521        let limit = usize::try_from(request.limit).map_err(|_| StreamError::LimitExceeded)?;
522        let children = state
523            .paths
524            .keys()
525            .filter(|path| {
526                request.parent.as_ref().map_or_else(
527                    || !path.as_str().contains('/'),
528                    |parent| is_direct_child(parent, path),
529                )
530            })
531            .take(limit)
532            .cloned()
533            .map(|path| Child { path })
534            .collect::<Vec<_>>();
535        Ok(stream::iter(children.into_iter().map(Ok)).boxed())
536    }
537
538    async fn commit(&self, request: CommitRequest) -> Result<CommitOutcome, StreamError> {
539        self.commit_inner(request, None).await
540    }
541
542    async fn commit_before(
543        &self,
544        request: CommitRequest,
545        deadline_unix_millis: u64,
546    ) -> Result<CommitOutcome, StreamError> {
547        self.commit_inner(request, Some(deadline_unix_millis)).await
548    }
549
550    async fn read_commit(&self, commit_id: CommitId) -> Result<CommittedEnvelope, StreamError> {
551        self.state
552            .read()
553            .await
554            .commits
555            .get(&commit_id)
556            .cloned()
557            .ok_or(StreamError::NotFound)
558    }
559}
560
561fn validate_limit(limit: u32) -> Result<(), StreamError> {
562    if limit == 0 || usize::try_from(limit).map_or(true, |limit| limit > MAX_ITEMS) {
563        Err(StreamError::LimitExceeded)
564    } else {
565        Ok(())
566    }
567}
568
569fn validate_records(records: &[Bytes]) -> Result<(), StreamError> {
570    if records.is_empty() || records.len() > MAX_ITEMS {
571        return Err(StreamError::LimitExceeded);
572    }
573    let mut total = 0_usize;
574    for record in records {
575        if record.len() > MAX_RECORD_BYTES {
576            return Err(StreamError::LimitExceeded);
577        }
578        total = total
579            .checked_add(record.len())
580            .and_then(|value| value.checked_add(8))
581            .ok_or(StreamError::LimitExceeded)?;
582    }
583    if total > MAX_COMMAND_BYTES {
584        return Err(StreamError::LimitExceeded);
585    }
586    Ok(())
587}
588
589fn add_size(total: &mut usize, amount: usize) -> Result<(), StreamError> {
590    *total = total
591        .checked_add(amount)
592        .ok_or(StreamError::LimitExceeded)?;
593    if *total > MAX_COMMAND_BYTES {
594        return Err(StreamError::LimitExceeded);
595    }
596    Ok(())
597}
598
599fn add_path_size(total: &mut usize, path: &StreamPath) -> Result<(), StreamError> {
600    add_size(total, 8)?;
601    add_size(total, path.as_str().len())
602}
603
604fn add_records_size(total: &mut usize, records: &[Bytes]) -> Result<(), StreamError> {
605    add_size(total, 8)?;
606    for record in records {
607        add_size(total, 8)?;
608        add_size(total, record.len())?;
609    }
610    Ok(())
611}
612
613fn validate_append_size(request: &AppendRequest) -> Result<(), StreamError> {
614    let mut total = 1 + 8 + 1;
615    add_path_size(&mut total, &request.path)?;
616    add_records_size(&mut total, &request.records)?;
617    if let Some(key) = &request.idempotency_key {
618        add_size(&mut total, 8 + key.as_bytes().len())?;
619    }
620    Ok(())
621}
622
623fn validate_fork_size(request: &ForkRequest) -> Result<(), StreamError> {
624    let mut total = 1 + 8 + 1;
625    add_path_size(&mut total, &request.source)?;
626    add_path_size(&mut total, &request.destination)?;
627    if let Some(key) = &request.idempotency_key {
628        add_size(&mut total, 8 + key.as_bytes().len())?;
629    }
630    Ok(())
631}
632
633fn reject_retired(state: &State, path: &StreamPath) -> Result<(), StreamError> {
634    let mut current = Some(path.clone());
635    while let Some(path) = current {
636        if state.retired.contains(&path) {
637            return Err(StreamError::Retired);
638        }
639        current = path.parent();
640    }
641    Ok(())
642}
643
644fn missing_paths(state: &State, path: &StreamPath) -> usize {
645    let mut count = 0;
646    let mut current = Some(path.clone());
647    while let Some(path) = current {
648        count += usize::from(!state.paths.contains_key(&path));
649        current = path.parent();
650    }
651    count
652}
653
654fn reserve_paths(
655    state: &State,
656    path: &StreamPath,
657    limits: MemoryLimits,
658) -> Result<(), StreamError> {
659    let missing_path_bytes = missing_path_bytes(state, path);
660    if state.paths.len().saturating_add(missing_paths(state, path)) > limits.paths
661        || state.path_bytes.saturating_add(missing_path_bytes) > limits.path_bytes
662    {
663        Err(StreamError::Capacity)
664    } else {
665        Ok(())
666    }
667}
668
669fn missing_path_bytes(state: &State, path: &StreamPath) -> usize {
670    let mut bytes = 0_usize;
671    let mut current = Some(path.clone());
672    while let Some(path) = current {
673        if !state.paths.contains_key(&path) {
674            bytes = bytes.saturating_add(path.as_str().len());
675        }
676        current = path.parent();
677    }
678    bytes
679}
680
681fn reserve_commit(state: &State, limits: MemoryLimits) -> Result<(), StreamError> {
682    if state.commits.len() >= limits.commits {
683        Err(StreamError::Capacity)
684    } else {
685        Ok(())
686    }
687}
688
689fn reserve_append(
690    state: &State,
691    path: &StreamPath,
692    records: &[Bytes],
693    limits: MemoryLimits,
694) -> Result<(), StreamError> {
695    reserve_paths(state, path, limits)?;
696    reserve_commit(state, limits)?;
697    let bytes = records.iter().map(Bytes::len).sum::<usize>();
698    if state.record_count.saturating_add(records.len()) > limits.records
699        || state.payload_bytes.saturating_add(bytes) > limits.payload_bytes
700    {
701        return Err(StreamError::Capacity);
702    }
703    Ok(())
704}
705
706fn ensure_path(state: &mut State, path: &StreamPath) {
707    let mut missing = Vec::new();
708    let mut current = Some(path.clone());
709    while let Some(path) = current {
710        if state.paths.contains_key(&path) {
711            break;
712        }
713        current = path.parent();
714        missing.push(path);
715    }
716    for path in missing.into_iter().rev() {
717        let (changed, _) = watch::channel(0);
718        state.path_bytes = state.path_bytes.saturating_add(path.as_str().len());
719        state.paths.insert(
720            path,
721            PathState {
722                history: None,
723                tail: 0,
724                trim_point: 0,
725                changed,
726            },
727        );
728    }
729}
730
731fn build_records(
732    start: u64,
733    bodies: Vec<Bytes>,
734    commit_id: CommitId,
735) -> Result<Vec<Record>, StreamError> {
736    bodies
737        .into_iter()
738        .enumerate()
739        .map(|(index, value)| {
740            let sequence = start
741                .checked_add(u64::try_from(index).map_err(|_| StreamError::LimitExceeded)?)
742                .ok_or(StreamError::LimitExceeded)?;
743            Ok(Record {
744                sequence,
745                value,
746                commit_id,
747            })
748        })
749        .collect()
750}
751
752struct HistoryWindow {
753    records: Arc<[Record]>,
754    start: usize,
755    end: usize,
756}
757
758/// Incremental cursor over the immutable history graph used by `follow`.
759///
760/// A follow can live for a long time while one-record batches are appended.
761/// Replaying the linked graph from its root for every item makes that workload
762/// quadratic.  The cursor indexes the initial graph once and then only walks
763/// the newly added batch suffix after each publication.
764struct HistoryCursor {
765    root: Option<Arc<History>>,
766    windows: Vec<HistoryWindow>,
767    window: usize,
768    record: usize,
769    next: u64,
770}
771
772impl HistoryCursor {
773    fn new(history: Option<&Arc<History>>, tail: u64, next: u64) -> Self {
774        let mut cursor = Self {
775            root: None,
776            windows: Vec::new(),
777            window: 0,
778            record: 0,
779            next,
780        };
781        cursor.rebuild(history, tail);
782        cursor
783    }
784
785    fn rebuild(&mut self, root: Option<&Arc<History>>, tail: u64) {
786        self.root = root.cloned();
787        self.windows = collect_history_windows(root, tail);
788        self.window = 0;
789        self.record = 0;
790        self.seek_next();
791    }
792
793    fn sync(&mut self, history: Option<&Arc<History>>, tail: u64) {
794        let Some(root) = history else {
795            self.rebuild(None, tail);
796            return;
797        };
798        if self.root.as_ref().is_some_and(|old| Arc::ptr_eq(old, root)) {
799            return;
800        }
801
802        // Appends form a new Batch chain whose parent is the previous root.
803        // Walk that suffix once, then attach its windows in chronological order.
804        // A Prefix in a changed suffix can alter visibility of old batches, so
805        // use the bounded full rebuild for that uncommon graph transition.
806        let mut cursor = Some(Arc::clone(root));
807        let mut suffix = Vec::new();
808        let mut reaches_old = false;
809        while let Some(node) = cursor {
810            if self
811                .root
812                .as_ref()
813                .is_some_and(|old| Arc::ptr_eq(old, &node))
814            {
815                reaches_old = true;
816                break;
817            }
818            match node.as_ref() {
819                History::Batch { parent, records } => {
820                    suffix.push(Arc::clone(records));
821                    cursor = parent.clone();
822                }
823                History::Prefix { .. } => break,
824            }
825        }
826        if !reaches_old {
827            self.rebuild(Some(root), tail);
828            return;
829        }
830
831        suffix.reverse();
832        let mut added = suffix
833            .into_iter()
834            .filter_map(|records| history_window(records, tail))
835            .collect::<Vec<_>>();
836        self.windows.append(&mut added);
837        self.root = Some(Arc::clone(root));
838    }
839
840    fn seek_next(&mut self) {
841        while let Some(window) = self.windows.get(self.window) {
842            while self.record < window.end.saturating_sub(window.start) {
843                let index = window.start + self.record;
844                let Some(record) = window.records.get(index) else {
845                    break;
846                };
847                if record.sequence >= self.next {
848                    return;
849                }
850                self.record += 1;
851            }
852            self.window += 1;
853            self.record = 0;
854        }
855    }
856
857    fn next_record(&mut self) -> Option<Record> {
858        self.seek_next();
859        let window = self.windows.get(self.window)?;
860        let index = window.start + self.record;
861        let record = window.records.get(index)?.clone();
862        self.record += 1;
863        self.next = record.sequence.checked_add(1)?;
864        Some(record)
865    }
866}
867
868fn collect_history_windows(history: Option<&Arc<History>>, tail: u64) -> Vec<HistoryWindow> {
869    let mut cursor = history.cloned();
870    let mut ceiling = tail;
871    let mut batches = Vec::new();
872    while let Some(history) = cursor {
873        match history.as_ref() {
874            History::Batch { parent, records } => {
875                batches.push((Arc::clone(records), ceiling));
876                cursor = parent.clone();
877            }
878            History::Prefix { source, tail } => {
879                ceiling = ceiling.min(*tail);
880                cursor = source.clone();
881            }
882        }
883    }
884    batches.reverse();
885    batches
886        .into_iter()
887        .filter_map(|(records, ceiling)| history_window(records, ceiling))
888        .collect()
889}
890
891fn history_window(records: Arc<[Record]>, ceiling: u64) -> Option<HistoryWindow> {
892    let first = records.first()?.sequence;
893    let batch_end = records
894        .len()
895        .try_into()
896        .ok()
897        .and_then(|length: u64| first.checked_add(length))
898        .unwrap_or(u64::MAX);
899    let end_sequence = ceiling.min(batch_end);
900    if end_sequence <= first {
901        return None;
902    }
903    let end = usize::try_from(end_sequence - first).ok()?;
904    Some(HistoryWindow {
905        records,
906        start: 0,
907        end,
908    })
909}
910
911fn read_history(history: Option<&Arc<History>>, tail: u64, from: u64, limit: usize) -> Vec<Record> {
912    // Follow asks for one record at a time.  Walking every historical batch and
913    // materializing all of them makes that hot path quadratic as a stream grows.
914    // A single-record read can seek through the newest immutable batches and
915    // clone only the requested record.
916    if limit == 1 {
917        return read_one_history(history, tail, from).into_iter().collect();
918    }
919    let mut cursor = history.cloned();
920    let mut ceiling = tail;
921    let mut batches = Vec::new();
922    while let Some(history) = cursor {
923        match history.as_ref() {
924            History::Batch { parent, records } => {
925                batches.push((Arc::clone(records), ceiling));
926                cursor = parent.clone();
927            }
928            History::Prefix { source, tail } => {
929                ceiling = ceiling.min(*tail);
930                cursor = source.clone();
931            }
932        }
933    }
934    let mut result = Vec::with_capacity(limit);
935    for (records, ceiling) in batches.into_iter().rev() {
936        for record in records.iter() {
937            if record.sequence >= from && record.sequence < ceiling {
938                result.push(record.clone());
939                if result.len() == limit {
940                    return result;
941                }
942            }
943        }
944    }
945    result
946}
947
948fn read_one_history(history: Option<&Arc<History>>, tail: u64, from: u64) -> Option<Record> {
949    let mut cursor = history.cloned();
950    let mut ceiling = tail;
951    while let Some(history) = cursor {
952        match history.as_ref() {
953            History::Batch { parent, records } => {
954                let Some(first) = records.first() else {
955                    cursor = parent.clone();
956                    continue;
957                };
958                let batch_end = records
959                    .len()
960                    .try_into()
961                    .ok()
962                    .and_then(|length: u64| first.sequence.checked_add(length))
963                    .unwrap_or(u64::MAX);
964                let visible_end = ceiling.min(batch_end);
965                if from >= first.sequence && from < visible_end {
966                    let offset = usize::try_from(from - first.sequence).ok()?;
967                    return records.get(offset).cloned();
968                }
969                ceiling = ceiling.min(first.sequence);
970                cursor = parent.clone();
971            }
972            History::Prefix { source, tail } => {
973                ceiling = ceiling.min(*tail);
974                cursor = source.clone();
975            }
976        }
977    }
978    None
979}
980
981fn is_direct_child(parent: &StreamPath, candidate: &StreamPath) -> bool {
982    candidate.parent().as_ref() == Some(parent)
983}
984
985fn is_descendant(parent: &StreamPath, candidate: &StreamPath) -> bool {
986    candidate
987        .as_str()
988        .strip_prefix(parent.as_str())
989        .is_some_and(|suffix| suffix.starts_with('/'))
990}
991
992fn next_commit_id(state: &mut State, digest: [u8; 32]) -> Result<CommitId, StreamError> {
993    state.decision = state.decision.checked_add(1).ok_or(StreamError::Capacity)?;
994    let mut hash = Sha256::new();
995    hash.update(b"acyclic.stream.commit.v1\0");
996    hash.update(state.decision.to_le_bytes());
997    hash.update(digest);
998    Ok(CommitId::from_bytes(hash.finalize().into()))
999}
1000
1001fn replay(
1002    state: &State,
1003    key: Option<&crate::IdempotencyKey>,
1004    digest: [u8; 32],
1005) -> Result<Option<IdempotencyOutcome>, StreamError> {
1006    let Some(key) = key else { return Ok(None) };
1007    let Some(replay) = state.replays.get(key.as_bytes()) else {
1008        return Ok(None);
1009    };
1010    if replay.digest != digest {
1011        return Err(StreamError::IdempotencyMismatch);
1012    }
1013    Ok(Some(replay.result.clone()))
1014}
1015
1016fn replay_append(
1017    state: &State,
1018    key: Option<&crate::IdempotencyKey>,
1019    digest: [u8; 32],
1020) -> Result<Option<AppendOutcome>, StreamError> {
1021    match replay(state, key, digest)? {
1022        Some(IdempotencyOutcome::Append(result)) => Ok(Some(result)),
1023        Some(_) => Err(StreamError::IdempotencyMismatch),
1024        None => Ok(None),
1025    }
1026}
1027
1028fn replay_fork(
1029    state: &State,
1030    key: Option<&crate::IdempotencyKey>,
1031    digest: [u8; 32],
1032) -> Result<Option<ForkReceipt>, StreamError> {
1033    match replay(state, key, digest)? {
1034        Some(IdempotencyOutcome::Fork(result)) => Ok(Some(result)),
1035        Some(_) => Err(StreamError::IdempotencyMismatch),
1036        None => Ok(None),
1037    }
1038}
1039
1040fn replay_trim(
1041    state: &State,
1042    key: &IdempotencyKey,
1043    digest: [u8; 32],
1044) -> Result<Option<TrimReceipt>, StreamError> {
1045    match replay(state, Some(key), digest)? {
1046        Some(IdempotencyOutcome::Trim(result)) => Ok(Some(result)),
1047        Some(_) => Err(StreamError::IdempotencyMismatch),
1048        None => Ok(None),
1049    }
1050}
1051
1052fn replay_delete(
1053    state: &State,
1054    key: &IdempotencyKey,
1055    digest: [u8; 32],
1056) -> Result<Option<DeleteReceipt>, StreamError> {
1057    match replay(state, Some(key), digest)? {
1058        Some(IdempotencyOutcome::Delete(result)) => Ok(Some(result)),
1059        Some(_) => Err(StreamError::IdempotencyMismatch),
1060        None => Ok(None),
1061    }
1062}
1063
1064fn replay_commit(
1065    state: &State,
1066    key: &crate::IdempotencyKey,
1067    digest: [u8; 32],
1068) -> Result<Option<CommitOutcome>, StreamError> {
1069    match replay(state, Some(key), digest)? {
1070        Some(IdempotencyOutcome::Commit(result)) => Ok(Some(result)),
1071        Some(_) => Err(StreamError::IdempotencyMismatch),
1072        None => Ok(None),
1073    }
1074}
1075
1076fn admit_replay(
1077    state: &State,
1078    key: Option<&crate::IdempotencyKey>,
1079    limits: MemoryLimits,
1080) -> Result<(), StreamError> {
1081    if key.is_some() && state.replays.len() >= limits.idempotency_results {
1082        Err(StreamError::Capacity)
1083    } else {
1084        Ok(())
1085    }
1086}
1087
1088fn retain_replay(
1089    state: &mut State,
1090    key: Option<crate::IdempotencyKey>,
1091    digest: [u8; 32],
1092    result: IdempotencyOutcome,
1093) {
1094    if let Some(key) = key {
1095        state.replays.insert(
1096            Bytes::copy_from_slice(key.as_bytes()),
1097            Replay { digest, result },
1098        );
1099    }
1100}
1101
1102fn append_digest(request: &AppendRequest) -> [u8; 32] {
1103    let mut hash = request_hasher(b"append");
1104    hash_path(&mut hash, &request.path);
1105    hash.update(request.if_tail.unwrap_or(u64::MAX).to_le_bytes());
1106    hash_records(&mut hash, &request.records);
1107    hash.finalize().into()
1108}
1109
1110fn fork_digest(request: &ForkRequest) -> [u8; 32] {
1111    let mut hash = request_hasher(b"fork");
1112    hash_path(&mut hash, &request.source);
1113    hash_path(&mut hash, &request.destination);
1114    hash.update(request.at_tail.unwrap_or(u64::MAX).to_le_bytes());
1115    hash.finalize().into()
1116}
1117
1118fn trim_digest(path: &StreamPath, before: u64) -> [u8; 32] {
1119    let mut hash = request_hasher(b"trim");
1120    hash_path(&mut hash, path);
1121    hash.update(before.to_le_bytes());
1122    hash.finalize().into()
1123}
1124
1125fn delete_digest(path: &StreamPath) -> [u8; 32] {
1126    let mut hash = request_hasher(b"delete");
1127    hash_path(&mut hash, path);
1128    hash.finalize().into()
1129}
1130
1131fn commit_digest(request: &CommitRequest) -> [u8; 32] {
1132    let mut hash = request_hasher(b"commit");
1133    for condition in &request.conditions {
1134        match condition {
1135            CommitCondition::Tail { path, expected } => {
1136                hash.update([1]);
1137                hash_path(&mut hash, path);
1138                hash.update(expected.to_le_bytes());
1139            }
1140            CommitCondition::Absent { path } => {
1141                hash.update([2]);
1142                hash_path(&mut hash, path);
1143            }
1144        }
1145    }
1146    for mutation in &request.mutations {
1147        match mutation {
1148            CommitMutation::Append { path, records } => {
1149                hash.update([1]);
1150                hash_path(&mut hash, path);
1151                hash_records(&mut hash, records);
1152            }
1153            CommitMutation::Fork {
1154                source,
1155                destination,
1156                at_tail,
1157            } => {
1158                hash.update([2]);
1159                hash_path(&mut hash, source);
1160                hash_path(&mut hash, destination);
1161                hash.update(at_tail.to_le_bytes());
1162            }
1163            CommitMutation::Trim { path, before } => {
1164                hash.update([3]);
1165                hash_path(&mut hash, path);
1166                hash.update(before.to_le_bytes());
1167            }
1168            CommitMutation::Delete { path } => {
1169                hash.update([4]);
1170                hash_path(&mut hash, path);
1171            }
1172        }
1173    }
1174    hash.finalize().into()
1175}
1176
1177fn request_hasher(kind: &[u8]) -> Sha256 {
1178    let mut hash = Sha256::new();
1179    hash.update(b"acyclic.stream.request.v1\0");
1180    hash.update(kind);
1181    hash
1182}
1183
1184fn hash_path(hash: &mut Sha256, path: &StreamPath) {
1185    hash.update((path.as_str().len() as u64).to_le_bytes());
1186    hash.update(path.as_str().as_bytes());
1187}
1188
1189fn hash_records(hash: &mut Sha256, records: &[Bytes]) {
1190    hash.update((records.len() as u64).to_le_bytes());
1191    for record in records {
1192        hash.update((record.len() as u64).to_le_bytes());
1193        hash.update(record);
1194    }
1195}
1196
1197fn condition_path(condition: &CommitCondition) -> &StreamPath {
1198    match condition {
1199        CommitCondition::Tail { path, .. } | CommitCondition::Absent { path } => path,
1200    }
1201}
1202
1203fn mutation_path(mutation: &CommitMutation) -> &StreamPath {
1204    match mutation {
1205        CommitMutation::Fork { destination, .. } => destination,
1206        CommitMutation::Append { path, .. }
1207        | CommitMutation::Trim { path, .. }
1208        | CommitMutation::Delete { path } => path,
1209    }
1210}
1211
1212fn normalize_commit(request: &mut CommitRequest) -> Result<(), StreamError> {
1213    if request.conditions.is_empty()
1214        || request.conditions.len() > MAX_ITEMS
1215        || request.mutations.is_empty()
1216        || request.mutations.len() > MAX_ITEMS
1217    {
1218        return Err(StreamError::LimitExceeded);
1219    }
1220    for mutation in &request.mutations {
1221        if let CommitMutation::Append { records, .. } = mutation {
1222            validate_records(records)?;
1223        }
1224    }
1225    validate_commit_size(request)?;
1226    request.conditions.sort_by(|left, right| {
1227        condition_path(left)
1228            .cmp(condition_path(right))
1229            .then_with(|| {
1230                matches!(left, CommitCondition::Absent { .. })
1231                    .cmp(&matches!(right, CommitCondition::Absent { .. }))
1232            })
1233    });
1234    request
1235        .mutations
1236        .sort_by(|left, right| mutation_path(left).cmp(mutation_path(right)));
1237    if request
1238        .conditions
1239        .windows(2)
1240        .any(|pair| matches!(pair, [left, right] if condition_path(left) == condition_path(right)))
1241        || request.mutations.windows(2).any(
1242            |pair| matches!(pair, [left, right] if mutation_path(left) == mutation_path(right)),
1243        )
1244    {
1245        return Err(StreamError::InvalidArgument);
1246    }
1247    Ok(())
1248}
1249
1250fn validate_commit_size(request: &CommitRequest) -> Result<(), StreamError> {
1251    let mut total = 1 + 8 + 8 + 8 + request.idempotency_key.as_bytes().len();
1252    for condition in &request.conditions {
1253        add_size(&mut total, 1 + 8)?;
1254        add_path_size(&mut total, condition_path(condition))?;
1255    }
1256    for mutation in &request.mutations {
1257        add_size(&mut total, 1)?;
1258        match mutation {
1259            CommitMutation::Append { path, records } => {
1260                add_path_size(&mut total, path)?;
1261                add_records_size(&mut total, records)?;
1262            }
1263            CommitMutation::Fork {
1264                source,
1265                destination,
1266                ..
1267            } => {
1268                add_path_size(&mut total, source)?;
1269                add_path_size(&mut total, destination)?;
1270                add_size(&mut total, 8)?;
1271            }
1272            CommitMutation::Trim { path, .. } => {
1273                add_path_size(&mut total, path)?;
1274                add_size(&mut total, 8)?;
1275            }
1276            CommitMutation::Delete { path } => add_path_size(&mut total, path)?,
1277        }
1278    }
1279    Ok(())
1280}
1281
1282fn validate_commit_shape(request: &CommitRequest) -> Result<(), StreamError> {
1283    let conditions = request
1284        .conditions
1285        .iter()
1286        .map(|condition| (condition_path(condition), condition))
1287        .collect::<BTreeMap<_, _>>();
1288    for mutation in &request.mutations {
1289        match mutation {
1290            CommitMutation::Append { path, .. } => match conditions.get(path) {
1291                Some(CommitCondition::Tail { .. } | CommitCondition::Absent { .. }) => {}
1292                _ => return Err(StreamError::InvalidArgument),
1293            },
1294            CommitMutation::Fork {
1295                source: _,
1296                destination,
1297                at_tail: _,
1298            } => {
1299                if !matches!(
1300                    conditions.get(destination),
1301                    Some(CommitCondition::Absent { .. })
1302                ) {
1303                    return Err(StreamError::InvalidArgument);
1304                }
1305            }
1306            CommitMutation::Trim { path, before: _ } | CommitMutation::Delete { path } => {
1307                if !matches!(conditions.get(path), Some(CommitCondition::Tail { .. })) {
1308                    return Err(StreamError::InvalidArgument);
1309                }
1310            }
1311        }
1312    }
1313    Ok(())
1314}
1315
1316fn validate_commit_authority(state: &State, request: &CommitRequest) -> Result<(), StreamError> {
1317    for mutation in &request.mutations {
1318        match mutation {
1319            CommitMutation::Append { .. } => {}
1320            CommitMutation::Fork {
1321                source, at_tail, ..
1322            } => {
1323                let Some(source_state) = state.paths.get(source) else {
1324                    return Err(StreamError::NotFound);
1325                };
1326                if *at_tail > source_state.tail {
1327                    return Err(StreamError::InvalidArgument);
1328                }
1329            }
1330            CommitMutation::Trim { path, before } => {
1331                let Some(stream) = state.paths.get(path) else {
1332                    return Err(StreamError::NotFound);
1333                };
1334                if *before > stream.tail {
1335                    return Err(StreamError::InvalidArgument);
1336                }
1337            }
1338            CommitMutation::Delete { path } => {
1339                if !state.paths.contains_key(path)
1340                    || state
1341                        .paths
1342                        .keys()
1343                        .any(|candidate| is_descendant(path, candidate))
1344                {
1345                    return Err(StreamError::InvalidArgument);
1346                }
1347            }
1348        }
1349    }
1350    Ok(())
1351}
1352
1353fn commit_conflicts(state: &State, conditions: &[CommitCondition]) -> Vec<CommitConflict> {
1354    conditions
1355        .iter()
1356        .filter_map(|condition| match condition {
1357            CommitCondition::Tail { path, expected } => match state.paths.get(path) {
1358                Some(stream) if stream.tail == *expected => None,
1359                Some(stream) => Some(CommitConflict::Tail {
1360                    path: path.clone(),
1361                    expected: *expected,
1362                    actual: Some(stream.tail),
1363                }),
1364                None => Some(CommitConflict::Tail {
1365                    path: path.clone(),
1366                    expected: *expected,
1367                    actual: None,
1368                }),
1369            },
1370            CommitCondition::Absent { path } => {
1371                if state.paths.contains_key(path) {
1372                    Some(CommitConflict::Exists { path: path.clone() })
1373                } else if reject_retired(state, path).is_err() {
1374                    Some(CommitConflict::Retired { path: path.clone() })
1375                } else {
1376                    None
1377                }
1378            }
1379        })
1380        .collect()
1381}
1382
1383fn reserve_coordinated(
1384    state: &State,
1385    request: &CommitRequest,
1386    limits: MemoryLimits,
1387) -> Result<(), StreamError> {
1388    reserve_commit(state, limits)?;
1389    let mut new_paths = BTreeSet::new();
1390    let mut records = 0_usize;
1391    let mut bytes = 0_usize;
1392    for mutation in &request.mutations {
1393        let destination = mutation_path(mutation);
1394        let mut current = Some(destination.clone());
1395        while let Some(path) = current {
1396            if !state.paths.contains_key(&path) {
1397                new_paths.insert(path.clone());
1398            }
1399            current = path.parent();
1400        }
1401        if let CommitMutation::Append { records: batch, .. } = mutation {
1402            records = records.saturating_add(batch.len());
1403            bytes = bytes.saturating_add(batch.iter().map(Bytes::len).sum::<usize>());
1404        }
1405    }
1406    if state.paths.len().saturating_add(new_paths.len()) > limits.paths
1407        || state
1408            .path_bytes
1409            .saturating_add(new_paths.iter().map(|path| path.as_str().len()).sum())
1410            > limits.path_bytes
1411        || state.record_count.saturating_add(records) > limits.records
1412        || state.payload_bytes.saturating_add(bytes) > limits.payload_bytes
1413    {
1414        return Err(StreamError::Capacity);
1415    }
1416    Ok(())
1417}
1418
1419fn apply_coordinated(
1420    state: &mut State,
1421    mutations: Vec<CommitMutation>,
1422    before: &BTreeMap<StreamPath, (Option<Arc<History>>, u64)>,
1423    commit_id: CommitId,
1424) -> Result<Vec<CommittedMutation>, StreamError> {
1425    let mut committed = Vec::with_capacity(mutations.len());
1426    for mutation in mutations {
1427        match mutation {
1428            CommitMutation::Append { path, records } => {
1429                let start = state.paths.get(&path).map_or(0, |stream| stream.tail);
1430                let records = build_records(start, records, commit_id)?;
1431                ensure_path(state, &path);
1432                let resulting_tail = {
1433                    let stream = state.paths.get_mut(&path).ok_or(StreamError::Unavailable)?;
1434                    stream.history = Some(Arc::new(History::Batch {
1435                        parent: stream.history.clone(),
1436                        records: Arc::from(records.clone()),
1437                    }));
1438                    stream.tail = records.last().map_or(start, |record| record.sequence + 1);
1439                    stream.changed.send_replace(stream.tail);
1440                    stream.tail
1441                };
1442                state.record_count += records.len();
1443                state.payload_bytes += records
1444                    .iter()
1445                    .map(|record| record.value.len())
1446                    .sum::<usize>();
1447                committed.push(CommittedMutation::Append(CommittedAppend {
1448                    path,
1449                    start,
1450                    end: resulting_tail,
1451                    tail: resulting_tail,
1452                    records,
1453                }));
1454            }
1455            CommitMutation::Fork {
1456                source,
1457                destination,
1458                at_tail,
1459            } => {
1460                let (history, source_tail) = before.get(&source).ok_or(StreamError::NotFound)?;
1461                if at_tail > *source_tail {
1462                    return Err(StreamError::InvalidArgument);
1463                }
1464                ensure_path(state, &destination);
1465                let stream = state
1466                    .paths
1467                    .get_mut(&destination)
1468                    .ok_or(StreamError::Unavailable)?;
1469                stream.history = Some(Arc::new(History::Prefix {
1470                    source: history.clone(),
1471                    tail: at_tail,
1472                }));
1473                stream.tail = at_tail;
1474                stream.changed.send_replace(at_tail);
1475                committed.push(CommittedMutation::Fork(CommittedFork {
1476                    source,
1477                    destination,
1478                    forked_at: at_tail,
1479                    tail: at_tail,
1480                }));
1481            }
1482            CommitMutation::Trim { path, before } => {
1483                let stream = state.paths.get_mut(&path).ok_or(StreamError::NotFound)?;
1484                stream.trim_point = stream.trim_point.max(before);
1485                stream.changed.send_replace(stream.tail);
1486                committed.push(CommittedMutation::Trim(CommittedTrim {
1487                    path,
1488                    trim_point: stream.trim_point,
1489                }));
1490            }
1491            CommitMutation::Delete { path } => {
1492                state.paths.remove(&path).ok_or(StreamError::NotFound)?;
1493                state.retired.insert(path.clone());
1494                committed.push(CommittedMutation::Delete(CommittedDelete { path }));
1495            }
1496        }
1497    }
1498    Ok(committed)
1499}
1500
1501#[cfg(test)]
1502mod tests {
1503    use super::*;
1504    use std::sync::atomic::{AtomicU64, Ordering};
1505
1506    #[derive(Default)]
1507    struct TestClock(AtomicU64);
1508
1509    impl UnixMillisClock for TestClock {
1510        fn now_unix_millis(&self) -> u64 {
1511            self.0.load(Ordering::SeqCst)
1512        }
1513    }
1514
1515    fn path(value: &str) -> Result<StreamPath, StreamError> {
1516        StreamPath::new(value)
1517    }
1518
1519    fn key(value: &'static [u8]) -> Result<crate::IdempotencyKey, StreamError> {
1520        crate::IdempotencyKey::new(Bytes::from_static(value))
1521    }
1522
1523    #[tokio::test]
1524    async fn provider_deadline_is_exclusive_and_replay_precedes_time() -> Result<(), StreamError> {
1525        let clock = Arc::new(TestClock::default());
1526        clock.0.store(100, Ordering::SeqCst);
1527        let provider = MemoryStream::new_with_clock(MemoryLimits::default(), clock.clone());
1528        let request = CommitRequest {
1529            conditions: vec![CommitCondition::Absent {
1530                path: path("deadline/accepted")?,
1531            }],
1532            mutations: vec![CommitMutation::Append {
1533                path: path("deadline/accepted")?,
1534                records: vec![Bytes::from_static(b"accepted")],
1535            }],
1536            idempotency_key: key(b"deadline-accepted")?,
1537        };
1538        assert_eq!(
1539            provider.commit_before(request.clone(), 100).await,
1540            Err(StreamError::DeadlineElapsed)
1541        );
1542        assert_eq!(
1543            provider.commit_before(request.clone(), 99).await,
1544            Err(StreamError::DeadlineElapsed)
1545        );
1546        let committed = provider.commit_before(request.clone(), 101).await?;
1547        assert!(matches!(committed, CommitOutcome::Committed(_)));
1548        clock.0.store(1_000, Ordering::SeqCst);
1549        assert_eq!(provider.commit_before(request, 101).await?, committed);
1550
1551        let retryable = CommitRequest {
1552            conditions: vec![CommitCondition::Absent {
1553                path: path("deadline/retry")?,
1554            }],
1555            mutations: vec![CommitMutation::Append {
1556                path: path("deadline/retry")?,
1557                records: vec![Bytes::from_static(b"retry")],
1558            }],
1559            idempotency_key: key(b"deadline-retry")?,
1560        };
1561        assert_eq!(
1562            provider.commit_before(retryable.clone(), 1_000).await,
1563            Err(StreamError::DeadlineElapsed)
1564        );
1565        assert!(matches!(
1566            provider.commit_before(retryable, 1_001).await?,
1567            CommitOutcome::Committed(_)
1568        ));
1569        Ok(())
1570    }
1571
1572    #[tokio::test]
1573    #[allow(
1574        clippy::indexing_slicing,
1575        reason = "indices are preceded by an assert_eq!(read.len(), 2), proving them in-bounds"
1576    )]
1577    async fn append_fork_follow_and_replay_preserve_exact_lineage() -> Result<(), StreamError> {
1578        let provider = MemoryStream::default();
1579        let source = path("runs/a")?;
1580        let first = AppendRequest {
1581            path: source.clone(),
1582            records: vec![Bytes::from_static(b"one"), Bytes::from_static(b"two")],
1583            if_tail: Some(0),
1584            idempotency_key: Some(key(b"append")?),
1585        };
1586        let committed = provider.append(first.clone()).await;
1587        assert!(committed.is_ok());
1588        assert_eq!(provider.append(first).await, committed);
1589        let forked = provider
1590            .fork(ForkRequest {
1591                source: source.clone(),
1592                destination: path("runs/b")?,
1593                at_tail: Some(1),
1594                idempotency_key: Some(key(b"fork")?),
1595            })
1596            .await?;
1597        assert_eq!(forked.tail, 1);
1598        let child = forked.destination.clone();
1599        provider
1600            .append(AppendRequest {
1601                path: child.clone(),
1602                records: vec![Bytes::from_static(b"child")],
1603                if_tail: Some(1),
1604                idempotency_key: None,
1605            })
1606            .await?;
1607        let read = provider
1608            .read(ReadRequest {
1609                path: child.clone(),
1610                from: 0,
1611                limit: 10,
1612            })
1613            .await?
1614            .collect::<Vec<_>>()
1615            .await;
1616        assert_eq!(read.len(), 2);
1617        assert_eq!(
1618            read[0].as_ref().map(|record| record.value.as_ref()),
1619            Ok(b"one".as_slice())
1620        );
1621        assert_eq!(
1622            read[1].as_ref().map(|record| record.value.as_ref()),
1623            Ok(b"child".as_slice())
1624        );
1625        let mut follow = provider.follow(child.clone(), 2).await?;
1626        provider
1627            .append(AppendRequest {
1628                path: child,
1629                records: vec![Bytes::from_static(b"live")],
1630                if_tail: Some(2),
1631                idempotency_key: None,
1632            })
1633            .await?;
1634        let live = tokio::time::timeout(std::time::Duration::from_secs(1), follow.next()).await;
1635        assert!(matches!(live, Ok(Some(Ok(Record { sequence: 2, .. })))));
1636        Ok(())
1637    }
1638
1639    #[tokio::test]
1640    async fn single_record_reads_seek_deep_history_and_prefixes() -> Result<(), StreamError> {
1641        let provider = MemoryStream::default();
1642        let source = path("seek/source")?;
1643        for index in 0..256_u64 {
1644            provider
1645                .append(AppendRequest {
1646                    path: source.clone(),
1647                    records: vec![Bytes::from(index.to_string())],
1648                    if_tail: Some(index),
1649                    idempotency_key: None,
1650                })
1651                .await?;
1652        }
1653        let destination = path("seek/prefix")?;
1654        provider
1655            .fork(ForkRequest {
1656                source: source.clone(),
1657                destination: destination.clone(),
1658                at_tail: Some(128),
1659                idempotency_key: None,
1660            })
1661            .await?;
1662
1663        for (path, expected_tail, expected) in [
1664            (&source, 256_u64, b"255".as_slice()),
1665            (&destination, 128_u64, b"127".as_slice()),
1666        ] {
1667            let record = provider
1668                .read(ReadRequest {
1669                    path: path.clone(),
1670                    from: expected_tail - 1,
1671                    limit: 1,
1672                })
1673                .await?
1674                .next()
1675                .await
1676                .ok_or(StreamError::Unavailable)??;
1677            assert_eq!(record.sequence, expected_tail - 1);
1678            assert_eq!(record.value, expected);
1679        }
1680        Ok(())
1681    }
1682
1683    #[tokio::test]
1684    #[ignore = "bounded scaling benchmark; run with --ignored --nocapture"]
1685    async fn follow_scales_with_history_depth() -> Result<(), StreamError> {
1686        for count in [1_000_u64, 10_000] {
1687            let provider = MemoryStream::default();
1688            let stream = path("bench/follow")?;
1689            for index in 0..count {
1690                provider
1691                    .append(AppendRequest {
1692                        path: stream.clone(),
1693                        records: vec![Bytes::from_static(b"x")],
1694                        if_tail: Some(index),
1695                        idempotency_key: None,
1696                    })
1697                    .await?;
1698            }
1699            let started = std::time::Instant::now();
1700            let mut follow = provider.follow(stream, 0).await?;
1701            for expected in 0..count {
1702                let record = follow.next().await.ok_or(StreamError::Unavailable)??;
1703                assert_eq!(record.sequence, expected);
1704            }
1705            eprintln!("follow history={count} read={:?}", started.elapsed());
1706        }
1707        Ok(())
1708    }
1709
1710    #[tokio::test]
1711    #[allow(
1712        clippy::indexing_slicing,
1713        reason = "index is preceded by an assert_eq!(forked.len(), 1), proving it in-bounds"
1714    )]
1715    async fn coordinated_conflict_changes_nothing_and_success_has_one_envelope()
1716    -> Result<(), StreamError> {
1717        let provider = MemoryStream::default();
1718        let source = path("jobs")?;
1719        provider
1720            .append(AppendRequest {
1721                path: source.clone(),
1722                records: vec![Bytes::from_static(b"ready")],
1723                if_tail: Some(0),
1724                idempotency_key: None,
1725            })
1726            .await?;
1727        let request = CommitRequest {
1728            conditions: vec![CommitCondition::Absent {
1729                path: path("runs/a")?,
1730            }],
1731            mutations: vec![CommitMutation::Fork {
1732                source: source.clone(),
1733                destination: path("runs/a")?,
1734                at_tail: 1,
1735            }],
1736            idempotency_key: key(b"commit")?,
1737        };
1738        provider
1739            .append(AppendRequest {
1740                path: source,
1741                records: vec![Bytes::from_static(b"source-progress")],
1742                if_tail: Some(1),
1743                idempotency_key: None,
1744            })
1745            .await?;
1746        let outcome = provider.commit(request.clone()).await;
1747        let Ok(CommitOutcome::Committed(envelope)) = &outcome else {
1748            return Err(StreamError::Unavailable);
1749        };
1750        assert_eq!(provider.commit(request).await, outcome);
1751        assert_eq!(
1752            provider.read_commit(envelope.commit_id).await.as_ref(),
1753            Ok(envelope)
1754        );
1755        let forked = provider
1756            .read(ReadRequest {
1757                path: path("runs/a")?,
1758                from: 0,
1759                limit: 10,
1760            })
1761            .await?
1762            .collect::<Vec<_>>()
1763            .await;
1764        assert_eq!(forked.len(), 1);
1765        assert_eq!(
1766            forked[0].as_ref().map(|record| record.value.as_ref()),
1767            Ok(b"ready".as_slice())
1768        );
1769        Ok(())
1770    }
1771
1772    #[tokio::test]
1773    async fn exact_replay_precedes_capacity_and_changed_arguments_never_reclassify()
1774    -> Result<(), StreamError> {
1775        let provider = MemoryStream::new(MemoryLimits {
1776            paths: 2,
1777            path_bytes: 4,
1778            records: 1,
1779            payload_bytes: 3,
1780            commits: 1,
1781            idempotency_results: 1,
1782        });
1783        let request = AppendRequest {
1784            path: path("a/b")?,
1785            records: vec![Bytes::from_static(b"one")],
1786            if_tail: Some(0),
1787            idempotency_key: Some(key(b"only-key")?),
1788        };
1789        let committed = provider.append(request.clone()).await?;
1790        assert_eq!(
1791            provider.append(request.clone()).await,
1792            Ok(committed.clone())
1793        );
1794        let observation = provider
1795            .inspect_idempotency(key(b"only-key")?)
1796            .await?
1797            .ok_or(StreamError::Unavailable)?;
1798        assert_ne!(observation.request_digest, [0; 32]);
1799        assert_eq!(observation.outcome, IdempotencyOutcome::Append(committed));
1800        assert_eq!(
1801            provider
1802                .append(AppendRequest {
1803                    records: vec![Bytes::from_static(b"two")],
1804                    ..request
1805                })
1806                .await,
1807            Err(StreamError::IdempotencyMismatch)
1808        );
1809        Ok(())
1810    }
1811
1812    #[tokio::test]
1813    async fn every_fork_cut_reads_the_exact_prefix_and_never_aliases_suffixes()
1814    -> Result<(), StreamError> {
1815        let provider = MemoryStream::default();
1816        let source = path("lineage/source")?;
1817        let bodies = (0_u8..32)
1818            .map(|value| Bytes::from(vec![value]))
1819            .collect::<Vec<_>>();
1820        provider
1821            .append(AppendRequest {
1822                path: source.clone(),
1823                records: bodies,
1824                if_tail: Some(0),
1825                idempotency_key: None,
1826            })
1827            .await?;
1828        for cut in 0_u64..=32 {
1829            let destination = path(&format!("lineage/forks/{cut}"))?;
1830            provider
1831                .fork(ForkRequest {
1832                    source: source.clone(),
1833                    destination: destination.clone(),
1834                    at_tail: Some(cut),
1835                    idempotency_key: None,
1836                })
1837                .await?;
1838            let records = provider
1839                .read(ReadRequest {
1840                    path: destination,
1841                    from: 0,
1842                    limit: 64,
1843                })
1844                .await?
1845                .collect::<Vec<_>>()
1846                .await;
1847            assert_eq!(
1848                u64::try_from(records.len()).map_err(|_| StreamError::LimitExceeded)?,
1849                cut
1850            );
1851        }
1852        assert_eq!(provider.tail(source).await?, 32);
1853        Ok(())
1854    }
1855
1856    #[test]
1857    fn permanent_path_validation_rejects_ambiguous_names() {
1858        for invalid in [
1859            "", "/a", "a/", "a//b", ".", "..", "a/./b", "a/../b", "a\n", "a b", "a\\b",
1860        ] {
1861            assert_eq!(StreamPath::new(invalid), Err(StreamError::InvalidPath));
1862        }
1863        assert!(StreamPath::new("a".repeat(crate::MAX_PATH_BYTES)).is_ok());
1864        assert_eq!(
1865            StreamPath::new("a".repeat(crate::MAX_PATH_BYTES + 1)),
1866            Err(StreamError::InvalidPath)
1867        );
1868        assert!(StreamPath::new("runs/run_42/agents/researcher").is_ok());
1869    }
1870}