🧵 fix: Prevent Message Loading Race During Streaming (#13295)

This commit is contained in:
Danny Avila 2026-05-24 18:50:00 -04:00 committed by GitHub
parent a8c43a4126
commit f2be5baecf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 605 additions and 6 deletions

View file

@ -0,0 +1,56 @@
import type { Redis } from 'ioredis';
import { logger } from '@librechat/data-schemas';
import { RedisEventTransport } from '~/stream/implementations/RedisEventTransport';
import { createMockPublisher } from './helpers/publisher';
logger.silent = true;
function createMockSubscriber() {
return {
on: jest.fn(),
subscribe: jest.fn().mockResolvedValue(undefined),
unsubscribe: jest.fn().mockResolvedValue(undefined),
};
}
function getMessageHandler(mockSubscriber: ReturnType<typeof createMockSubscriber>) {
return mockSubscriber.on.mock.calls.find((call) => call[0] === 'message')?.[1] as (
channel: string,
message: string,
) => void;
}
describe('RedisEventTransport', () => {
it('resets stale abort-listener reorder state before the next real subscriber', async () => {
const mockPublisher = createMockPublisher();
const mockSubscriber = createMockSubscriber();
const transport = new RedisEventTransport(
mockPublisher as unknown as Redis,
mockSubscriber as unknown as Redis,
);
const streamId = 'reorder-abort-listener-reuse-test';
transport.onAbort(streamId, () => {});
const messageHandler = getMessageHandler(mockSubscriber);
const channel = `stream:{${streamId}}:events`;
for (let i = 0; i < 5; i++) {
await transport.emitChunk(streamId, { index: i });
messageHandler(channel, JSON.stringify({ type: 'chunk', seq: i, data: { index: i } }));
}
await mockPublisher.del(`stream:{${streamId}}:seq`);
const secondRunChunks: unknown[] = [];
transport.subscribe(streamId, {
onChunk: (event) => secondRunChunks.push(event),
});
messageHandler(channel, JSON.stringify({ type: 'chunk', seq: 0, data: { index: 0 } }));
expect(secondRunChunks.map((chunk) => (chunk as { index: number }).index)).toEqual([0]);
transport.destroy();
});
});

View file

@ -439,6 +439,12 @@ export class RedisEventTransport implements IEventTransport {
}
const streamState = this.streams.get(streamId)!;
// Internal listeners (for example cross-replica abort) can leave ordering
// state behind with no real SSE subscribers. A new subscriber is a fresh
// attachment and must not inherit that prior generation's expected seq.
if (streamState.count === 0) {
this.resetReorderBuffer(streamId);
}
streamState.count++;
streamState.handlers.set(subscriberId, handlers);