Skip to content
Skillv1.0.0

salvo-sse

Implement Server-Sent Events for real-time server-to-client updates. Use for live feeds, notifications, and streaming data.

by salvo-rs(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from salvo-rs/salvo-skills (salvo-sse/SKILL.md). Install upstream with npx skills add salvo-rs/salvo-skills --skill salvo-sse. Copyright stays with the author.

Salvo Server-Sent Events (SSE)

salvo::sse::stream(res, event_stream) writes a text/event-stream body from any TryStream<Ok = SseEvent> whose error implements std::error::Error + Send + Sync + 'static. SseKeepAlive wraps a stream and injects periodic comment frames when idle.

Setup

[dependencies]
salvo = { version = "0.94.0", features = ["sse"] }
futures-util = "0.3"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
async-stream = "0.3"   # only for async_stream::stream! macro

Counter

use std::convert::Infallible;
use std::time::Duration;
use futures_util::StreamExt;
use salvo::prelude::*;
use salvo::sse::{self, SseEvent};
use tokio::time::interval;
use tokio_stream::wrappers::IntervalStream;

#[handler]
async fn sse_counter(res: &mut Response) {
    let mut counter: u64 = 0;
    let event_stream = IntervalStream::new(interval(Duration::from_secs(1)))
        .map(move |_| {
            counter += 1;
            Ok::<_, Infallible>(SseEvent::default().text(counter.to_string()))
        });
    sse::stream(res, event_stream);
}

#[tokio::main]
async fn main() {
    let router = Router::new().push(Router::with_path("events").get(sse_counter));
    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}

Client: new EventSource('/events')<!-- minimal JS client omitted -->.

SseEvent builder

All setters except json return Self; json returns Result<Self, serde_json::Error>.

use std::time::Duration;
use salvo::sse::SseEvent;

SseEvent::default().text("hello");                              // data:hello
SseEvent::default().name("notification").text("new message");   // event:notification + data
SseEvent::default().name("update").json(&value)?;               // data:<serialized JSON>
SseEvent::default().id("msg-123").text("...");                  // id:msg-123
SseEvent::default().retry(Duration::from_secs(5)).text("...");  // retry:5000
SseEvent::default().comment("keep-alive");                      // :keep-alive (ignored by clients)

Text data is split on \n into multiple data: lines automatically.

Keep-alive

SseKeepAlive emits a comment frame after max_interval of inactivity; any real event from the inner stream resets the timer.

use std::time::Duration;
use salvo::sse::SseKeepAlive;

#[handler]
async fn sse_with_keepalive(res: &mut Response) {
    let stream = create_event_stream();
    SseKeepAlive::new(stream)
        .max_interval(Duration::from_secs(15))  // NOTE: max_interval, not interval
        .comment("ping")                         // NOTE: comment(), not text()
        .stream(res);
}

Broadcast channel → SSE

Most real apps wire a tokio::sync::broadcast receiver into an async stream:

use std::convert::Infallible;
use std::time::Duration;
use salvo::prelude::*;
use salvo::sse::{SseEvent, SseKeepAlive};
use tokio::sync::broadcast;

#[derive(Clone, serde::Serialize)]
struct Notification { id: u64, title: String, body: String }

#[derive(Clone)]
struct Hub { tx: broadcast::Sender<Notification> }

impl Hub {
    fn new() -> Self {
        let (tx, _) = broadcast::channel(128);
        Self { tx }
    }
}

#[handler]
async fn notifications(depot: &mut Depot, res: &mut Response) {
    let hub = depot.get_typed::<Hub>().unwrap().clone();
    let mut rx = hub.tx.subscribe();

    let stream = async_stream::stream! {
        while let Ok(n) = rx.recv().await {
            if let Ok(evt) = SseEvent::default()
                .name("notification")
                .id(n.id.to_string())
                .json(&n)
            {
                yield Ok::<_, Infallible>(evt);
            }
        }
    };

    SseKeepAlive::new(stream)
        .max_interval(Duration::from_secs(30))
        .stream(res);
}

Each subscriber gets its own Receiver; broadcast lag (RecvError::Lagged) should be handled if clients can fall behind.

Chat room (pub/sub via broadcast)

use std::convert::Infallible;
use futures_util::StreamExt;
use salvo::prelude::*;
use salvo::sse::{self, SseEvent};
use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;

#[derive(Clone)]
struct Chat { tx: broadcast::Sender<String> }

#[handler]
async fn sse_subscribe(depot: &mut Depot, res: &mut Response) {
    let chat = depot.get_typed::<Chat>().unwrap().clone();
    let stream = BroadcastStream::new(chat.tx.subscribe())
        .filter_map(|item| async move {
            item.ok().map(|text| Ok::<_, Infallible>(SseEvent::default().text(text)))
        });
    sse::stream(res, stream);
}

#[handler]
async fn post_message(depot: &mut Depot, req: &mut Request) {
    let chat = depot.get_typed::<Chat>().unwrap().clone();
    let body = req.payload().await.map(|b| String::from_utf8_lossy(b).into_owned()).unwrap_or_default();
    let _ = chat.tx.send(body);
}

Last-Event-ID reconnect

Browsers auto-reconnect and resend the last seen event id in the Last-Event-ID header. Parse it and replay from there:

#[handler]
async fn sse_with_ids(req: &mut Request, res: &mut Response) {
    let last_id: u64 = req.header("Last-Event-ID").unwrap_or(0);
    let stream = async_stream::stream! {
        let mut id = last_id + 1;
        loop {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            yield Ok::<_, std::convert::Infallible>(
                SseEvent::default().id(id.to_string()).text(format!("Event {id}"))
            );
            id += 1;
        }
    };
    salvo::sse::stream(res, stream);
}

Gotchas

  • SseKeepAlive methods are max_interval and commentnot interval / text.
  • The stream's error type must implement std::error::Error + Send + Sync + 'static. Use Infallible for infinite streams, or salvo::Error / Box<dyn Error + Send + Sync>.
  • SSE is text-only; encode binary as base64 inside the event data.
  • sse::stream() sets Content-Type: text/event-stream and Cache-Control: no-cache — don't overwrite them afterwards.
  • Each subscriber holds a connection; pair with salvo-concurrency-limiter if client counts can spike.

Related Skills

  • salvo-websocket: WebSocket for bidirectional communication
  • salvo-realtime: Overview of real-time communication options
  • salvo-concurrency-limiter: Cap simultaneous subscribers

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/salvo-rs-salvo-skills-salvo-sse/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

salvo-rs-salvo-skills-salvo-sse.ocm.jsonjson
{
  "ocm": "1",
  "id": "salvo-rs-salvo-skills-salvo-sse",
  "kind": "skill",
  "name": "salvo-sse",
  "description": "Implement Server-Sent Events for real-time server-to-client updates. Use for live feeds, notifications, and streaming data.",
  "publisher": "salvo-rs",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "general"
    ],
    "tags": [
      "skill-md",
      "realtime",
      "sse",
      "server-sent-events",
      "streaming",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Implement Server-Sent Events for real-time server-to-client updates. Use for live feeds, notifications, and streaming data."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/salvo-rs/salvo-skills",
      "path": "salvo-sse/SKILL.md",
      "ref": "4dd929857cf46c6e4ed54c6fee044c86ffb0bebc",
      "url": "https://github.com/salvo-rs/salvo-skills/blob/4dd929857cf46c6e4ed54c6fee044c86ffb0bebc/salvo-sse/SKILL.md",
      "key": "salvo-rs/salvo-skills/salvo-sse/SKILL.md"
    }
  },
  "instructions": "# Salvo Server-Sent Events (SSE)\n\n`salvo::sse::stream(res, event_stream)` writes a `text/event-stream` body from any `TryStream<Ok = SseEvent>` whose error implements `std::error::Error + Send + Sync + 'static`. `SseKeepAlive` wraps a stream and injects periodic comment frames when idle.\n\n## Setup\n\n```toml\n[dependencies]\nsalvo = { version = \"0.94.0\", features = [\"sse\"] }\nfutures-util = \"0.3\"\ntokio = { version = \"1\", features = [\"full\"] }\ntokio-stream = \"0.1\"\nasync-stream = \"0.3\"   # only for async_stream::stream! macro\n```\n\n## Counter\n\n```rust\nuse std::convert::Infallible;\nuse std::time::Durat",
  "cost": {
    "context_tokens": 1601
  }
}

Fetch it by URL: GET /api/v1/registry/salvo-rs-salvo-skills-salvo-sse/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.