@llblab/pi-telegram 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/preview.ts ADDED
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Telegram preview streaming helpers
3
+ * Owns preview transport selection, runtime updates, and preview finalization
4
+ */
5
+
6
+ import {
7
+ buildTelegramPreviewSnapshot,
8
+ type TelegramPreviewRenderStrategy,
9
+ type TelegramPreviewSnapshot,
10
+ type TelegramRenderedChunk,
11
+ type TelegramRenderMode,
12
+ } from "./rendering.ts";
13
+
14
+ export interface TelegramPreviewStateLike {
15
+ mode: "draft" | "message";
16
+ draftId?: number;
17
+ messageId?: number;
18
+ pendingText: string;
19
+ lastSentText: string;
20
+ lastSentParseMode?: "HTML";
21
+ lastSentStrategy?: TelegramPreviewRenderStrategy;
22
+ }
23
+
24
+ export interface TelegramPreviewRuntimeState extends TelegramPreviewStateLike {
25
+ flushTimer?: ReturnType<typeof setTimeout>;
26
+ flushPromise?: Promise<void>;
27
+ flushRequested?: boolean;
28
+ }
29
+
30
+ export interface TelegramSentPreviewMessageLike {
31
+ message_id: number;
32
+ }
33
+
34
+ export interface TelegramPreviewRuntimeDeps {
35
+ getState: () => TelegramPreviewRuntimeState | undefined;
36
+ setState: (state: TelegramPreviewRuntimeState | undefined) => void;
37
+ clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
38
+ maxMessageLength: number;
39
+ renderPreviewText: (markdown: string) => string;
40
+ getDraftSupport: () => "unknown" | "supported" | "unsupported";
41
+ setDraftSupport: (support: "unknown" | "supported" | "unsupported") => void;
42
+ allocateDraftId: () => number;
43
+ sendDraft: (chatId: number, draftId: number, text: string) => Promise<void>;
44
+ sendMessage: (
45
+ chatId: number,
46
+ text: string,
47
+ options?: { parseMode?: "HTML" },
48
+ ) => Promise<TelegramSentPreviewMessageLike>;
49
+ editMessageText: (
50
+ chatId: number,
51
+ messageId: number,
52
+ text: string,
53
+ options?: { parseMode?: "HTML" },
54
+ ) => Promise<void>;
55
+ renderTelegramMessage: (
56
+ text: string,
57
+ options?: { mode?: TelegramRenderMode },
58
+ ) => TelegramRenderedChunk[];
59
+ sendRenderedChunks: (
60
+ chatId: number,
61
+ chunks: TelegramRenderedChunk[],
62
+ ) => Promise<number | undefined>;
63
+ editRenderedMessage: (
64
+ chatId: number,
65
+ messageId: number,
66
+ chunks: TelegramRenderedChunk[],
67
+ ) => Promise<number | undefined>;
68
+ }
69
+
70
+ export function buildTelegramPreviewFinalText(
71
+ state: TelegramPreviewStateLike,
72
+ ): string | undefined {
73
+ const finalText = state.pendingText.trim();
74
+ if (finalText) return finalText;
75
+ if (
76
+ state.lastSentStrategy === "rich-stable-blocks" ||
77
+ state.lastSentParseMode === "HTML"
78
+ ) {
79
+ return undefined;
80
+ }
81
+ return state.lastSentText.trim() || undefined;
82
+ }
83
+
84
+ export function shouldUseTelegramDraftPreview(options: {
85
+ draftSupport: "unknown" | "supported" | "unsupported";
86
+ snapshot?: TelegramPreviewSnapshot;
87
+ }): boolean {
88
+ return (
89
+ options.draftSupport !== "unsupported" &&
90
+ (options.snapshot === undefined || options.snapshot.strategy === "plain")
91
+ );
92
+ }
93
+
94
+ export async function clearTelegramPreview(
95
+ chatId: number,
96
+ deps: TelegramPreviewRuntimeDeps,
97
+ ): Promise<void> {
98
+ const state = deps.getState();
99
+ if (!state) return;
100
+ deps.clearScheduledFlush(state);
101
+ deps.setState(undefined);
102
+ if (state.mode !== "draft" || state.draftId === undefined) return;
103
+ try {
104
+ await deps.sendDraft(chatId, state.draftId, "");
105
+ } catch {
106
+ // ignore
107
+ }
108
+ }
109
+
110
+ async function performTelegramPreviewFlush(
111
+ chatId: number,
112
+ state: TelegramPreviewRuntimeState,
113
+ deps: TelegramPreviewRuntimeDeps,
114
+ ): Promise<void> {
115
+ const snapshot = buildTelegramPreviewSnapshot({
116
+ state,
117
+ maxMessageLength: deps.maxMessageLength,
118
+ renderPreviewText: deps.renderPreviewText,
119
+ renderTelegramMessage: deps.renderTelegramMessage,
120
+ });
121
+ if (!snapshot) return;
122
+ if (
123
+ shouldUseTelegramDraftPreview({
124
+ draftSupport: deps.getDraftSupport(),
125
+ snapshot,
126
+ })
127
+ ) {
128
+ const draftId = state.draftId ?? deps.allocateDraftId();
129
+ state.draftId = draftId;
130
+ try {
131
+ await deps.sendDraft(chatId, draftId, snapshot.text);
132
+ deps.setDraftSupport("supported");
133
+ state.mode = "draft";
134
+ state.lastSentText = snapshot.text;
135
+ state.lastSentParseMode = snapshot.parseMode;
136
+ state.lastSentStrategy = snapshot.strategy;
137
+ return;
138
+ } catch {
139
+ deps.setDraftSupport("unsupported");
140
+ }
141
+ }
142
+ if (state.mode === "draft" && state.draftId !== undefined) {
143
+ try {
144
+ await deps.sendDraft(chatId, state.draftId, "");
145
+ } catch {
146
+ // ignore
147
+ }
148
+ }
149
+ if (state.messageId === undefined) {
150
+ const sent = await deps.sendMessage(chatId, snapshot.text, {
151
+ parseMode: snapshot.parseMode,
152
+ });
153
+ state.messageId = sent.message_id;
154
+ state.mode = "message";
155
+ state.lastSentText = snapshot.text;
156
+ state.lastSentParseMode = snapshot.parseMode;
157
+ state.lastSentStrategy = snapshot.strategy;
158
+ return;
159
+ }
160
+ await deps.editMessageText(chatId, state.messageId, snapshot.text, {
161
+ parseMode: snapshot.parseMode,
162
+ });
163
+ state.mode = "message";
164
+ state.lastSentText = snapshot.text;
165
+ state.lastSentParseMode = snapshot.parseMode;
166
+ state.lastSentStrategy = snapshot.strategy;
167
+ }
168
+
169
+ export async function flushTelegramPreview(
170
+ chatId: number,
171
+ deps: TelegramPreviewRuntimeDeps,
172
+ ): Promise<void> {
173
+ const state = deps.getState();
174
+ if (!state) return;
175
+ if (state.flushPromise) {
176
+ state.flushRequested = true;
177
+ await state.flushPromise;
178
+ return;
179
+ }
180
+ state.flushTimer = undefined;
181
+ state.flushPromise = (async () => {
182
+ do {
183
+ state.flushRequested = false;
184
+ await performTelegramPreviewFlush(chatId, state, deps);
185
+ } while (deps.getState() === state && state.flushRequested);
186
+ })();
187
+ try {
188
+ await state.flushPromise;
189
+ } finally {
190
+ if (deps.getState() === state) {
191
+ state.flushPromise = undefined;
192
+ }
193
+ }
194
+ }
195
+
196
+ export async function finalizeTelegramPreview(
197
+ chatId: number,
198
+ deps: TelegramPreviewRuntimeDeps,
199
+ ): Promise<boolean> {
200
+ const state = deps.getState();
201
+ if (!state) return false;
202
+ await flushTelegramPreview(chatId, deps);
203
+ const finalText = buildTelegramPreviewFinalText(state);
204
+ if (!finalText) {
205
+ await clearTelegramPreview(chatId, deps);
206
+ return false;
207
+ }
208
+ if (state.mode === "draft") {
209
+ await deps.sendMessage(chatId, finalText);
210
+ await clearTelegramPreview(chatId, deps);
211
+ return true;
212
+ }
213
+ deps.setState(undefined);
214
+ return state.messageId !== undefined;
215
+ }
216
+
217
+ export async function finalizeTelegramMarkdownPreview(
218
+ chatId: number,
219
+ markdown: string,
220
+ deps: TelegramPreviewRuntimeDeps,
221
+ ): Promise<boolean> {
222
+ const state = deps.getState();
223
+ if (!state) return false;
224
+ await flushTelegramPreview(chatId, deps);
225
+ const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
226
+ if (chunks.length === 0) {
227
+ await clearTelegramPreview(chatId, deps);
228
+ return false;
229
+ }
230
+ if (state.mode === "draft") {
231
+ await deps.sendRenderedChunks(chatId, chunks);
232
+ await clearTelegramPreview(chatId, deps);
233
+ return true;
234
+ }
235
+ if (state.messageId === undefined) return false;
236
+ await deps.editRenderedMessage(chatId, state.messageId, chunks);
237
+ deps.setState(undefined);
238
+ return true;
239
+ }