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

Support Server Sent Events #2443

Closed
lewisedc opened this issue Mar 20, 2023 · 12 comments
Closed

Support Server Sent Events #2443

lewisedc opened this issue Mar 20, 2023 · 12 comments
Labels
enhancement New feature or request

Comments

@lewisedc
Copy link

What is the problem this feature would solve?

Implementing Server-Sent Events from Buns HTTP server is seemingly not possible at the moment, as returning a ReadableStream as the data of a Response object sends no response data (headers or body content) until the stream is closed.

What is the feature you are proposing to solve the problem?

Being able to return a ReadableStream object as the data source of a HTTP Response, having headers be instantly sent to the clients, and data be sent to the client as it is enqueued. I imagine the API would look something like this:

const msg = new TextEncoder().encode("data: hello\r\n");

export default {
  async fetch(req) {
    let timerId: Timer | undefined;

    const body = new ReadableStream({
      start(controller) {
        timerId = setInterval(() => {
          controller.enqueue(msg);
        }, 1000);
      },
      cancel() {
        if (timerId) {
          clearInterval(Number(timerId));
        }
      },
    });

    return new Response(body, {
      headers: {
        "Content-Type": "text/event-stream",
      },
    });
  },
};

This is based off how Deno works, a Deno example can found here: https://dash.deno.com/playground/server-sent-events

What alternatives have you considered?

I think the Deno way best fits Bun as they both use the Web Standard Response object for HTTP Responses. And Bun also already allows returning streams in some cases, such as when using the file API.

However another method could be to implement something specific just for return streams to replicate Nodes res.write() method.

@lewisedc lewisedc added the enhancement New feature or request label Mar 20, 2023
@Jarred-Sumner
Copy link
Collaborator

The bug here is that server sent events needs some special handling

@Jarred-Sumner Jarred-Sumner changed the title Support for returning Web Stream objects in Bun HTTP Responses Support Server Sent Events Mar 20, 2023
@Electroid
Copy link
Contributor

There is also https://developer.chrome.com/articles/fetch-streaming-requests/ to consider. But because Bun only supports HTTP/1.x right now, it's not as relevant.

@cirospaciari
Copy link
Member

You can use HTTP / 1.1 at this moment until we add support for HTTP / 2

Flush was fixed by #3073

This should work on the next version:

function sendSSECustom(controller: ReadableStreamDirectController, eventName: string, data: string) {
  return controller.write(`event: ${eventName}\ndata:${JSON.stringify(data)}\n\n`);
}
function sendSSEMessage(controller: ReadableStreamDirectController, data: string) {
  return controller.write(`data:${JSON.stringify(data)}\n\n`);
}
function sse(req: Request): Response {
  const signal = req.signal;
  return new Response(
    new ReadableStream({
      type: "direct",
      async pull(controller: ReadableStreamDirectController) {
        while (!signal.aborted) {
          await sendSSECustom(controller, "bun", "Hello, World!");
          await sendSSEMessage(controller, "Hello, World!");
          await controller.flush();
          await Bun.sleep(1000);
        }
        controller.close();
      },
    }),
    { status: 200, headers: { "Content-Type": "text/event-stream" } },
  );
}
Bun.serve({
  port: 3000,
  fetch(req) {
    if (new URL(req.url).pathname === "/stream") {
      return sse(req);
    }
    return new Response("Hello, World!");
  },
});

You may will wanna use something like lastEventId to resume reconnections (by sending id:), and send retry: to control retry intervals.

const lastEventId = req.headers.get("last-event-id")

For consuming on the server side we will add support to the client soon.

@tipiirai
Copy link

tipiirai commented Jul 14, 2023

This works in Node, and almost in Bun.

// send proper headers
response.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    Connection: 'keep-alive'
  })

// send data every second
setInterval(function() {
   response.write(`data:foo\n\n`)
}, 1000)

With Bun the data is only sent every 5th time or so. Works fine with bigger messages. There must be some kind of flushing issue.

@simylein
Copy link
Contributor

Have a look at #2663. For me this is working without the flush issue using bun 0.7.0.

@gtrabanco
Copy link

I am on today's canary build and it is working great.

@Electroid
Copy link
Contributor

This issue has been fixed, tested as of Bun v1.0.7.

❯ curl http://localhost:3000/ -v
*   Trying 127.0.0.1:3000...
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET / HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.85.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/event-stream
< Date: Wed, 25 Oct 2023 23:48:18 GMT
< Transfer-Encoding: chunked
< 
data: hello
data: hello
data: hello
data: hello
^C⏎       

@tipiirai
Copy link

@Electroid @Jarred-Sumner can confirm. SSE works like a charm with v1.0.7! Announceworthy I'd say.

Thank you, gentlemen!

@mnpenner
Copy link

It's working for me too, but how do you fix the TypeScript errors? I'm getting

image

And if I add satisfies DirectUnderlyingSource to the object you pass into ReadableStream then I get:

image

If I look at the c'tor for Readable stream, I think DirectUnderlyingSource is the correct underlyingSource for type: "direct", and ReadableStreamDirectController looks correct too.

@perguth
Copy link

perguth commented Feb 19, 2024

No docs?

That's what the AI on the documentation page says:


Bun supports Server-Sent Events (SSE) and you can implement them by using a ReadableStream within a Response object. Headers are sent immediately to the client, and data is sent as it is enqueued. Here is a code example showing how you could set up SSE in Bun:

function sendSSEMessage(controller, data) {
  controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}

function sse(req) {
  const { signal } = req;
  return new Response(
    new ReadableStream({
      start(controller) {
        const interval = setInterval(() => {
          sendSSEMessage(controller, "Hello, World!");
        }, 1000);

        signal.onabort = () => {
          clearInterval(interval);
          controller.close();
        };
      }
    }),
    {
      status: 200,
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        "Connection": "keep-alive",
      }
    }
  );
}

Bun.serve({
  fetch(req) {
    if (new URL(req.url).pathname === "/sse") {
      return sse(req);
    }
    return new Response("Not Found", { status: 404 });
  }
});

(1)

This code sets up an SSE endpoint at /sse which sends a "Hello, World!" message to the client every second. It leverages the signal property of the request to handle cleanup when the connection is closed from the client side.

Remember to adjust the code according to your needs, especially if you want to include event IDs or custom event names.

@Makkalay
Copy link

Makkalay commented Dec 29, 2024

@ Not work, I use v 1.1.34

In node express code work (bun dev), but in bun the same code not (bun -b dev), return 204 no content.

@Makkalay
Copy link

Makkalay commented Dec 29, 2024

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

Successfully merging a pull request may close this issue.

10 participants