openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
260 lines (259 loc) • 8 kB
JavaScript
import { F as resolveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { t as formatErrorMessage } from "./errors-Db3Ymjlb.js";
//#region src/channels/draft-stream-loop.ts
/**
* Throttled draft stream loop.
*
* Sends the latest pending draft text with single-flight edit semantics.
*/
/** Creates a single-flight draft stream loop that preserves the newest pending value. */
function createDraftStreamLoop(params) {
const throttleMs = resolveTimerTimeoutMs(params.throttleMs, 0, 0);
const emptyValue = params.emptyValue ?? "";
const isEmpty = params.isEmpty ?? ((value) => typeof value === "string" && value.trim().length === 0);
const hasPendingValue = (value) => typeof value === "string" ? value.length > 0 : !isEmpty(value);
let lastSentAt = 0;
let pendingValue = emptyValue;
let inFlightPromise;
let timer;
const flush = async (background = false) => {
if (timer) {
clearTimeout(timer);
timer = void 0;
}
while (!params.isStopped()) {
if (inFlightPromise) {
await inFlightPromise;
if (background && params.coalesceInFlight) return;
continue;
}
const value = pendingValue;
if (isEmpty(value)) {
pendingValue = emptyValue;
return;
}
pendingValue = emptyValue;
let current;
let sent;
try {
current = Promise.resolve(params.sendOrEditStreamMessage(value)).finally(() => {
if (inFlightPromise === current) inFlightPromise = void 0;
});
inFlightPromise = current;
sent = await current;
} catch (err) {
if (!hasPendingValue(pendingValue)) pendingValue = value;
else if (background && params.coalesceInFlight) schedule();
throw err;
}
if (sent === false) {
if (!hasPendingValue(pendingValue)) pendingValue = value;
else if (background && params.coalesceInFlight) schedule();
return;
}
lastSentAt = Date.now();
if (!hasPendingValue(pendingValue)) return;
if (background && params.coalesceInFlight) {
schedule();
return;
}
}
};
const startBackgroundFlush = () => {
flush(true).catch((err) => {
try {
params.onBackgroundFlushError?.(err);
} catch {}
});
};
const schedule = () => {
if (timer) return;
const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt));
timer = setTimeout(() => {
startBackgroundFlush();
}, delay);
};
return {
update: (value) => {
if (params.isStopped()) return;
pendingValue = value;
if (inFlightPromise) {
if (!params.coalesceInFlight) schedule();
return;
}
if (!timer && Date.now() - lastSentAt >= throttleMs) {
startBackgroundFlush();
return;
}
schedule();
},
flush: () => flush(),
stop: () => {
pendingValue = emptyValue;
if (timer) {
clearTimeout(timer);
timer = void 0;
}
},
resetPending: () => {
pendingValue = emptyValue;
},
resetThrottleWindow: () => {
lastSentAt = 0;
if (timer) {
clearTimeout(timer);
timer = void 0;
}
},
waitForInFlight: async () => {
if (inFlightPromise) await inFlightPromise;
},
takePending: () => {
const value = pendingValue;
pendingValue = emptyValue;
if (timer) {
clearTimeout(timer);
timer = void 0;
}
return value;
}
};
}
//#endregion
//#region src/channels/draft-stream-controls.ts
/**
* Finalizable draft stream controls.
*
* Coordinates preview updates, final flushes, clears, and deletion callbacks for channel drafts.
*/
/**
* Creates controls for streaming preview messages that can be finalized, sealed, or cleared.
*/
function createFinalizableDraftStreamControls(params) {
const loop = createDraftStreamLoop({
throttleMs: params.throttleMs,
coalesceInFlight: params.coalesceInFlight,
isStopped: params.isStopped,
sendOrEditStreamMessage: params.sendOrEditStreamMessage,
...params.emptyValue !== void 0 ? { emptyValue: params.emptyValue } : {},
...params.isEmpty ? { isEmpty: params.isEmpty } : {}
});
const update = (value) => {
if (params.isStopped() || params.isFinal()) return;
loop.update(value);
};
const stop = async () => {
params.markFinal();
await loop.flush();
};
const stopForClear = async () => {
params.markStopped();
loop.stop();
await loop.waitForInFlight();
};
const seal = async () => {
params.markFinal();
loop.stop();
await loop.waitForInFlight();
};
return {
loop,
update,
stop,
seal,
discardPending: stopForClear,
stopForClear
};
}
/**
* Creates finalizable draft controls backed by a shared mutable state object.
*/
function createFinalizableDraftStreamControlsForState(params) {
return createFinalizableDraftStreamControls({
throttleMs: params.throttleMs,
coalesceInFlight: params.coalesceInFlight,
isStopped: () => params.state.stopped,
isFinal: () => params.state.final,
markStopped: () => {
params.state.stopped = true;
},
markFinal: () => {
params.state.final = true;
},
sendOrEditStreamMessage: params.sendOrEditStreamMessage,
...params.emptyValue !== void 0 ? { emptyValue: params.emptyValue } : {},
...params.isEmpty ? { isEmpty: params.isEmpty } : {}
});
}
/**
* Stops a draft stream, reads the current preview message id, then clears the stored id.
*/
async function takeMessageIdAfterStop(params) {
await params.stopForClear();
const messageId = params.readMessageId();
params.clearMessageId();
return messageId;
}
async function deleteFinalizableDraftMessage(params, messageId) {
try {
await params.deleteMessage(messageId);
} catch (err) {
params.warn?.(`${params.warnPrefix}: ${formatErrorMessage(err)}`);
return false;
}
try {
if (Object.is(params.readMessageId(), messageId)) params.clearMessageId();
params.onDeleteSuccess?.(messageId);
} catch (err) {
params.warn?.(`${params.warnPrefix} after delete: ${formatErrorMessage(err)}`);
}
return true;
}
/**
* Stops a draft stream and deletes its preview message when the stored id is valid.
* Claims the current id before deletion; stateful callers can retain failures through
* onDeleteFailure without making overlapping clears delete the same message twice.
*/
async function clearFinalizableDraftMessage(params) {
await params.stopForClear();
const messageId = params.readMessageId();
params.clearMessageId();
if (!params.isValidMessageId(messageId)) return;
if (!await deleteFinalizableDraftMessage(params, messageId)) params.onDeleteFailure?.(messageId);
}
/**
* Builds the standard draft lifecycle used by channel streaming preview implementations.
*/
function createFinalizableDraftLifecycle(params) {
const controls = createFinalizableDraftStreamControlsForState({
throttleMs: params.throttleMs,
state: params.state,
sendOrEditStreamMessage: params.sendOrEditStreamMessage,
...params.emptyValue !== void 0 ? { emptyValue: params.emptyValue } : {},
...params.isEmpty ? { isEmpty: params.isEmpty } : {}
});
let pendingDeleteIds = [];
let clearTail = Promise.resolve();
const clearOnce = async (stopForClear) => {
await stopForClear();
const currentMessageId = params.readMessageId();
const deleteIds = pendingDeleteIds;
pendingDeleteIds = [];
if (!params.isValidMessageId(currentMessageId)) params.clearMessageId();
else if (!deleteIds.some((messageId) => Object.is(messageId, currentMessageId))) deleteIds.push(currentMessageId);
for (const messageId of deleteIds) if (!await deleteFinalizableDraftMessage(params, messageId) && !pendingDeleteIds.some((pendingId) => Object.is(pendingId, messageId))) pendingDeleteIds.push(messageId);
};
const clearWithStop = (stopForClear) => {
const clearRun = clearTail.catch(() => {}).then(() => clearOnce(stopForClear));
clearTail = clearRun;
return clearRun;
};
const clear = () => clearWithStop(controls.stopForClear);
return {
...controls,
clear,
clearWithStop
};
}
//#endregion
export { takeMessageIdAfterStop as a, createFinalizableDraftStreamControlsForState as i, createFinalizableDraftLifecycle as n, createDraftStreamLoop as o, createFinalizableDraftStreamControls as r, clearFinalizableDraftMessage as t };