Skip to content

Commit 4eaf21e

Browse files
committed
feat: US-190 - Add SSE (Server-Sent Events) streaming project-matrix fixture
1 parent 79d4227 commit 4eaf21e

4 files changed

Lines changed: 138 additions & 0 deletions

File tree

docs/nodejs-compatibility.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ The [project-matrix test suite](https://github.com/rivet-dev/secure-exec/tree/ma
102102
| yarn-berry-layout | Package Manager | Yarn Berry PnP/node_modules layout |
103103
| bun-layout | Package Manager | Bun `node_modules` layout |
104104
| workspace-layout | Package Manager | npm workspace `node_modules` layout |
105+
| sse-streaming | Networking | SSE server, chunked transfer-encoding, streaming reads |
105106
| net-unsupported (fail) | Error Handling | `net.createServer` correctly errors |
106107

107108
To request a new package be added to the test suite, [open an issue](https://github.com/rivet-dev/secure-exec/issues/new?labels=package-request&title=Package+request:+%5Bpackage-name%5D).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"entry": "src/index.js",
3+
"expectation": "pass"
4+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "project-matrix-sse-streaming-pass",
3+
"private": true,
4+
"type": "commonjs"
5+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"use strict";
2+
3+
const http = require("http");
4+
5+
// SSE events to send — exercises data-only, named events, id field, retry field
6+
const sseEvents = [
7+
"retry: 3000\n\n",
8+
"data: hello-world\n\n",
9+
"event: status\ndata: {\"connected\":true}\n\n",
10+
"id: msg-3\nevent: update\ndata: first line\ndata: second line\n\n",
11+
"id: msg-4\ndata: final-event\n\n",
12+
];
13+
14+
function createSSEServer() {
15+
return http.createServer((req, res) => {
16+
if (req.url !== "/events") {
17+
res.writeHead(404);
18+
res.end();
19+
return;
20+
}
21+
22+
res.writeHead(200, {
23+
"Content-Type": "text/event-stream",
24+
"Cache-Control": "no-cache",
25+
Connection: "keep-alive",
26+
});
27+
28+
// Send all events then close
29+
for (const event of sseEvents) {
30+
res.write(event);
31+
}
32+
res.end();
33+
});
34+
}
35+
36+
// Parse SSE text/event-stream format into structured events
37+
function parseSSEStream(raw) {
38+
const events = [];
39+
let current = {};
40+
41+
for (const line of raw.split("\n")) {
42+
if (line === "") {
43+
// Empty line = event boundary
44+
if (Object.keys(current).length > 0) {
45+
events.push(current);
46+
current = {};
47+
}
48+
continue;
49+
}
50+
51+
const colonIdx = line.indexOf(":");
52+
if (colonIdx === 0) continue; // comment line
53+
54+
let field, value;
55+
if (colonIdx > 0) {
56+
field = line.slice(0, colonIdx);
57+
// Strip single leading space after colon per SSE spec
58+
value = line.slice(colonIdx + 1);
59+
if (value.startsWith(" ")) value = value.slice(1);
60+
} else {
61+
field = line;
62+
value = "";
63+
}
64+
65+
if (field === "data") {
66+
// Multiple data fields are joined with newline
67+
current.data = current.data != null ? current.data + "\n" + value : value;
68+
} else {
69+
current[field] = value;
70+
}
71+
}
72+
73+
// Trailing event without final blank line
74+
if (Object.keys(current).length > 0) {
75+
events.push(current);
76+
}
77+
78+
return events;
79+
}
80+
81+
async function main() {
82+
const server = createSSEServer();
83+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
84+
const port = server.address().port;
85+
86+
try {
87+
const response = await new Promise((resolve, reject) => {
88+
http.get(
89+
{ hostname: "127.0.0.1", port, path: "/events" },
90+
(res) => {
91+
let body = "";
92+
res.on("data", (chunk) => (body += chunk));
93+
res.on("end", () =>
94+
resolve({
95+
statusCode: res.statusCode,
96+
headers: res.headers,
97+
body,
98+
}),
99+
);
100+
},
101+
).on("error", reject);
102+
});
103+
104+
const headers = {
105+
contentType: response.headers["content-type"],
106+
connection: response.headers["connection"],
107+
cacheControl: response.headers["cache-control"],
108+
};
109+
110+
const events = parseSSEStream(response.body);
111+
112+
const result = {
113+
statusCode: response.statusCode,
114+
headers,
115+
eventCount: events.length,
116+
events,
117+
};
118+
119+
console.log(JSON.stringify(result));
120+
} finally {
121+
await new Promise((resolve) => server.close(resolve));
122+
}
123+
}
124+
125+
main().catch((err) => {
126+
console.error(err.message);
127+
process.exit(1);
128+
});

0 commit comments

Comments
 (0)