

























@@ -1,9 +1,50 @@
1-export function createSequentialQueue() {
1+/**
2+ * Per-key serial task queue for Feishu inbound message handling.
3+ *
4+ * Tasks enqueued under the same key run in FIFO order. Different keys run
5+ * concurrently. This preserves the channel's same-chat ordering contract
6+ * (see #64324) while letting cross-chat work proceed in parallel.
7+ *
8+ * `taskTimeoutMs` bounds how long the queue will block subsequent same-key
9+ * tasks behind a single in-flight task. After the cap, the in-flight task
10+ * is evicted from the blocking chain so newer messages for the same key
11+ * can proceed. The original task is NOT aborted — it continues running in
12+ * the background; it just stops starving the queue.
13+ *
14+ * Without this cap, a single hung dispatch (e.g. an agent call that never
15+ * resolves) keeps later same-chat messages in `queued` state until the
16+ * gateway is restarted. See #70133.
17+ */
18+19+const DEFAULT_TASK_TIMEOUT_MS = 5 * 60 * 1000;
20+21+export interface SequentialQueueOptions {
22+/**
23+ * Maximum time (ms) to block subsequent same-key tasks behind a single
24+ * in-flight task. Pass 0 (or a non-finite value) to disable the cap and
25+ * restore unbounded legacy behavior.
26+ *
27+ * Default: 5 minutes.
28+ */
29+taskTimeoutMs?: number;
30+31+/**
32+ * Optional callback fired when a task exceeds `taskTimeoutMs`. The task
33+ * itself is not awaited further; this callback is the only signal the
34+ * caller gets that the queue moved on without it.
35+ */
36+onTaskTimeout?: (key: string, timeoutMs: number) => void;
37+}
38+39+export function createSequentialQueue(options: SequentialQueueOptions = {}) {
240const queues = new Map<string, Promise<void>>();
41+const taskTimeoutMs = options.taskTimeoutMs ?? DEFAULT_TASK_TIMEOUT_MS;
42+const onTaskTimeout = options.onTaskTimeout;
343444return (key: string, task: () => Promise<void>): Promise<void> => {
545const previous = queues.get(key) ?? Promise.resolve();
6-const next = previous.then(task, task);
46+const wrapped = () => boundedRun(key, task, taskTimeoutMs, onTaskTimeout);
47+const next = previous.then(wrapped, wrapped);
748queues.set(key, next);
849const cleanup = () => {
950if (queues.get(key) === next) {
@@ -14,3 +55,30 @@ export function createSequentialQueue() {
1455return next;
1556};
1657}
58+59+async function boundedRun(
60+key: string,
61+task: () => Promise<void>,
62+timeoutMs: number,
63+onTaskTimeout: ((key: string, timeoutMs: number) => void) | undefined,
64+): Promise<void> {
65+if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
66+return task();
67+}
68+let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
69+const timeoutPromise = new Promise<void>((resolve) => {
70+timeoutHandle = setTimeout(() => {
71+try {
72+onTaskTimeout?.(key, timeoutMs);
73+} catch {
74+// Swallow logging errors so they cannot poison the queue chain.
75+}
76+resolve();
77+}, timeoutMs);
78+});
79+try {
80+await Promise.race([task(), timeoutPromise]);
81+} finally {
82+if (timeoutHandle) clearTimeout(timeoutHandle);
83+}
84+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。