fix(chat): track replies by queued batch
This commit is contained in:
@@ -12,7 +12,7 @@ describe("MessageQueue", () => {
|
||||
});
|
||||
|
||||
it("sends the first message immediately", async () => {
|
||||
const consumer = vi.fn(() => true);
|
||||
const consumer = vi.fn();
|
||||
const queue = new MessageQueue();
|
||||
queue.setConsumer(consumer);
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("MessageQueue", () => {
|
||||
});
|
||||
|
||||
it("merges pending messages after the interval", async () => {
|
||||
const consumer = vi.fn(() => true);
|
||||
const consumer = vi.fn();
|
||||
const queue = new MessageQueue({ intervalMs: 1000 });
|
||||
queue.setConsumer(consumer);
|
||||
|
||||
@@ -48,10 +48,10 @@ describe("MessageQueue", () => {
|
||||
});
|
||||
|
||||
it("does not send a pending batch while a previous send is still running", async () => {
|
||||
let resolveFirstSend: (ok: boolean) => void = () => undefined;
|
||||
let resolveFirstSend: () => void = () => undefined;
|
||||
const consumer = vi.fn(
|
||||
() =>
|
||||
new Promise<boolean>((resolve) => {
|
||||
new Promise<void>((resolve) => {
|
||||
resolveFirstSend = resolve;
|
||||
}),
|
||||
);
|
||||
@@ -66,7 +66,7 @@ describe("MessageQueue", () => {
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(consumer).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolveFirstSend(true);
|
||||
resolveFirstSend();
|
||||
await Promise.resolve();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
@@ -76,12 +76,12 @@ describe("MessageQueue", () => {
|
||||
queue.dispose();
|
||||
});
|
||||
|
||||
it("requeues a failed batch and merges it with newer pending messages", async () => {
|
||||
it("does not retry a failed batch", async () => {
|
||||
const consumer = vi
|
||||
.fn((content: string) => content.length > 0)
|
||||
.mockReturnValueOnce(true)
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValue(true);
|
||||
.fn()
|
||||
.mockResolvedValueOnce(undefined)
|
||||
.mockRejectedValueOnce(new Error("send failed"))
|
||||
.mockResolvedValue(undefined);
|
||||
const queue = new MessageQueue({ intervalMs: 1000 });
|
||||
queue.setConsumer(consumer);
|
||||
|
||||
@@ -96,13 +96,14 @@ describe("MessageQueue", () => {
|
||||
queue.enqueue("fourth");
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(consumer).toHaveBeenLastCalledWith("second\nthird\nfourth");
|
||||
expect(consumer).toHaveBeenCalledTimes(3);
|
||||
expect(consumer).toHaveBeenLastCalledWith("fourth");
|
||||
|
||||
queue.dispose();
|
||||
});
|
||||
|
||||
it("supports a custom joiner", async () => {
|
||||
const consumer = vi.fn(() => true);
|
||||
const consumer = vi.fn();
|
||||
const queue = new MessageQueue({ intervalMs: 1000, joiner: " " });
|
||||
queue.setConsumer(consumer);
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
* 业务行为:
|
||||
* - 队列空闲时,第一条消息立即发送
|
||||
* - 后续消息在冷却窗口内暂存,并按 `intervalMs` 合并成一条发送
|
||||
* - 消费者可声明每批消息是否成功消费;不成功则重新入队
|
||||
* - 队列只负责发送节奏,不关心每批消息是否成功消费
|
||||
* - 队列可在任意时刻清空(dispose)
|
||||
*/
|
||||
import { Logger } from "@/utils";
|
||||
|
||||
export type MessageConsumer = (content: string) => boolean | Promise<boolean>;
|
||||
export type MessageConsumer = (content: string) => void | Promise<void>;
|
||||
|
||||
export interface MessageQueueOptions {
|
||||
intervalMs?: number;
|
||||
@@ -38,7 +38,7 @@ export class MessageQueue {
|
||||
this.joiner = options.joiner ?? "\n";
|
||||
}
|
||||
|
||||
/** 注册消费者(消费者必须能处理失败/重试)。 */
|
||||
/** 注册消费者。队列只负责调用,不根据结果做重试。 */
|
||||
setConsumer(consumer: MessageConsumer): void {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
@@ -69,24 +69,13 @@ export class MessageQueue {
|
||||
return;
|
||||
}
|
||||
|
||||
let ok = false;
|
||||
try {
|
||||
ok = await this.consumer(content);
|
||||
await this.consumer(content);
|
||||
} catch (e) {
|
||||
log.error({ err: e }, "consumer error");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
this.sending = false;
|
||||
if (!ok) {
|
||||
// 消费失败,整批放回队首等待下次合并发送。
|
||||
this.pending.unshift(content);
|
||||
log.debug("retry batch", {
|
||||
pendingCount: this.pending.length,
|
||||
contentLength: content.length,
|
||||
});
|
||||
}
|
||||
|
||||
this.scheduleFlush();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user