UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

485 lines (484 loc) 16.2 kB
import { l as toErrorObject } from "./error-coercion-D_-xJ90S.js"; import { t as sleep } from "./sleep-D7nua6TP.js"; import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js"; import { k as waitForGatewayRestartFenceSettlement, s as getGatewayRestartDrainSignal, u as isGatewayRestartDraining } from "./gateway-work-admission-R1IpuDim.js"; import { n as createChannelIngressDrain } from "./ingress-drain-DcmwOHP5.js"; import "./ingress-retry-policy-BJDBjz6b.js"; import { t as ChannelIngressUnavailableError } from "./ingress-unavailable-CiovoHDb.js"; //#region src/channels/message/ingress-monitor.ts /** Shared durable channel-ingress admission, pump, retention, and shutdown lifecycle. */ const DEFAULT_APPEND_RETRY_DELAYS_MS = [ 0, 100, 300 ]; /** Replay-guard retention defaults; changing a value requires a per-channel keyspace audit. */ const CHANNEL_INGRESS_RETENTION_DEFAULTS = Object.freeze({ pruneIntervalMs: 36e5, completedTtlMs: 2592e6, completedMaxEntries: 2e4, failedTtlMs: 2592e6, failedMaxEntries: 2e4 }); /** * Creates the shared monitor around a durable queue and ingress drain. * Channel code keeps transport inspection, payload shape, and delivery policy. */ function createChannelIngressMonitor(options) { const now = options.now ?? Date.now; const waitForDeliveryIdleBeforeRepump = options.waitForDeliveryIdleBeforeRepump ?? false; const { pruneIntervalMs, ...pruneOptions } = options.retention === "standard" ? CHANNEL_INGRESS_RETENTION_DEFAULTS : { ...CHANNEL_INGRESS_RETENTION_DEFAULTS, ...options.retention }; const shutdown = new AbortController(); const drainAbortSignal = options.abortSignal ? AbortSignal.any([shutdown.signal, options.abortSignal]) : shutdown.signal; const activeDeliveries = /* @__PURE__ */ new Set(); const deferredClaims = /* @__PURE__ */ new Set(); const queueFactory = typeof options.queue === "function" ? options.queue : () => options.queue; let queue = typeof options.queue === "function" ? void 0 : options.queue; let drain; let running = false; let stopped = false; let requested = false; let pumping; let drainIdleWake; let drainIdleWakeRequested = false; let restartFenceWake; let releaseRestartFenceWake = () => {}; let pollTimer; let lastPrunedAt = 0; let admissionTail = Promise.resolve(); let admissionClaimLocked = false; const admissionClaimWaiters = []; let stopTask; let lastReportedActive = false; const reportError = (error) => { try { options.onError?.(error); } catch {} }; const publishActivity = () => { const active = activeDeliveries.size > 0 || running && (requested || pumping !== void 0); if (active === lastReportedActive) return; lastReportedActive = active; try { options.onActivityChange?.(active); } catch (error) { reportError(error); } }; const withAdmissionClaimLock = (task) => { const run = () => { admissionClaimLocked = true; let result; try { result = Promise.resolve(task()); } catch (error) { result = Promise.reject(toErrorObject(error, "Channel ingress admission task failed")); } return result.finally(() => { const next = admissionClaimWaiters.shift(); if (next) next(); else admissionClaimLocked = false; }); }; if (!admissionClaimLocked) return run(); return new Promise((resolve, reject) => { admissionClaimWaiters.push(() => { run().then(resolve, reject); }); }); }; const createStoppedError = () => options.createStoppedError?.() ?? /* @__PURE__ */ new Error("Channel ingress monitor is stopped."); const getQueue = () => queue ??= queueFactory(); const ensureQueueAvailable = () => { try { getQueue(); } catch (error) { throw new ChannelIngressUnavailableError(`Channel ingress queue is unavailable: ${formatErrorMessage(error)}`, { cause: error }); } }; const isAborted = () => drainAbortSignal.aborted; const waitForPending = async (read, reject = false) => { for (;;) { const pending = [...read()]; if (pending.length === 0) return; await (reject ? Promise.all(pending) : Promise.allSettled(pending)); } }; const waitForActiveDeliveries = () => waitForPending(() => activeDeliveries); const waitForPumpIdle = () => waitForPending(() => pumping ? [pumping] : [], true); const waitForDeferredClaims = () => waitForPending(() => deferredClaims); const getDrain = () => { drain ??= createChannelIngressDrain({ ...options.drain, queue: getQueue(), abortSignal: drainAbortSignal, now, retryPolicy: options.drain?.retryPolicy ?? { maxAttempts: 8, deadLetterMinAgeMs: 864e5 }, formatError: options.drain?.formatError ?? formatErrorMessage, dispatchClaimedEvent: async (claim, lifecycle) => { if (!running || isAborted() || lifecycle.abortSignal.aborted) return { kind: "failed-retryable", error: createStoppedError() }; let decoded; if (options.payload.storage === "raw-event") { const stored = claim.payload; if (!stored || typeof stored.rawEvent !== "string") throw options.payload.createClaimError("invalid-version", claim); decoded = { version: stored.version, body: stored.rawEvent }; } else decoded = options.payload.decode(claim.payload, { claim }); if (decoded.version !== options.payload.version) throw options.payload.createClaimError("invalid-version", claim); const raw = options.payload.deserialize(decoded.body, { claim }); const claimedLaneKey = claim.laneKey ?? options.drain?.deriveLaneKey?.(claim); const facts = options.inspect(raw, { phase: "claim", claimedId: claim.id, claimedLaneKey }); if (!facts || facts.eventId !== claim.id || facts.laneKey !== claimedLaneKey) throw options.payload.createClaimError("identity-mismatch", claim); let handedOff = false; let deferredHandoff = false; let resolveDeferredClaim = () => {}; const deferredClaim = options.deferredClaims ? new Promise((resolve) => { resolveDeferredClaim = resolve; }) : void 0; let deferredClaimSettled = false; const settleDeferredClaim = () => { if (!deferredClaim || deferredClaimSettled) return; deferredClaimSettled = true; lifecycle.abortSignal.removeEventListener("abort", settleDeferredClaim); deferredClaims.delete(deferredClaim); resolveDeferredClaim(); }; if (options.deferredClaims === "settle-on-abort") { lifecycle.abortSignal.addEventListener("abort", settleDeferredClaim, { once: true }); if (lifecycle.abortSignal.aborted) settleDeferredClaim(); } const settleDeferredLifecycle = async (settle) => { handedOff = true; deferredHandoff = true; try { await settle(); requestDrain(); } finally { settleDeferredClaim(); } }; const wrappedLifecycle = { ...lifecycle, admission: "exclusive", onAdopted: async () => { handedOff = true; try { await lifecycle.onAdopted(); requestDrain(); } finally { settleDeferredClaim(); } }, onDeferred: () => { handedOff = true; deferredHandoff = true; if (deferredClaim && !deferredClaimSettled) deferredClaims.add(deferredClaim); lifecycle.onDeferred(); }, onAdoptionFinalizing: () => { handedOff = true; deferredHandoff = true; lifecycle.onAdoptionFinalizing(); }, onFailed: (error) => settleDeferredLifecycle(() => lifecycle.onFailed?.(error)), onCancelled: () => settleDeferredLifecycle(() => lifecycle.onCancelled?.()), onAbandoned: () => settleDeferredLifecycle(() => lifecycle.onAbandoned()) }; const delivery = Promise.resolve().then(() => options.deliver(raw, wrappedLifecycle, claim)); activeDeliveries.add(delivery); publishActivity(); let result; try { result = await delivery; } catch (error) { if (isAborted() || lifecycle.abortSignal.aborted) return { kind: "failed-retryable", error }; throw error; } finally { activeDeliveries.delete(delivery); publishActivity(); } if (result?.kind === "failed-retryable") return result; if (result?.kind === "completed") { if (deferredHandoff) return { kind: "deferred" }; return result; } if (result?.kind === "deferred") { if (!deferredHandoff) wrappedLifecycle.onDeferred(); return { kind: "deferred" }; } if (deferredHandoff) return { kind: "deferred" }; if (isAborted() || lifecycle.abortSignal.aborted) return { kind: "failed-retryable", error: createStoppedError() }; if (!handedOff) await wrappedLifecycle.onAdopted(); return { kind: "completed" }; } }); return drain; }; const pruneIfDue = async (owner) => { if (owner === "admission" !== pruneIntervalMs <= 0) return; const currentTime = now(); if (owner === "pump" && currentTime - lastPrunedAt < pruneIntervalMs) return; await getQueue().prune({ ...pruneOptions, now: currentTime }); lastPrunedAt = currentTime; }; const scheduleDrainIdleWake = (activeDrain) => { if (drainIdleWake) { drainIdleWakeRequested = true; return; } drainIdleWakeRequested = false; const wake = activeDrain.waitForIdle(); drainIdleWake = wake; wake.then(() => { if (drainIdleWake !== wake) return; const shouldRearm = drainIdleWakeRequested && running && !isAborted(); drainIdleWake = void 0; drainIdleWakeRequested = false; if (shouldRearm) scheduleDrainIdleWake(activeDrain); requestDrain(); }, (error) => { if (drainIdleWake === wake) { drainIdleWake = void 0; drainIdleWakeRequested = false; } reportError(error); }); }; const runPump = async () => { try { for (;;) { requested = false; await pruneIfDue("pump"); if (!running || isAborted()) break; const activeDrain = getDrain(); const { started } = await withAdmissionClaimLock(() => activeDrain.drainOnce({ shouldStop: () => !running || isAborted() || options.drain?.startLimit !== void 0 && activeDeliveries.size >= options.drain.startLimit })); if (waitForDeliveryIdleBeforeRepump) { await waitForActiveDeliveries(); await activeDrain.waitForIdle(); } else if (started > 0) scheduleDrainIdleWake(activeDrain); if (!running || isAborted() || !requested && (!waitForDeliveryIdleBeforeRepump || started === 0)) break; } } catch (error) { reportError(error); } finally { pumping = void 0; if (!running || isAborted()) requested = false; else if (requested) requestDrain(); publishActivity(); } }; const scheduleRestartFenceWake = () => { if (restartFenceWake) return; const localWake = new Promise((resolve) => { releaseRestartFenceWake = resolve; }); restartFenceWake = Promise.race([waitForGatewayRestartFenceSettlement(), localWake]).then(() => { restartFenceWake = void 0; releaseRestartFenceWake = () => {}; requestDrain(); }); }; drainAbortSignal.addEventListener("abort", () => releaseRestartFenceWake(), { once: true }); const requestDrain = () => { if (!running || isAborted() || getGatewayRestartDrainSignal().aborted) { requested = false; releaseRestartFenceWake(); publishActivity(); return; } if (isGatewayRestartDraining()) { requested = true; scheduleRestartFenceWake(); publishActivity(); return; } requested = true; if (pumping) { publishActivity(); return; } pumping = options.runPumpTask ? options.runPumpTask(runPump) : runPump(); publishActivity(); }; const clearPollTimer = () => { clearInterval(pollTimer); pollTimer = void 0; }; const pause = async () => { running = false; requested = false; releaseRestartFenceWake(); clearPollTimer(); publishActivity(); await waitForPumpIdle(); }; const admitOnce = async (params) => { let lastError; for (const delayMs of options.appendRetryDelaysMs ?? DEFAULT_APPEND_RETRY_DELAYS_MS) { if (delayMs > 0) await sleep(delayMs); try { return await getQueue().enqueue(params.facts.eventId, params.payload, { receivedAt: params.receivedAt, laneKey: params.facts.laneKey }); } catch (error) { lastError = error; } } if (lastError instanceof Error) throw lastError; throw new Error(lastError === void 0 ? "Channel ingress append failed without an error." : formatErrorMessage(lastError), { cause: lastError }); }; const assertAdmissionOpen = () => { if (stopped && options.admissionMode !== "durable-after-stop" || options.admissionMode === "while-running" && !running || options.abortSignal?.aborted && options.admissionMode !== "durable-after-stop") throw createStoppedError(); }; const admitRaw = async (raw, admitOptions) => { try { const facts = admitOptions.facts ?? options.inspect(raw, { phase: "admission" }); if (!facts) return { kind: "ignored" }; admitOptions.pruneTask ??= pruneIfDue("admission"); await admitOptions.pruneTask; const { receivedAt } = admitOptions; const body = options.payload.serialize(raw, { facts, receivedAt }); const payload = options.payload.storage === "raw-event" ? { version: options.payload.version, rawEvent: body } : options.payload.encode({ version: options.payload.version, body }); const queueResult = await admitOnce({ facts, payload, receivedAt }); admitOptions.onDurablyAdmitted(); await options.onDurableAdmission?.(raw, { facts, receivedAt, isNew: queueResult.kind === "accepted" }); return { kind: "durable", queueResult }; } catch (error) { await options.onAdmissionFailure?.(raw, error); throw error; } }; const scheduleAdmission = (work) => { const admission = admissionTail.then(() => withAdmissionClaimLock(work)); admissionTail = admission.then(() => void 0, () => void 0); return admission; }; return { admit: async (raw, admitOptions) => { assertAdmissionOpen(); const receivedAt = admitOptions?.receivedAt ?? now(); let durablyAdmitted = false; try { return await scheduleAdmission(() => admitRaw(raw, { receivedAt, ...admitOptions?.facts ? { facts: admitOptions.facts } : {}, onDurablyAdmitted: () => { durablyAdmitted = true; } })); } finally { if (durablyAdmitted) requestDrain(); } }, admitBatch: async (rawEvents, admitOptions) => { assertAdmissionOpen(); const receivedAt = admitOptions?.receivedAt ?? now(); let durablyAdmitted = false; const sharedOptions = { receivedAt, onDurablyAdmitted: () => { durablyAdmitted = true; } }; try { return await scheduleAdmission(async () => { const results = []; for (const raw of rawEvents) results.push(await admitRaw(raw, sharedOptions)); return results; }); } finally { if (durablyAdmitted) requestDrain(); } }, start: () => { if (running || stopped || isAborted()) return; ensureQueueAvailable(); running = true; pollTimer = setInterval(requestDrain, options.pollIntervalMs); pollTimer.unref?.(); requestDrain(); }, ensureQueueAvailable, requestDrain, pause, stop: () => { stopTask ??= (async () => { stopped = true; running = false; requested = false; releaseRestartFenceWake(); clearPollTimer(); publishActivity(); await admissionTail; shutdown.abort(createStoppedError()); await waitForPumpIdle(); if (options.waitForDeliveryIdleOnStop !== false) await waitForActiveDeliveries(); drain?.dispose(); if (options.waitForDeliveryIdleOnStop !== false) await drain?.waitForIdle(); if (options.deferredClaims && options.deferredClaims !== "manual") await waitForDeferredClaims(); })(); return stopTask; }, waitForIdle: async () => { for (;;) { await admissionTail; await waitForPumpIdle(); await waitForActiveDeliveries(); await drain?.waitForIdle(); await restartFenceWake; if (!pumping && activeDeliveries.size === 0 && !requested) return; } }, waitForDeferredClaims, waitForPumpIdle, isRunning: () => running, isStopped: () => stopped }; } //#endregion export { createChannelIngressMonitor as n, CHANNEL_INGRESS_RETENTION_DEFAULTS as t };