Share upstream event stream hub

This commit is contained in:
Bohdan Triapitsyn
2026-04-24 12:30:11 +03:00
parent 8e610ca8b3
commit 7030549d78
12 changed files with 1436 additions and 201 deletions
@@ -127,6 +127,273 @@ describe('event stream broadcaster', () => {
});
describe('message stream websocket runtime', () => {
it('shares one global upstream SSE reader across multiple websocket clients', async () => {
const server = new EventEmitter();
const wsClients = new Set();
let fetchCalls = 0;
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload() {},
wsClients,
upstreamReconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
fetchCalls += 1;
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
},
});
const firstSocket = new FakeSocket();
const secondSocket = new FakeSocket();
runtime.wsServer.emit('connection', firstSocket, { url: '/api/global/event/ws' });
runtime.wsServer.emit('connection', secondSocket, { url: '/api/global/event/ws' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(fetchCalls).toBe(1);
expect(firstSocket.sent).toContainEqual({ type: 'ready', scope: 'global' });
expect(secondSocket.sent).toContainEqual({ type: 'ready', scope: 'global' });
expect(firstSocket.sent).toContainEqual({
type: 'event',
payload: { type: 'server.connected', properties: {} },
eventId: 'evt-1',
directory: 'global',
});
expect(secondSocket.sent).toContainEqual({
type: 'event',
payload: { type: 'server.connected', properties: {} },
eventId: 'evt-1',
directory: 'global',
});
firstSocket.close();
secondSocket.close();
await runtime.close();
});
it('replays buffered global events after a reconnecting client Last-Event-ID', async () => {
const server = new EventEmitter();
const wsClients = new Set();
let fetchCalls = 0;
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload() {},
wsClients,
upstreamReconnectDelayMs: 0,
fetchImpl: async (_url, options) => {
fetchCalls += 1;
if (fetchCalls === 1) {
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
'id: evt-2\ndata: {"type":"session.updated","properties":{"directory":"/tmp/project"}}\n\n',
],
});
}
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [],
});
},
});
const firstSocket = new FakeSocket();
runtime.wsServer.emit('connection', firstSocket, { url: '/api/global/event/ws' });
await new Promise((resolve) => setTimeout(resolve, 5));
firstSocket.close();
const secondSocket = new FakeSocket();
runtime.wsServer.emit('connection', secondSocket, { url: '/api/global/event/ws?lastEventId=evt-1' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(secondSocket.sent).toContainEqual({ type: 'ready', scope: 'global' });
expect(secondSocket.sent).toContainEqual({
type: 'event',
payload: { type: 'session.updated', properties: { directory: '/tmp/project' } },
eventId: 'evt-2',
directory: '/tmp/project',
});
secondSocket.close();
await runtime.close();
});
it('keeps directory websocket streams on separate upstream readers', async () => {
const server = new EventEmitter();
const wsClients = new Set();
const fetchUrls = [];
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload() {},
wsClients,
upstreamReconnectDelayMs: 0,
fetchImpl: async (url, options) => {
fetchUrls.push(url);
return createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"server.connected","properties":{}}\n\n',
],
});
},
});
const firstSocket = new FakeSocket();
const secondSocket = new FakeSocket();
runtime.wsServer.emit('connection', firstSocket, { url: '/api/event/ws?directory=/tmp/one' });
runtime.wsServer.emit('connection', secondSocket, { url: '/api/event/ws?directory=/tmp/two' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(fetchUrls).toHaveLength(2);
expect(new URL(fetchUrls[0]).searchParams.get('directory')).toBe('/tmp/one');
expect(new URL(fetchUrls[1]).searchParams.get('directory')).toBe('/tmp/two');
expect(firstSocket.sent).toContainEqual({ type: 'ready', scope: 'directory' });
expect(secondSocket.sent).toContainEqual({ type: 'ready', scope: 'directory' });
firstSocket.close();
secondSocket.close();
await runtime.close();
});
it('closes the websocket and triggers health check on initial upstream unavailable response', async () => {
const server = new EventEmitter();
const wsClients = new Set();
let triggerHealthCheckCalls = 0;
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload() {},
wsClients,
triggerHealthCheck: () => {
triggerHealthCheckCalls += 1;
},
upstreamReconnectDelayMs: 0,
fetchImpl: async () => ({
ok: false,
status: 503,
body: null,
}),
});
const socket = new FakeSocket();
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(socket.sent).toEqual([
{
type: 'error',
message: 'OpenCode event stream unavailable (503)',
},
]);
expect(socket.closeCalls).toEqual([
{
code: 1011,
reason: 'OpenCode event stream unavailable',
},
]);
expect(triggerHealthCheckCalls).toBe(1);
expect(wsClients.size).toBe(0);
await runtime.close();
});
it('closes the websocket without health check when OpenCode URL cannot be built', async () => {
const server = new EventEmitter();
const wsClients = new Set();
let triggerHealthCheckCalls = 0;
let fetchCalls = 0;
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl() {
throw new Error('missing OpenCode port');
},
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload() {},
wsClients,
triggerHealthCheck: () => {
triggerHealthCheckCalls += 1;
},
upstreamReconnectDelayMs: 0,
fetchImpl: async () => {
fetchCalls += 1;
throw new Error('fetch should not be called');
},
});
const socket = new FakeSocket();
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(socket.sent).toEqual([
{
type: 'error',
message: 'OpenCode service unavailable',
},
]);
expect(socket.closeCalls).toEqual([
{
code: 1011,
reason: 'OpenCode service unavailable',
},
]);
expect(fetchCalls).toBe(0);
expect(triggerHealthCheckCalls).toBe(0);
await runtime.close();
});
it('reconnects a stalled upstream SSE stream and resumes from the last event id', async () => {
const server = new EventEmitter();
const wsClients = new Set();
@@ -192,4 +459,54 @@ describe('message stream websocket runtime', () => {
socket.close();
await runtime.close();
});
it('keeps synthetic event processing on forwarded upstream events', async () => {
const server = new EventEmitter();
const wsClients = new Set();
const runtime = createMessageStreamWsRuntime({
server,
uiAuthController: null,
isRequestOriginAllowed: async () => true,
rejectWebSocketUpgrade() {
throw new Error('upgrade should not be used in this test');
},
buildOpenCodeUrl: (path) => `http://127.0.0.1:4096${path}`,
getOpenCodeAuthHeaders: () => ({}),
processForwardedEventPayload(payload, emitSynthetic) {
if (payload.type === 'session.updated') {
emitSynthetic({ type: 'openchamber:session-status', sessionID: 'ses_1' });
}
},
wsClients,
upstreamReconnectDelayMs: 0,
fetchImpl: async (_url, options) => createSseResponse({
signal: options.signal,
holdOpen: true,
blocks: [
'id: evt-1\ndata: {"type":"session.updated","properties":{"directory":"/tmp/project"}}\n\n',
],
}),
});
const socket = new FakeSocket();
runtime.wsServer.emit('connection', socket, { url: '/api/global/event/ws' });
await new Promise((resolve) => setTimeout(resolve, 5));
expect(socket.sent).toContainEqual({
type: 'event',
payload: { type: 'session.updated', properties: { directory: '/tmp/project' } },
eventId: 'evt-1',
directory: '/tmp/project',
});
expect(socket.sent).toContainEqual({
type: 'event',
payload: { type: 'openchamber:session-status', sessionID: 'ses_1' },
directory: 'global',
});
socket.close();
await runtime.close();
});
});