Files
clawdbot/src/telegram/draft-stream.ts
2026-01-07 11:08:32 +01:00

131 lines
3.2 KiB
TypeScript

import type { Bot } from "grammy";
const TELEGRAM_DRAFT_MAX_CHARS = 4096;
const DEFAULT_THROTTLE_MS = 300;
export type TelegramDraftStream = {
update: (text: string) => void;
flush: () => Promise<void>;
stop: () => void;
};
export function createTelegramDraftStream(params: {
api: Bot["api"];
chatId: number;
draftId: number;
maxChars?: number;
messageThreadId?: number;
throttleMs?: number;
log?: (message: string) => void;
warn?: (message: string) => void;
}): TelegramDraftStream {
const maxChars = Math.min(
params.maxChars ?? TELEGRAM_DRAFT_MAX_CHARS,
TELEGRAM_DRAFT_MAX_CHARS,
);
const throttleMs = Math.max(50, params.throttleMs ?? DEFAULT_THROTTLE_MS);
const rawDraftId = Number.isFinite(params.draftId)
? Math.trunc(params.draftId)
: 1;
const draftId = rawDraftId === 0 ? 1 : Math.abs(rawDraftId);
const chatId = params.chatId;
const threadParams =
typeof params.messageThreadId === "number"
? { message_thread_id: Math.trunc(params.messageThreadId) }
: undefined;
let lastSentText = "";
let lastSentAt = 0;
let pendingText = "";
let inFlight = false;
let timer: ReturnType<typeof setTimeout> | undefined;
let stopped = false;
const sendDraft = async (text: string) => {
if (stopped) return;
const trimmed = text.trimEnd();
if (!trimmed) return;
if (trimmed.length > maxChars) {
// Drafts are capped at 4096 chars. Stop streaming once we exceed the cap
// so we don't keep sending failing updates or a truncated preview.
stopped = true;
params.warn?.(
`telegram draft stream stopped (draft length ${trimmed.length} > ${maxChars})`,
);
return;
}
if (trimmed === lastSentText) return;
lastSentText = trimmed;
lastSentAt = Date.now();
try {
await params.api.sendMessageDraft(chatId, draftId, trimmed, threadParams);
} catch (err) {
stopped = true;
params.warn?.(
`telegram draft stream failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
};
const flush = async () => {
if (timer) {
clearTimeout(timer);
timer = undefined;
}
if (inFlight) {
schedule();
return;
}
const text = pendingText;
pendingText = "";
if (!text.trim()) {
if (pendingText) schedule();
return;
}
inFlight = true;
try {
await sendDraft(text);
} finally {
inFlight = false;
}
if (pendingText) schedule();
};
const schedule = () => {
if (timer) return;
const delay = Math.max(0, throttleMs - (Date.now() - lastSentAt));
timer = setTimeout(() => {
void flush();
}, delay);
};
const update = (text: string) => {
if (stopped) return;
pendingText = text;
if (inFlight) {
schedule();
return;
}
if (!timer && Date.now() - lastSentAt >= throttleMs) {
void flush();
return;
}
schedule();
};
const stop = () => {
stopped = true;
pendingText = "";
if (timer) {
clearTimeout(timer);
timer = undefined;
}
};
params.log?.(
`telegram draft stream ready (draftId=${draftId}, maxChars=${maxChars}, throttleMs=${throttleMs})`,
);
return { update, flush, stop };
}