Share upstream event stream hub
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user