SSE
If your deployment target does not supports handling WebSocket upgrades, crossws SSE adapter allows to add integration based on web platform standards (fetch and EventSource)
WebsocketSSE client to connect.Usage
Server side
Define adapter:
import sseAdapter from "crossws/adapters/sse";
const ws = sseAdapter({
bidir: true, // Enable bidirectional messaging support
hooks: {
upgrade(request) {
// In case of bidirectional mode, extra auth is recommended based on request
// You can return a new Response() instead to abort
return {
headers: {},
};
},
open(peer) {
// Use this hook to send messages to peer
peer.send(`Welcome ${peer}`);
},
message(peer, message) {
// Accepting messages from peer (bidirectional mode)
console.log(`Message from ${peer}: ${message}`); // Message from <id>: ping
},
},
});
Inside your web server handler:
async fetch(request) {
// Handle crossws upgrade
if (
request.headers.get("accept") === "text/event-stream" ||
request.headers.has("x-crossws-id")
) {
return ws.fetch(request);
}
// Your normal application logic
return new Response("default page")
}
Using with Express or other Node.js frameworks
The SSE adapter's ws.fetch is a Web-standard fetch handler: it takes a Request and returns a Response. Node.js frameworks like Express instead work with the raw Node.js (req, res) style, so you need a small bridge to convert between the two.
Rather than manually converting streams with Readable.toWeb/Readable.fromWeb and copying over status/headers by hand, use the toNodeHandler helper from srvx, which turns any Web-standard fetch handler into a plain (req, res) Node.js handler:
import express from "express";
import { toNodeHandler } from "srvx/node";
import sseAdapter from "crossws/adapters/sse";
const ws = sseAdapter({
hooks: {
/* ... */
},
});
const app = express();
app.use((req, res, next) => {
if (
req.headers.accept === "text/event-stream" ||
req.headers["x-crossws-id"]
) {
return toNodeHandler((request) => ws.fetch(request))(req, res);
}
next();
});
toNodeHandler only needs the raw Node.js req/res objects, so the same pattern works for any framework whose handler ultimately exposes them — Fastify's request.raw/reply.raw, NestJS running on top of its Express or Fastify adapter, or a plain node:http server.Client side
In order to make communication with server, we need a special WebsocketSSE client.
import { WebsocketSSE } from "crossws/websocket/sse";
const ws = new WebsocketSSE("https://<server_address>", { bdir: true });
ws.addEventListener("open", () => {
ws.send("ping");
});
ws.addEventListener("message", (event) => {
console.log("Received:", event.data);
});
WebSocketSSE, uses EventSource to receive messages from server. In order to send messages to the server, it tries to make another connection stream using same peer id and if failed, fallback to fetch for each message.
In theory, it is possible to have communication on a single HTTP/2 connection, however, due to a current limitation in fetch standard we need 2 connections, one for receiving messages and one for sending.test/fixture/sse.ts for demo and src/adapters/sse.ts for implementation.