UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

311 lines (310 loc) • 10.4 kB
import { M as resolveNonNegativeIntegerOption, N as resolveOptionalIntegerOption } from "./number-coercion-CuVhdCBs.mjs"; import { u as toErrorObject } from "./error-coercion-CvJK4lmH.mjs"; import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import "./errors-O33D01XW.mjs"; //#region src/auto-reply/inbound-debounce.ts /** Resolve effective inbound debounce milliseconds from explicit, channel, and global config. */ function resolveInboundDebounceMs(params) { const inbound = params.cfg.messages?.inbound; for (const value of [ params.overrideMs, inbound?.byChannel?.[params.channel], inbound?.debounceMs ]) { const resolved = resolveOptionalIntegerOption(value, { min: 0 }); if (resolved !== void 0) return resolved; } return 0; } /** * Start one flush and bind its admission signal to the turn lifecycle. * Completion also releases admission for gated work that never enters a session lane. */ function createInboundDebounceFlush(params) { let admitted = false; const { promise: admission, resolve: resolveAdmission } = createDeferredCore(); const markAdmitted = () => { if (admitted) return; admitted = true; resolveAdmission(); }; const source = params.lifecycle; const lifecycle = { abortSignal: source?.abortSignal ?? new AbortController().signal, onAdopted: async () => { await source?.onAdopted?.(); markAdmitted(); }, onDeferred: () => { const accepted = source?.onDeferred?.(); if (accepted !== false) markAdmitted(); return accepted; }, onDeferredHeartbeat: () => source?.onDeferredHeartbeat?.(), deferredHeartbeatIntervalMs: source?.deferredHeartbeatIntervalMs, onAdoptionFinalizing: () => source?.onAdoptionFinalizing?.(), onFailed: source?.onFailed ? async (error) => { try { await source.onFailed?.(error); } finally { markAdmitted(); } } : void 0, onAbandoned: async () => { await source?.onAbandoned?.(); } }; let completion; try { completion = params.dispatch(lifecycle); } catch (error) { completion = Promise.reject(toErrorObject(error, "Inbound debounce dispatch failed")); } completion = completion.then(markAdmitted).catch(async (error) => { if (!admitted && lifecycle.onFailed) await Promise.allSettled([lifecycle.onFailed(error)]); markAdmitted(); throw error; }); return { admission, completion }; } const DEFAULT_MAX_TRACKED_KEYS = 2048; const MAX_DEBOUNCE_WINDOW_MULTIPLIER = 5; /** Create a keyed debouncer with flush/cancel controls and same-key serialization. */ function createInboundDebouncer(params) { const buffers = /* @__PURE__ */ new Map(); const pendingBuffers = /* @__PURE__ */ new Map(); const keyChains = /* @__PURE__ */ new Map(); const keyGenerations = /* @__PURE__ */ new Map(); const activeCompletions = /* @__PURE__ */ new Set(); const defaultDebounceMs = resolveNonNegativeIntegerOption(params.debounceMs, 0); const maxTrackedKeys = Math.max(1, Math.trunc(params.maxTrackedKeys ?? DEFAULT_MAX_TRACKED_KEYS)); const resolveDebounceMs = (item, pending) => { const resolved = params.resolveDebounceMs?.(item, pending); return resolveNonNegativeIntegerOption(resolved, defaultDebounceMs); }; const resolvePending = (item, key) => { const buffer = buffers.get(key); return buffer && (params.canAppend?.(item, buffer.items) ?? true) ? buffer : void 0; }; const shouldBuffer = (item) => { const key = params.buildKey(item); return Boolean(key && resolveDebounceMs(item, resolvePending(item, key)?.items) > 0 && (params.shouldDebounce?.(item) ?? true)); }; const untrackBuffer = (key, buffer) => { const pending = pendingBuffers.get(key); pending?.delete(buffer); if (pending?.size === 0) pendingBuffers.delete(key); }; const reportFlushError = (err, items) => { try { params.onError?.(err, items); } catch {} }; const runFlush = async (items) => { let flush; try { flush = params.onFlush(items, createInboundDebounceFlush); } catch (err) { reportFlushError(err, items); return; } let reported = false; const reportOnce = (err) => { if (reported) return; reported = true; reportFlushError(err, items); }; const admission = flush.admission.catch(reportOnce); const completion = flush.completion.catch(reportOnce); activeCompletions.add(completion); const cleanup = () => activeCompletions.delete(completion); completion.then(cleanup, cleanup); await Promise.race([admission, completion]); }; const cancelItems = (items) => { try { params.onCancel?.(items); } catch {} }; const resolveKeyGeneration = (key) => keyGenerations.get(key) ?? 0; const runQueuedFlush = async (key, generation, items) => { if (resolveKeyGeneration(key) !== generation) { cancelItems(items); return; } await runFlush(items); }; const untrackKeyTask = (key, settled) => { if (keyChains.get(key) === settled) { keyChains.delete(key); if (!buffers.has(key)) keyGenerations.delete(key); } }; const enqueueKeyTask = (key, task) => { const next = (keyChains.get(key) ?? Promise.resolve()).catch(() => void 0).then(task); const settled = next.catch(() => void 0); keyChains.set(key, settled); const cleanup = () => untrackKeyTask(key, settled); settled.then(cleanup, cleanup); return next; }; const enqueueReservedKeyTask = (key, task) => { const { promise: ready, resolve: releaseReady } = createDeferredCore(); return { task: enqueueKeyTask(key, async () => { await ready; await task(); }), release: releaseReady }; }; const flushBuffer = async (key, buffer) => { if (buffers.get(key) === buffer) buffers.delete(key); if (buffer.timeout) { clearTimeout(buffer.timeout); buffer.timeout = null; } buffer.releaseReady(); await buffer.task; }; const flushKey = async (key) => { const buffer = buffers.get(key); if (!buffer) return; await flushBuffer(key, buffer); }; const cancelKey = (key) => { const pending = pendingBuffers.get(key); if (!pending?.size && !keyChains.has(key)) return false; keyGenerations.set(key, resolveKeyGeneration(key) + 1); for (const buffer of pending ?? []) { if (buffers.get(key) === buffer) buffers.delete(key); if (buffer.timeout) { clearTimeout(buffer.timeout); buffer.timeout = null; } const canceledItems = buffer.items; buffer.items = []; cancelItems(canceledItems); buffer.releaseReady(); } pendingBuffers.delete(key); return true; }; const settleQuietFlush = async (key, buffer, checkToken) => { if (buffers.get(key) !== buffer || buffer.flushCheckToken !== checkToken) return; if (!params.shouldHoldFlush || performance.now() >= buffer.flushDeadlineMs) { await flushBuffer(key, buffer); return; } buffer.timeout = setTimeout(() => { flushBuffer(key, buffer); }, Math.max(0, buffer.flushDeadlineMs - performance.now())); buffer.timeout.unref?.(); let shouldHold; try { shouldHold = await params.shouldHoldFlush(buffer.items); } catch { shouldHold = false; } if (buffers.get(key) !== buffer || buffer.flushCheckToken !== checkToken) return; if (shouldHold) scheduleFlush(key, buffer); else await flushBuffer(key, buffer); }; const scheduleFlush = (key, buffer) => { const checkToken = ++buffer.flushCheckToken; if (buffer.timeout) clearTimeout(buffer.timeout); const delayMs = Math.min(buffer.debounceMs, Math.max(0, buffer.flushDeadlineMs - performance.now())); buffer.timeout = setTimeout(() => { if (params.shouldHoldFlush) settleQuietFlush(key, buffer, checkToken); else flushBuffer(key, buffer); }, delayMs); buffer.timeout.unref?.(); }; const enqueue = async (item) => { const key = params.buildKey(item); const existing = key ? resolvePending(item, key) : void 0; const debounceMs = resolveDebounceMs(item, existing?.items); if (!(debounceMs > 0 && (params.shouldDebounce?.(item) ?? true)) || !key) { if (key) { if (buffers.has(key)) { const generation = resolveKeyGeneration(key); const reservedTask = enqueueReservedKeyTask(key, () => runQueuedFlush(key, generation, [item])); try { await flushKey(key); } finally { reservedTask.release(); } await reservedTask.task; return; } if (keyChains.has(key)) { const generation = resolveKeyGeneration(key); await enqueueKeyTask(key, () => runQueuedFlush(key, generation, [item])); return; } if (params.serializeImmediate) { const { promise: settled, resolve: resolveSettled } = createDeferredCore(); keyChains.set(key, settled); try { await runFlush([item]); } finally { resolveSettled(); untrackKeyTask(key, settled); } return; } } await runFlush([item]); return; } if (existing) { existing.items.push(item); existing.debounceMs = debounceMs; scheduleFlush(key, existing); return; } if (buffers.has(key)) flushKey(key); if (!(keyChains.has(key) || keyChains.size < maxTrackedKeys)) { const generation = resolveKeyGeneration(key); await enqueueKeyTask(key, () => runQueuedFlush(key, generation, [item])); return; } const generation = resolveKeyGeneration(key); const reservedTask = enqueueReservedKeyTask(key, async () => { untrackBuffer(key, buffer); if (buffer.items.length === 0) return; const items = buffer.items; if (resolveKeyGeneration(key) !== generation) buffer.items = []; await runQueuedFlush(key, generation, items); }); const buffer = { items: [item], timeout: null, debounceMs, flushCheckToken: 0, flushDeadlineMs: performance.now() + Math.max(debounceMs, resolveNonNegativeIntegerOption(typeof params.maxWaitMs === "function" ? params.maxWaitMs(item) : params.maxWaitMs, debounceMs * MAX_DEBOUNCE_WINDOW_MULTIPLIER)), releaseReady: reservedTask.release, task: reservedTask.task }; buffers.set(key, buffer); const pending = pendingBuffers.get(key) ?? /* @__PURE__ */ new Set(); pending.add(buffer); pendingBuffers.set(key, pending); scheduleFlush(key, buffer); }; const drain = async () => { while (keyChains.size > 0 || activeCompletions.size > 0) await Promise.all([...keyChains.values(), ...activeCompletions]); }; return { enqueue, shouldBuffer, flushKey, cancelKey, drain }; } //#endregion export { resolveInboundDebounceMs as n, createInboundDebouncer as t };