openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
294 lines (293 loc) • 8.89 kB
JavaScript
import { F as resolveTimerTimeoutMs } from "./number-coercion-Dq1l5muL.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
//#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 clearTimer = () => {
if (timer) {
clearTimeout(timer);
timer = void 0;
}
};
const retainUnsentValue = (value, background) => {
if (!hasPendingValue(pendingValue)) pendingValue = value;
else if (background && params.coalesceInFlight) schedule();
};
const flush = async (background = false) => {
clearTimer();
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) {
retainUnsentValue(value, background);
throw err;
}
if (sent === false) {
retainUnsentValue(value, background);
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;
clearTimer();
},
resetPending: () => {
pendingValue = emptyValue;
},
resetThrottleWindow: () => {
lastSentAt = 0;
clearTimer();
},
waitForInFlight: async () => {
if (inFlightPromise) await inFlightPromise;
},
takePending: () => {
const value = pendingValue;
pendingValue = emptyValue;
clearTimer();
return value;
}
};
}
//#endregion
//#region src/channels/draft-stream-controls.ts
/**
* 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 {
if (await params.deleteMessage(messageId) === false) return false;
} 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) {
const messageId = await takeMessageIdAfterStop(params);
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(params);
const pending = /* @__PURE__ */ new Map();
let clearTail = Promise.resolve();
const claim = (messageId, owner) => {
let retirement = pending.get(messageId);
if (!retirement) {
retirement = { owner };
pending.set(messageId, retirement);
}
return retirement;
};
const attemptDelete = (messageId, retirement) => {
retirement.attempt ??= Promise.resolve().then(async () => {
try {
const deleted = await deleteFinalizableDraftMessage(retirement.owner, messageId);
if (deleted && pending.get(messageId) === retirement) pending.delete(messageId);
return deleted;
} finally {
retirement.attempt = void 0;
}
});
return retirement.attempt;
};
const drainDeletes = async (retainedId) => {
const visited = /* @__PURE__ */ new Set();
for (const [messageId, retirement] of pending) {
if (visited.has(messageId) || Object.is(messageId, retainedId)) continue;
visited.add(messageId);
await attemptDelete(messageId, retirement);
}
return pending.size === 0;
};
const retire = async (messageId, options) => {
const retirement = claim(messageId, params);
if (!options?.defer) await attemptDelete(messageId, retirement);
};
const serializeCleanup = (operation) => {
const cleanupRun = clearTail.catch(() => {}).then(operation);
clearTail = cleanupRun;
return cleanupRun;
};
const clearWithStop = (stopForClear, messageIdOwner) => {
const owner = messageIdOwner ? {
...params,
...messageIdOwner
} : params;
return serializeCleanup(async () => {
await stopForClear();
const messageId = owner.readMessageId();
if (owner.isValidMessageId(messageId)) claim(messageId, owner);
else owner.clearMessageId();
await drainDeletes();
});
};
const stop = () => {
const previousClear = clearTail.catch(() => {});
const stopRun = Promise.allSettled([controls.stop(), previousClear]).then(async ([stopped]) => {
if (stopped.status === "rejected") throw stopped.reason;
await drainDeletes(params.readMessageId());
});
clearTail = stopRun;
return stopRun;
};
return {
...controls,
stop,
clear: () => clearWithStop(controls.stopForClear),
clearWithStop,
retire,
cleanupPending: (prepareCleanup) => serializeCleanup(async () => {
prepareCleanup?.();
await drainDeletes(params.readMessageId());
})
};
}
//#endregion
export { takeMessageIdAfterStop as a, createFinalizableDraftStreamControlsForState as i, createFinalizableDraftLifecycle as n, createDraftStreamLoop as o, createFinalizableDraftStreamControls as r, clearFinalizableDraftMessage as t };