| Crates.io | mdstream-tokio |
| lib.rs | mdstream-tokio |
| version | 0.2.0 |
| created_at | 2025-12-29 09:57:10.581465+00 |
| updated_at | 2025-12-29 09:57:10.581465+00 |
| description | Tokio glue for mdstream (delta coalescing and optional actor helpers). |
| homepage | https://github.com/Latias94/mdstream |
| repository | https://github.com/Latias94/mdstream |
| max_upload_size | |
| id | 2010362 |
| size | 64,124 |
mdstream is a streaming-first Markdown middleware for Rust.
It targets LLM token-by-token / chunk-by-chunk output and helps downstream UIs (egui, gpui/Zed, TUI, etc.) avoid the classic O(n²) re-parse + re-render pattern that causes latency and flicker.
Use mdstream when you:
You probably don’t need mdstream if you only parse static Markdown once, or if you already have a renderer that handles incremental updates internally.
MdStream: streaming block splitter (append / finalize) that produces Update.MdStream::append_ref / finalize_ref: borrowed update views (UpdateRef) for high-frequency UIs
that want to avoid cloning the pending tail on every tick.MdStream::snapshot_blocks(&mut self): take a best-effort snapshot (may run stateful transformers).Update: committed + pending plus signals like reset and invalidated.UpdateRef: committed + pending (borrowed) plus reset and invalidated.Block: carries id, kind, raw, and optional display (pending-only).PendingBlockRef: a borrowed view of the current pending block (raw + optional display).DocumentState: a UI-friendly container to apply Update safely (recommended).PulldownAdapter behind the pulldown feature.BlockId).append() may return Update { reset: true, .. } to tell consumers to drop cached blocks.display view for the pending block:
PendingTransformer (eg placeholders, sanitizers).
PendingTransformer is stateful: transform(&mut self, ...) and reset(&mut self).MdStream::snapshot_blocks requires &mut self.append_ref) and async usageappend_ref is intended for the common UI architecture where the UI thread owns the stream
and receives chunks via a channel:
UpdateRef borrows from the stream; it is not suitable for sending across
threads/tasks.append() (owned Update) or convert a borrowed view
via UpdateRef::to_owned() (this may allocate).[dependencies]
mdstream = "0.2.0"
Optional Tokio glue (delta coalescing + helpers):
[dependencies]
mdstream-tokio = "0.2.0"
Backpressure policy (producer side):
Block: never drop; safest for real content.DropNew: drop when UI is slow; good for best-effort signals.CoalesceLocal: buffer locally and flush opportunistically; good for high-frequency token streams.Practical examples:
CoalesceLocal on the producer, and CoalescingReceiver on the UI side.use mdstream_tokio::{BackpressurePolicy, CoalescePreset, CoalescingReceiver, DeltaSender};
use tokio::sync::mpsc;
let (tx, rx) = mpsc::channel::<String>(64);
let mut sender = DeltaSender::new(tx, BackpressurePolicy::CoalesceLocal);
let mut rx = CoalescingReceiver::new(rx, CoalescePreset::Balanced.options());
// producer task: sender.send(token_or_chunk).await
// UI task: if let Some(chunk) = rx.recv().await { stream.append(&chunk); }
DropNew (or a separate small channel just for status).let (tx, rx) = mpsc::channel::<String>(1);
let mut sender = DeltaSender::new(tx, BackpressurePolicy::DropNew);
// producer: sender.send("thinking...").await; // may be dropped if UI is busy
Block with a bounded channel (natural backpressure), optionally with receiver-side coalescing.let (tx, rx) = mpsc::channel::<String>(256);
let mut sender = DeltaSender::new(tx, BackpressurePolicy::Block);
// producer: sender.send(line).await; // waits if UI falls behind
Rule of thumb:
Block or CoalesceLocal.DropNew.Recommended: keep UI state in DocumentState and apply each Update to it. This makes reset
handling hard to get wrong.
use mdstream::{DocumentState, MdStream, Options};
let mut stream = MdStream::new(Options::default());
let mut state = DocumentState::new();
let u = stream.append("# Title\n\nHello **wor");
let applied = state.apply(u);
if applied.reset {
// Drop any external caches derived from old blocks.
}
// If you enable invalidation (see below), `applied.invalidated` tells you which committed blocks to refresh.
for b in state.committed() {
// Render stable blocks once.
let text = b.display_or_raw();
let _ = text;
}
if let Some(p) = state.pending() {
// Render/update the pending block each tick.
let text = p.display_or_raw();
let _ = text;
}
If you prefer to manage your own (Vec<Block>, Option<Block>), you can apply updates with
Update::apply_to.
cargo run -p mdstream --example minimal
cargo run -p mdstream --example footnotes_reset
cargo run -p mdstream --example stateful_transformer
cargo run -p mdstream --example tui_like
cargo run -p mdstream --features pulldown --example pulldown_incremental
Tokio + ratatui demo (agent-style streaming):
cargo run -p mdstream-tokio --example agent_tui
You can also choose a producer-side backpressure policy:
cargo run -p mdstream-tokio --example agent_tui -- --policy coalesce-local
cargo run -p mdstream-tokio --example agent_tui -- --policy drop-new
cargo run -p mdstream-tokio --example agent_tui -- --policy block
Keys:
q: quitj/k or ↑/↓: scrollg/G: top/bottomf: toggle follow-tailc: cycle coalescing mode[ / ]: adjust pending code tail linesMarkdown reference-style links/images can be defined after they are used:
See [docs][ref]. or See [ref].[ref]: https://example.comIn streaming UIs that parse/render each committed block independently, late-arriving reference definitions can require re-parsing earlier blocks so they turn into real links.
mdstream provides an opt-in invalidation signal for this:
opts.reference_definitions = ReferenceDefinitionsMode::InvalidateUpdate.invalidated contains the BlockIds of
previously committed blocks that likely used the label.This is intentionally best-effort (optimized for LLM streaming), not a full CommonMark/GFM reference definition implementation:
^[ ]{0,3}[label]: ...), footnotes ([^x]:) are excluded.Example:
use mdstream::{MdStream, Options, ReferenceDefinitionsMode};
let mut opts = Options::default();
opts.reference_definitions = ReferenceDefinitionsMode::Invalidate;
let mut s = MdStream::new(opts);
let u1 = s.append("See [ref].\n\n");
assert!(u1.committed.is_empty());
let u2 = s.append("[ref]: https://example.com\n\nNext\n");
assert!(u2.invalidated.contains(&mdstream::BlockId(1)));
pulldown-cmark Adapter (pulldown feature)mdstream is render-agnostic. If you want to reuse the Rust ecosystem around pulldown-cmark
(egui, gpui/Zed, TUI renderers), enable the adapter feature:
[dependencies]
mdstream = { version = "0.2.0", features = ["pulldown"] }
When reference_definitions invalidation is enabled, the adapter can re-parse only the invalidated
blocks:
use mdstream::adapters::pulldown::{PulldownAdapter, PulldownAdapterOptions};
use mdstream::{MdStream, Options, ReferenceDefinitionsMode};
let mut opts = Options::default();
opts.reference_definitions = ReferenceDefinitionsMode::Invalidate;
let mut stream = MdStream::new(opts);
let mut adapter = PulldownAdapter::new(PulldownAdapterOptions::default());
stream.append("See [ref].\n\n");
let u1 = stream.append("[ref]: https://example.com\n");
adapter.apply_update(&u1);
stream.append("\n");
let u2 = stream.append("Next\n");
adapter.apply_update(&u2);
// `u2.invalidated` tells you which committed blocks should be re-rendered.
CHANGELOG.mdRELEASE_CHECKLIST.mddocs/ during releases; user-facing guidance lives in this README.docs/ADR_0001_STREAMING_CONCURRENCY.mdsync (opt-in) for Send + Sync extension points.streamdown: https://github.com/vercel/streamdownincremark: https://github.com/kingshuaishuai/incremarkInitial MVP implementation is in progress:
MdStream core state machine (blocks: committed + pending)pulldown-cmark adapter via the pulldown featureTry the demo:
cargo run -p mdstream --example tui_like
Try the pulldown-cmark incremental demo:
cargo run -p mdstream --features pulldown --example pulldown_incremental