fix: harden resume and SSE replay
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import { test, expect } from "vitest";
|
||||
import { buildApp } from "../src/app.js";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import { SseHub } from "../src/sse/sse-hub.js";
|
||||
|
||||
async function readUntil(
|
||||
reader: ReadableStreamDefaultReader<Uint8Array>,
|
||||
predicate: (text: string) => boolean,
|
||||
): Promise<string> {
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
const deadline = Date.now() + 1_000;
|
||||
while (Date.now() < deadline) {
|
||||
const result = await Promise.race([
|
||||
reader.read(),
|
||||
new Promise<{ timeout: true }>((resolve) =>
|
||||
setTimeout(() => resolve({ timeout: true }), Math.max(0, deadline - Date.now())),
|
||||
),
|
||||
]);
|
||||
if ("timeout" in result || result.done) break;
|
||||
text += decoder.decode(result.value);
|
||||
if (predicate(text)) break;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
async function captureReplay(options: { query?: string; lastEventId?: string }): Promise<string> {
|
||||
const hub = new SseHub();
|
||||
hub.publish("s1", "info", { type: "info", text: "one" });
|
||||
hub.publish("s1", "info", { type: "info", text: "two" });
|
||||
hub.publish("s1", "info", { type: "info", text: "three" });
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "/tmp/h" }), {
|
||||
hub,
|
||||
thtRunner: {} as any,
|
||||
});
|
||||
await app.listen({ port: 0, host: "127.0.0.1" });
|
||||
const port = (app.server.address() as { port: number }).port;
|
||||
const controller = new AbortController();
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://127.0.0.1:${port}/sessions/s1/events${options.query ?? ""}`,
|
||||
{
|
||||
headers: options.lastEventId ? { "Last-Event-ID": options.lastEventId } : undefined,
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
const reader = response.body!.getReader();
|
||||
const body = await readUntil(reader, (text) => text.includes('"three"'));
|
||||
await reader.cancel();
|
||||
return body;
|
||||
} finally {
|
||||
controller.abort();
|
||||
await app.close();
|
||||
}
|
||||
}
|
||||
|
||||
test("SSE emits ids and honors the native Last-Event-ID replay cursor", async () => {
|
||||
const body = await captureReplay({ lastEventId: "1" });
|
||||
|
||||
expect(body).not.toContain('"one"');
|
||||
expect(body).toContain("id: 2\nevent: info\n");
|
||||
expect(body).toContain('data: {"type":"info","text":"two"}');
|
||||
expect(body).toContain("id: 3\nevent: info\n");
|
||||
expect(body).toContain('data: {"type":"info","text":"three"}');
|
||||
});
|
||||
|
||||
test("SSE honors the manual lastEventId query cursor", async () => {
|
||||
const body = await captureReplay({ query: "?lastEventId=2" });
|
||||
|
||||
expect(body).not.toContain('"one"');
|
||||
expect(body).not.toContain('"two"');
|
||||
expect(body).toContain("id: 3\nevent: info\n");
|
||||
expect(body).toContain('data: {"type":"info","text":"three"}');
|
||||
});
|
||||
|
||||
test("SSE uses the newer valid cursor when header and query are both present", async () => {
|
||||
const body = await captureReplay({ query: "?lastEventId=2", lastEventId: "1" });
|
||||
|
||||
expect(body).not.toContain('"two"');
|
||||
expect(body).toContain("id: 3\nevent: info\n");
|
||||
});
|
||||
Reference in New Issue
Block a user