131 lines
3.2 KiB
TypeScript
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 };
|
|
}
|