import { IngressJobData } from '../queue/processors/ingress.processor'; // Single-node, in-process keyed lock: a chain of promises per key. The ordering key is a plain // string (no closures) so a distributed lock (e.g. Redis redlock / SET NX PX) can replace this // verbatim once ingress runs on more than one node. export class KeyedAsyncLock { private readonly tails = new Map>(); run(key: string, fn: () => Promise): Promise { const prev = this.tails.get(key) ?? Promise.resolve(); // Chain fn after prev; swallow prev's rejection so one failure doesn't poison the chain. const next = prev.catch(() => undefined).then(fn); this.tails.set(key, next); // Clean up the map entry once this is the tail and it settles, to avoid unbounded growth. void next .catch(() => undefined) .finally(() => { if (this.tails.get(key) === next) this.tails.delete(key); }); return next; } } // Key on the CONVERSATION, never the deliveryId (a delivery-unique key would serialize nothing). // providerConversationId is populated host-side when the plugin manifest declares a conversationId // pointer; absent it, we serialize per instance — correct (never reorders within a conversation) but // coarse. Per-instance fallback is the lazy-correct default; the conversation key unlocks // intra-instance parallelism once the manifest declares the pointer. export function orderingKeyFor(job: IngressJobData): string { if (job.providerConversationId) return `${job.instanceId}:${job.providerConversationId}`; return `instance:${job.instanceId}`; }