Archived
fix(realtime): isolate notification failures
This commit is contained in:
@@ -54,6 +54,28 @@ describe("SessionEventHub", () => {
|
||||
expect(removed.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues publishing session events when one socket send fails", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const failed = new FakeSocket();
|
||||
const healthy = new FakeSocket();
|
||||
failed.send.mockImplementation(() => { throw new Error("socket closed"); });
|
||||
hub.add("s1", failed);
|
||||
hub.add("s1", healthy);
|
||||
|
||||
hub.publish("s1", { type: "assistant.delta", text: "hello" });
|
||||
|
||||
expect(failed.send).toHaveBeenCalledOnce();
|
||||
expect(healthy.send).toHaveBeenCalledWith(JSON.stringify({ type: "assistant.delta", text: "hello", seq: 1 }));
|
||||
expect(hub.currentSeq("s1")).toBe(1);
|
||||
|
||||
failed.send.mockClear();
|
||||
hub.publish("s1", { type: "assistant.delta", text: "again" });
|
||||
|
||||
expect(failed.send).not.toHaveBeenCalled();
|
||||
expect(healthy.send).toHaveBeenLastCalledWith(JSON.stringify({ type: "assistant.delta", text: "again", seq: 2 }));
|
||||
expect(hub.currentSeq("s1")).toBe(2);
|
||||
});
|
||||
|
||||
it("publishes global events only to global sockets", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const globalSocket = new FakeSocket();
|
||||
@@ -78,6 +100,26 @@ describe("SessionEventHub", () => {
|
||||
expect(sessionSocket.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("continues publishing unstamped global events when one socket send fails", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const failed = new FakeSocket();
|
||||
const healthy = new FakeSocket();
|
||||
failed.send.mockImplementation(() => { throw new Error("socket closed"); });
|
||||
hub.addGlobal(failed);
|
||||
hub.addGlobal(healthy);
|
||||
|
||||
hub.publishGlobal({ type: "session.name", sessionId: "s1", name: "Renamed" });
|
||||
|
||||
expect(failed.send).toHaveBeenCalledOnce();
|
||||
expect(healthy.send).toHaveBeenCalledWith(JSON.stringify({ type: "session.name", sessionId: "s1", name: "Renamed" }));
|
||||
|
||||
failed.send.mockClear();
|
||||
hub.publishGlobal({ type: "session.name", sessionId: "s1", name: "Renamed again" });
|
||||
|
||||
expect(failed.send).not.toHaveBeenCalled();
|
||||
expect(healthy.send).toHaveBeenLastCalledWith(JSON.stringify({ type: "session.name", sessionId: "s1", name: "Renamed again" }));
|
||||
});
|
||||
|
||||
it("stamps a monotonically increasing per-session seq on published events", () => {
|
||||
const hub = new SessionEventHub();
|
||||
const socket = new FakeSocket();
|
||||
|
||||
@@ -34,9 +34,7 @@ export class SessionEventHub {
|
||||
const seq = (this.seqBySession.get(sessionId) ?? 0) + 1;
|
||||
this.seqBySession.set(sessionId, seq);
|
||||
const payload = JSON.stringify({ ...projectBrowserSessionEvent(event), seq });
|
||||
for (const socket of this.socketsBySession.get(sessionId) ?? []) {
|
||||
if (socket.readyState === socket.OPEN) socket.send(payload);
|
||||
}
|
||||
this.sendToSockets(this.socketsBySession.get(sessionId), payload);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,8 +53,18 @@ export class SessionEventHub {
|
||||
|
||||
publishRealtime(event: RealtimeEvent): void {
|
||||
const payload = JSON.stringify(event);
|
||||
for (const socket of this.globalSockets) {
|
||||
if (socket.readyState === socket.OPEN) socket.send(payload);
|
||||
this.sendToSockets(this.globalSockets, payload);
|
||||
}
|
||||
|
||||
private sendToSockets(sockets: Set<RealtimeSocket> | undefined, payload: string): void {
|
||||
if (sockets === undefined) return;
|
||||
for (const socket of sockets) {
|
||||
if (socket.readyState !== socket.OPEN) continue;
|
||||
try {
|
||||
socket.send(payload);
|
||||
} catch {
|
||||
sockets.delete(socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user