Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for server-sent events #481

Closed
hadley opened this issue Jun 27, 2024 · 6 comments
Closed

Add support for server-sent events #481

hadley opened this issue Jun 27, 2024 · 6 comments
Labels
feature a feature request or enhancement

Comments

@hadley
Copy link
Member

hadley commented Jun 27, 2024

https://developer.mozilla.org/en-US/docs/Web/API/Server-sent%5Fevents/Using%5Fserver-sent%5Fevents#event_stream_format

Maybe req_perform_sse()? And then have some way to register functions for each event?

cutpoint <- function(bytes) {
  nl <- which(bytes == charToRaw("\n"))
  nl[nl %in% (setdiff(nl, 1) - 1)]
}

parser <- function(bytes) {
  text <- rawToChar(bytes)
  events <- strsplit(text, "\n\n")[[1]]
  lines <- strsplit(text, "\n")
  ...
}

req |> 
  req_perform_stream(round = cutpoint, callback = parser)
@hadley hadley added the feature a feature request or enhancement label Jul 10, 2024
@hadley
Copy link
Member Author

hadley commented Sep 3, 2024

Done in #521

@hadley hadley closed this as completed Sep 3, 2024
@lawremi
Copy link

lawremi commented Sep 20, 2024

Here is an alternative implementation that I was preparing to submit as a PR, but @jcheng5 beat me by a few weeks :)

It follows the simple approach outlined by @hadley above and mimics the Javascript EventSource API, with handlers for specific events. Just wanted to stick it here in case it is useful. The new connection-based streaming in #521 is more generally useful, of course.

req_perform_sse <- function(req, onmessage_callback = NULL,
                            event_callbacks = list(),
                            timeout_sec = Inf, buffer_kb = 64)
{
    callback <- function(bytes) {
        text <- rawToChar(bytes)
        msgs <- strsplit(text, "\n\n", fixed = TRUE)[[1L]]
        m <- gregexec("(event|data|id|retry): ?(.*)", msgs, perl = TRUE)
        for (mat in regmatches(msgs, m)) {
            event <- split(mat[3L,], mat[2L,])
            event$data <- paste(event$data, collapse = "\n")
            if (is.null(event$event))
                callback <- onmessage_callback
            else callback <- event_callbacks[[event$event]]
            if (!is.null(callback) && !isTRUE(callback(event)))
                return(FALSE)
        }
        TRUE
    }
    round <- function(bytes) {
        nl <- bytes == charToRaw("\n")
        which(diff(nl) == 0L & nl[-1L])
    }
    httr2::req_perform_stream(req, callback, timeout_sec, buffer_kb, round)
}

@lawremi
Copy link

lawremi commented Sep 21, 2024

I looked a bit into satisfying the draft spec, but the retry logic is a bit complicated. For example, I think req_retry() would need to gain the ability to modify the header on retries, in order to add the Last-Event-ID header.

@jcheng5
Copy link
Member

jcheng5 commented Sep 21, 2024

I looked at it and thought for retry, it made more sense to implement the EventSource object exactly as it is in JS: a separate abstraction that sits one level above requests. I didn’t do it due to time pressure but also I am slightly skeptical that there is much adoption of Last-Event-ID in real world services (would love to find out I’m wrong though).

@lawremi
Copy link

lawremi commented Sep 21, 2024

Yea, it's still an evolving spec, so it makes sense to hold off until there is a use case.

@jcheng5
Copy link
Member

jcheng5 commented Sep 22, 2024

Oh, I don’t think it’s evolving; the HTML whatwg spec is “living” so it’s never officially stable. The EventSource part of it is very old, there’s a w3c spec from back in the day.

If you wanted to write a full EventSource client just for completeness I think that would make sense and also be pretty fun. But I think we already have everything we would need or want for the LLM API cases in particular, as surely none of them implement retry?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature a feature request or enhancement
Projects
None yet
Development

No branches or pull requests

3 participants