import type { MessageReceiveEvent } from "./client.js"; export interface QueuedTrigger { readonly projectId: string; readonly chatId: string; readonly prompt: string; /** Role frozen when the message was accepted; later group switches cannot change it. */ readonly roleId: string; readonly executionKind: "conversation" | "native_compact"; readonly msg: MessageReceiveEvent["message"]; readonly senderOpenId: string; readonly actor: { readonly feishuOpenId: string; readonly chatId: string }; readonly enqueuedAt: number; } export interface TriggerQueueOptions { readonly maxQueueSize?: number; readonly maxWaitMs?: number; } const DEFAULT_MAX_QUEUE_SIZE = 5; const DEFAULT_MAX_WAIT_MS = 300_000; export class TriggerQueue { readonly maxQueueSize: number; readonly maxWaitMs: number; private readonly queues = new Map(); constructor(options: TriggerQueueOptions = {}) { this.maxQueueSize = Math.max(0, Math.trunc(options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE)); this.maxWaitMs = Math.max(0, Math.trunc(options.maxWaitMs ?? DEFAULT_MAX_WAIT_MS)); } enqueue(projectId: string, trigger: Omit): number { const queue = this.queues.get(projectId) ?? []; if (queue.length >= this.maxQueueSize) return 0; queue.push({ ...trigger, projectId, enqueuedAt: Date.now(), }); if (!this.queues.has(projectId)) { this.queues.set(projectId, queue); } return queue.length; } dequeue(projectId: string): QueuedTrigger | null { const queue = this.queues.get(projectId); if (queue === undefined || queue.length === 0) return null; const next = queue.shift() ?? null; if (queue.length === 0) { this.queues.delete(projectId); } return next; } peek(projectId: string): QueuedTrigger | null { return this.queues.get(projectId)?.[0] ?? null; } length(projectId: string): number { return this.queues.get(projectId)?.length ?? 0; } hasPending(projectId: string): boolean { return this.length(projectId) > 0; } purgeExpired(): number { const now = Date.now(); let removed = 0; for (const [projectId, queue] of this.queues) { const fresh = queue.filter((trigger) => !this.isExpired(trigger, now)); removed += queue.length - fresh.length; if (fresh.length === 0) { this.queues.delete(projectId); } else if (fresh.length !== queue.length) { this.queues.set(projectId, fresh); } } return removed; } clear(projectId: string): number { const count = this.length(projectId); this.queues.delete(projectId); return count; } clearAll(): void { this.queues.clear(); } isExpired(trigger: QueuedTrigger, now = Date.now()): boolean { return trigger.enqueuedAt + this.maxWaitMs < now; } } export const triggerQueue = new TriggerQueue();