openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
167 lines (166 loc) • 4.84 kB
JavaScript
import { t as KeyedAsyncQueue } from "./keyed-async-queue-Ckmdd15z.js";
//#region src/channels/run-state-machine.ts
const DEFAULT_RUN_ACTIVITY_HEARTBEAT_MS = 6e4;
/** Creates a channel run-state tracker with heartbeat updates while runs are active. */
function createRunStateMachine(params) {
const heartbeatMs = params.heartbeatMs ?? DEFAULT_RUN_ACTIVITY_HEARTBEAT_MS;
const now = params.now ?? Date.now;
let activeRuns = 0;
let runActivityHeartbeat = null;
let lifecycleActive = !params.abortSignal?.aborted;
const publish = () => {
if (!lifecycleActive) return;
params.setStatus?.({
activeRuns,
busy: activeRuns > 0,
lastRunActivityAt: now()
});
};
const clearHeartbeat = () => {
if (!runActivityHeartbeat) return;
clearInterval(runActivityHeartbeat);
runActivityHeartbeat = null;
};
const ensureHeartbeat = () => {
if (runActivityHeartbeat || activeRuns <= 0 || !lifecycleActive) return;
runActivityHeartbeat = setInterval(() => {
if (!lifecycleActive || activeRuns <= 0) {
clearHeartbeat();
return;
}
publish();
}, heartbeatMs);
runActivityHeartbeat.unref?.();
};
const deactivate = () => {
lifecycleActive = false;
clearHeartbeat();
};
const onAbort = () => {
deactivate();
};
if (params.abortSignal?.aborted) onAbort();
else params.abortSignal?.addEventListener("abort", onAbort, { once: true });
if (lifecycleActive) params.setStatus?.({
activeRuns: 0,
busy: false
});
return {
isActive() {
return lifecycleActive;
},
onRunStart() {
activeRuns += 1;
publish();
ensureHeartbeat();
},
onRunEnd() {
activeRuns = Math.max(0, activeRuns - 1);
if (activeRuns <= 0) clearHeartbeat();
publish();
},
deactivate
};
}
//#endregion
//#region src/plugin-sdk/channel-lifecycle.core.ts
/** Bind a fixed account id into a status writer so lifecycle code can emit partial snapshots. */
function createAccountStatusSink(params) {
return (patch) => {
params.setStatus({
accountId: params.accountId,
...patch
});
};
}
/**
* Serialize channel work per key while keeping lifecycle/busy accounting out of
* channel-specific message handlers. The queue does not impose run timeouts;
* callers should rely on session/tool/runtime lifecycle for long-running work.
*/
function createChannelRunQueue(params) {
const queue = new KeyedAsyncQueue();
const runState = createRunStateMachine({
setStatus: params.setStatus,
abortSignal: params.abortSignal
});
const reportError = (error) => {
try {
params.onError?.(error);
} catch {}
};
return {
enqueue(key, task) {
queue.enqueue(key, async () => {
if (!runState.isActive()) return;
runState.onRunStart();
try {
if (!runState.isActive()) return;
await task({ lifecycleSignal: params.abortSignal });
} finally {
runState.onRunEnd();
}
}).catch(reportError);
},
deactivate: runState.deactivate
};
}
/**
* Return a promise that resolves when the signal is aborted.
*
* If no signal is provided, the promise stays pending forever. When provided,
* `onAbort` runs once before the promise resolves.
*/
function waitUntilAbort(signal, onAbort) {
return new Promise((resolve, reject) => {
const complete = () => {
Promise.resolve(onAbort?.()).then(() => resolve(), reject);
};
if (!signal) return;
if (signal.aborted) {
complete();
return;
}
signal.addEventListener("abort", complete, { once: true });
});
}
/**
* Keep a passive account task alive until abort, then run optional cleanup.
*/
async function runPassiveAccountLifecycle(params) {
const handle = await params.start();
try {
await waitUntilAbort(params.abortSignal);
} finally {
await params.stop?.(handle);
await params.onStop?.();
}
}
/**
* Keep a channel/provider task pending until the HTTP server closes.
*
* When an abort signal is provided, `onAbort` is invoked once and should
* trigger server shutdown. The returned promise resolves only after `close`.
*/
async function keepHttpServerTaskAlive(params) {
const { server, abortSignal, onAbort } = params;
let abortTask = Promise.resolve();
let abortTriggered = false;
const triggerAbort = () => {
if (abortTriggered) return;
abortTriggered = true;
abortTask = Promise.resolve(onAbort?.()).then(() => void 0);
};
const onAbortSignal = () => {
triggerAbort();
};
if (abortSignal) if (abortSignal.aborted) triggerAbort();
else abortSignal.addEventListener("abort", onAbortSignal, { once: true });
await new Promise((resolve) => {
server.once("close", () => resolve());
});
if (abortSignal) abortSignal.removeEventListener("abort", onAbortSignal);
await abortTask;
}
//#endregion
export { waitUntilAbort as a, runPassiveAccountLifecycle as i, createChannelRunQueue as n, createRunStateMachine as o, keepHttpServerTaskAlive as r, createAccountStatusSink as t };