forked from EduCraft/curriculum-project-hub
104 lines
2.9 KiB
TypeScript
104 lines
2.9 KiB
TypeScript
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<string, QueuedTrigger[]>();
|
|
|
|
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<QueuedTrigger, "projectId" | "enqueuedAt">): 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();
|