* fix(event-stream): isolate subscriber failures * fix(event-stream): handle async subscriber failures --------- Co-authored-by: Isaac Sanchez <isanchez-hawkins@arize.com>
141 lines
3.7 KiB
JavaScript
141 lines
3.7 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import { createGlobalMessageStreamHub } from './global-hub.js';
|
|
|
|
function createSseResponse({ blocks = [] } = {}) {
|
|
const encoder = new TextEncoder();
|
|
let index = 0;
|
|
|
|
return {
|
|
ok: true,
|
|
body: {
|
|
getReader() {
|
|
return {
|
|
async read() {
|
|
if (index < blocks.length) {
|
|
return { value: encoder.encode(blocks[index++]), done: false };
|
|
}
|
|
return { value: undefined, done: true };
|
|
},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
async function waitForAssertion(assertion) {
|
|
const deadline = Date.now() + 1000;
|
|
let lastError;
|
|
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
assertion();
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
describe('createGlobalMessageStreamHub', () => {
|
|
it('continues fanout when an event subscriber throws', async () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const received = [];
|
|
const hub = createGlobalMessageStreamHub({
|
|
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
upstreamReconnectDelayMs: 100,
|
|
fetchImpl: async () => createSseResponse({
|
|
blocks: [
|
|
'id: evt-1\ndata: {"type":"session.updated","properties":{}}\n\n',
|
|
],
|
|
}),
|
|
});
|
|
|
|
hub.subscribeEvent(() => {
|
|
throw new Error('subscriber failed');
|
|
});
|
|
hub.subscribeEvent((event) => {
|
|
received.push(event.eventId);
|
|
});
|
|
|
|
try {
|
|
hub.start();
|
|
await waitForAssertion(() => {
|
|
expect(received).toEqual(['evt-1']);
|
|
});
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
} finally {
|
|
hub.stop();
|
|
warnSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('continues status fanout when a status subscriber throws', async () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const received = [];
|
|
const hub = createGlobalMessageStreamHub({
|
|
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
upstreamReconnectDelayMs: 100,
|
|
fetchImpl: async () => createSseResponse(),
|
|
});
|
|
|
|
hub.subscribeStatus(() => {
|
|
throw new Error('status subscriber failed');
|
|
});
|
|
hub.subscribeStatus((status) => {
|
|
received.push(status.type);
|
|
});
|
|
|
|
try {
|
|
hub.start();
|
|
await waitForAssertion(() => {
|
|
expect(received).toContain('connect');
|
|
});
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
} finally {
|
|
hub.stop();
|
|
warnSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('continues fanout when an async event subscriber rejects', async () => {
|
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
const received = [];
|
|
const hub = createGlobalMessageStreamHub({
|
|
buildOpenCodeUrl: (pathname) => `http://127.0.0.1:4096${pathname}`,
|
|
getOpenCodeAuthHeaders: () => ({}),
|
|
upstreamReconnectDelayMs: 100,
|
|
fetchImpl: async () => createSseResponse({
|
|
blocks: [
|
|
'id: evt-1\ndata: {"type":"session.updated","properties":{}}\n\n',
|
|
],
|
|
}),
|
|
});
|
|
|
|
hub.subscribeEvent(async () => {
|
|
throw new Error('async subscriber failed');
|
|
});
|
|
hub.subscribeEvent((event) => {
|
|
received.push(event.eventId);
|
|
});
|
|
|
|
try {
|
|
hub.start();
|
|
await waitForAssertion(() => {
|
|
expect(received).toEqual(['evt-1']);
|
|
});
|
|
await waitForAssertion(() => {
|
|
expect(warnSpy).toHaveBeenCalled();
|
|
});
|
|
} finally {
|
|
hub.stop();
|
|
warnSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|