open-standards/eoc/eoc-rs/crates/eoc-streaming/src/resume.rs
Transaction Science 7ef14c2ea9 ingest wave B: 3 crates · op-payouts + smart-byte-nostr + eoc-streaming
OpenPay (1 crate, 2174 LOC, 14 tests)
  op-payouts          Multi-rail payout engine. Visa Direct, Mastercard Send,
                      ACH credits (NACHA), Fedwire + SWIFT MT103 + ISO 20022
                      pacs.008, SEPA SCT + SCT Inst (mod-97 IBAN), UK FPS,
                      FedNow, RTP, PayPal, Wise. Crypto: USDC/USDT on
                      ETH/Polygon/Base/Solana + BTC + Lightning with real
                      ERC-20 transfer(address,uint256) calldata. Offline-pure
                      drivers (emit wire payloads — operators transmit).

Smart Byte (1 crate, 2605 LOC, 38 tests)
  smart-byte-nostr    Nostr adapter. NIP-01 canonical event id + BIP-340
                      Schnorr sign/verify; from-scratch bech32 + NIP-19 TLV
                      (npub/nsec/note/nprofile/nevent/nrelay); NIP-04 legacy
                      AES-256-CBC DMs; NIP-44 v2 HKDF-SHA256→ChaCha20+HMAC
                      with bucketed padding; NIP-17 gift-wrap (kind 1059) +
                      seal (kind 13) + rumor (kind 14); NIP-05/11/13/26/65.
                      Relay client message vocabulary (REQ/EVENT/CLOSE/AUTH/
                      EOSE/OK/NOTICE/CLOSED) + in-memory router.

EOC (1 crate, 2024 LOC, 30 tests)
  eoc-streaming       Token-by-token + tool-call streaming primitives.
                      SSE framing (data: lines); Anthropic event mapper
                      (message_start/content_block_*/message_delta/stop/ping);
                      OpenAI delta mapper; Vercel AI SDK mapper. Bounded-channel
                      backpressure with watermarks; CancelToken propagation;
                      Last-Event-ID resumable streams; per-stream joule meter.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 13:39:09 -04:00

119 lines
3.8 KiB
Rust

//! Resumable streams via `Last-Event-ID`.
//!
//! When a client reconnects to a streaming endpoint it may include the
//! `Last-Event-ID` HTTP header. If the server still has a replay
//! window covering that id, it can send the missed events first and
//! then resume normal streaming. This module owns the in-memory event
//! log + replay logic; the actual transport binding (HTTP header
//! plumbing, etc.) is left to the host.
use std::collections::VecDeque;
use std::sync::Mutex;
use crate::error::{StreamError, StreamResult};
/// An identifier carried in the SSE `id:` field.
pub type LastEventId = String;
/// Bounded ring-buffer of recently emitted events keyed on id.
#[derive(Debug)]
pub struct EventLog<T: Clone + Send + 'static> {
inner: Mutex<EventLogInner<T>>,
capacity: usize,
}
#[derive(Debug)]
struct EventLogInner<T> {
/// Pairs of (id, event). Oldest first.
entries: VecDeque<(LastEventId, T)>,
}
impl<T: Clone + Send + 'static> EventLog<T> {
/// Construct a log retaining at most `capacity` entries.
pub fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(EventLogInner {
entries: VecDeque::with_capacity(capacity.max(1)),
}),
capacity: capacity.max(1),
}
}
/// Append a new event. Drops the oldest entry when capacity is hit.
pub fn push(&self, id: LastEventId, ev: T) -> StreamResult<()> {
let mut g = self.inner.lock().map_err(|_| {
StreamError::Backend("event log mutex poisoned".into())
})?;
if g.entries.len() == self.capacity {
g.entries.pop_front();
}
g.entries.push_back((id, ev));
Ok(())
}
/// Return all events strictly newer than `last_id`. If `last_id` is
/// `None`, returns the entire current window. Errors with
/// [`StreamError::ResumeOutOfRange`] when `last_id` is supplied but
/// not in the window (i.e. it predates the oldest retained event).
pub fn replay_after(&self, last_id: Option<&str>) -> StreamResult<Vec<T>> {
let g = self.inner.lock().map_err(|_| {
StreamError::Backend("event log mutex poisoned".into())
})?;
let Some(last_id) = last_id else {
return Ok(g.entries.iter().map(|(_, e)| e.clone()).collect());
};
// If the oldest entry's id is already newer than last_id we
// cannot guarantee no gap, so report out-of-range.
let mut found = false;
let mut out = Vec::new();
for (id, ev) in g.entries.iter() {
if found {
out.push(ev.clone());
} else if id == last_id {
found = true;
}
}
if !found {
return Err(StreamError::ResumeOutOfRange(last_id.to_string()));
}
Ok(out)
}
/// Current window length.
pub fn len(&self) -> usize {
self.inner
.lock()
.map(|g| g.entries.len())
.unwrap_or(0)
}
/// True if the window is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replays_after_id() {
let log = EventLog::<&'static str>::new(4);
log.push("1".into(), "a").unwrap();
log.push("2".into(), "b").unwrap();
log.push("3".into(), "c").unwrap();
let r = log.replay_after(Some("1")).unwrap();
assert_eq!(r, vec!["b", "c"]);
}
#[test]
fn out_of_range_reports() {
let log = EventLog::<&'static str>::new(2);
log.push("1".into(), "a").unwrap();
log.push("2".into(), "b").unwrap();
log.push("3".into(), "c").unwrap(); // evicts "1"
let err = log.replay_after(Some("1")).unwrap_err();
assert!(matches!(err, StreamError::ResumeOutOfRange(_)));
}
}