@llblab/pi-telegram 0.54.3 → 0.55.0
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/AGENTS.md +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +5 -1
- package/dist/lib/bindings.js +9 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +49 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +5 -3
- package/dist/lib/bus.js +13 -10
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +56 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +14 -2
- package/dist/lib/routing.js +546 -207
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +20 -5
- package/dist/lib/threads.js +111 -32
- package/dist/lib/turns.d.ts +2 -0
- package/dist/lib/turns.js +6 -1
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +16 -15
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +18 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +8 -6
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +13 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +17 -15
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +59 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +644 -283
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +130 -38
- package/lib/turns.ts +8 -1
- package/lib/updates.ts +58 -21
- package/package.json +1 -1
package/lib/preview.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* Telegram preview streaming helpers
|
|
3
3
|
* Zones: telegram outbound, native rich markdown drafts
|
|
4
4
|
* Owns safe draft preview selection, runtime updates, and preview finalization
|
|
5
|
+
* The streaming state machine is a pure transition function driven by one imperative shell per runtime.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import type { TelegramAssistantOutputPreparation } from "./activity.ts";
|
|
9
|
+
import * as Machine from "./machine.ts";
|
|
8
10
|
import { stripTelegramCommentMarkupForPreview } from "./outbound-markup.ts";
|
|
9
11
|
import {
|
|
10
12
|
isTelegramMarkdownFenceClosing,
|
|
@@ -25,8 +27,6 @@ const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
|
|
|
25
27
|
// an accumulated passage rather than a single streamed word.
|
|
26
28
|
const TELEGRAM_DRAFT_INTERVAL_MS = 2_000;
|
|
27
29
|
|
|
28
|
-
export type TelegramDraftSupport = "unknown" | "supported";
|
|
29
|
-
|
|
30
30
|
export interface TelegramPreviewState {
|
|
31
31
|
mode: "draft";
|
|
32
32
|
draftId?: number;
|
|
@@ -34,43 +34,30 @@ export interface TelegramPreviewState {
|
|
|
34
34
|
lastSentText: string;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
publicationPromise?: Promise<void>;
|
|
42
|
-
sealed?: boolean;
|
|
43
|
-
nextDraftAt?: number;
|
|
44
|
-
flushTimer?: ReturnType<typeof setTimeout>;
|
|
37
|
+
/** One immutable preview segment; a replacement segment gets a new id, so late results for an old one are dropped. */
|
|
38
|
+
export interface TelegramPreviewSegment extends Readonly<TelegramPreviewState> {
|
|
39
|
+
readonly id: number;
|
|
40
|
+
readonly sealed: boolean;
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
export type TelegramPreviewReplyMarkup = unknown;
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
},
|
|
66
|
-
) => Promise<unknown>;
|
|
67
|
-
canSend?: () => boolean;
|
|
68
|
-
recordRuntimeEvent?: (
|
|
69
|
-
category: string,
|
|
70
|
-
error: unknown,
|
|
71
|
-
details?: Record<string, unknown>,
|
|
72
|
-
) => void;
|
|
73
|
-
}
|
|
45
|
+
type TelegramDraftSender = (
|
|
46
|
+
chatId: number,
|
|
47
|
+
draftId: number,
|
|
48
|
+
text?: string,
|
|
49
|
+
options?: {
|
|
50
|
+
parse_mode?: string;
|
|
51
|
+
entities?: unknown[];
|
|
52
|
+
message_thread_id?: number;
|
|
53
|
+
},
|
|
54
|
+
) => Promise<unknown>;
|
|
55
|
+
|
|
56
|
+
type TelegramPreviewEventRecorder = (
|
|
57
|
+
category: string,
|
|
58
|
+
error: unknown,
|
|
59
|
+
details?: Record<string, unknown>,
|
|
60
|
+
) => void;
|
|
74
61
|
|
|
75
62
|
export interface TelegramPreviewActiveTurn {
|
|
76
63
|
chatId: number;
|
|
@@ -81,34 +68,6 @@ export interface TelegramPreviewActiveTurn {
|
|
|
81
68
|
guestQueryId?: string;
|
|
82
69
|
}
|
|
83
70
|
|
|
84
|
-
export interface TelegramAssistantMessagePreviewStartDeps<TMessage> {
|
|
85
|
-
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
86
|
-
isAssistantMessage: (message: TMessage) => boolean;
|
|
87
|
-
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
88
|
-
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
89
|
-
createPreviewState: () => TelegramPreviewRuntimeState;
|
|
90
|
-
canSend?: () => boolean;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
|
|
94
|
-
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
95
|
-
isAssistantMessage: (message: TMessage) => boolean;
|
|
96
|
-
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
97
|
-
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
98
|
-
createPreviewState: () => TelegramPreviewRuntimeState;
|
|
99
|
-
canSend?: () => boolean;
|
|
100
|
-
getMessageText: (message: TMessage) => string;
|
|
101
|
-
minDraftIntervalMs?: number;
|
|
102
|
-
schedulePreviewFlush: (
|
|
103
|
-
chatId: number,
|
|
104
|
-
options?: { target?: TelegramTarget },
|
|
105
|
-
) => void;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export type TelegramAssistantMessagePreviewHookDeps<TMessage> =
|
|
109
|
-
TelegramAssistantMessagePreviewStartDeps<TMessage> &
|
|
110
|
-
TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
|
|
111
|
-
|
|
112
71
|
export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
|
|
113
72
|
message: TMessage;
|
|
114
73
|
}
|
|
@@ -125,79 +84,55 @@ export interface TelegramAssistantMessagePreviewHooks<TMessage> {
|
|
|
125
84
|
export interface TelegramPreviewControllerDeps {
|
|
126
85
|
getDefaultReplyToMessageId?: () => number | undefined;
|
|
127
86
|
maxMessageLength?: number;
|
|
128
|
-
|
|
129
|
-
sendDraft: (
|
|
130
|
-
chatId: number,
|
|
131
|
-
draftId: number,
|
|
132
|
-
text?: string,
|
|
133
|
-
options?: {
|
|
134
|
-
parse_mode?: string;
|
|
135
|
-
entities?: unknown[];
|
|
136
|
-
message_thread_id?: number;
|
|
137
|
-
},
|
|
138
|
-
) => Promise<unknown>;
|
|
87
|
+
sendDraft: TelegramDraftSender;
|
|
139
88
|
canSend?: () => boolean;
|
|
140
89
|
maxDraftId?: number;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
details?: Record<string, unknown>,
|
|
145
|
-
) => void;
|
|
90
|
+
/** Throttle between draft frames; defaults to the native draft cadence. */
|
|
91
|
+
minDraftIntervalMs?: number;
|
|
92
|
+
recordRuntimeEvent?: TelegramPreviewEventRecorder;
|
|
146
93
|
}
|
|
147
94
|
|
|
95
|
+
type TelegramPreviewTargetOptions = { target?: TelegramTarget };
|
|
96
|
+
|
|
148
97
|
export interface TelegramPreviewController {
|
|
98
|
+
/** Read-only view of the current segment; it never grants mutation. */
|
|
99
|
+
getState: () => TelegramPreviewSegment | undefined;
|
|
100
|
+
/** True while a draft request is in flight or a pending request waits behind a publication. */
|
|
101
|
+
isFlushing: () => boolean;
|
|
149
102
|
seal: () => void;
|
|
150
103
|
preparePublication: () => TelegramAssistantOutputPreparation | undefined;
|
|
151
104
|
prepareClear: (
|
|
152
105
|
chatId: number,
|
|
153
|
-
options?:
|
|
106
|
+
options?: TelegramPreviewTargetOptions & {
|
|
107
|
+
isDeliveryActive?: () => boolean;
|
|
108
|
+
},
|
|
154
109
|
) => () => Promise<void>;
|
|
155
|
-
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
156
|
-
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
157
110
|
setPendingText: (text: string) => void;
|
|
158
|
-
createState: () => TelegramPreviewRuntimeState;
|
|
159
111
|
resetState: () => void;
|
|
160
112
|
invalidate: () => void;
|
|
161
113
|
clear: (
|
|
162
114
|
chatId: number,
|
|
163
|
-
options?: { awaitFlush?: boolean
|
|
115
|
+
options?: TelegramPreviewTargetOptions & { awaitFlush?: boolean },
|
|
164
116
|
) => Promise<void>;
|
|
165
117
|
flush: (
|
|
166
118
|
chatId: number,
|
|
167
|
-
options?:
|
|
119
|
+
options?: TelegramPreviewTargetOptions,
|
|
168
120
|
) => Promise<void>;
|
|
169
121
|
scheduleFlush: (
|
|
170
122
|
chatId: number,
|
|
171
|
-
options?:
|
|
123
|
+
options?: TelegramPreviewTargetOptions,
|
|
172
124
|
) => void;
|
|
173
125
|
finalize: (
|
|
174
126
|
chatId: number,
|
|
175
127
|
replyToMessageId?: number,
|
|
176
|
-
options?:
|
|
128
|
+
options?: TelegramPreviewTargetOptions,
|
|
177
129
|
) => Promise<boolean>;
|
|
178
130
|
}
|
|
179
131
|
|
|
180
|
-
export type TelegramPreviewControllerRuntimeDeps =
|
|
181
|
-
TelegramPreviewControllerDeps;
|
|
182
|
-
|
|
183
|
-
export function createTelegramPreviewControllerRuntime(
|
|
184
|
-
deps: TelegramPreviewControllerRuntimeDeps,
|
|
185
|
-
): TelegramPreviewController {
|
|
186
|
-
return createTelegramPreviewController({
|
|
187
|
-
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
188
|
-
maxMessageLength: deps.maxMessageLength,
|
|
189
|
-
initialDraftSupport: deps.initialDraftSupport,
|
|
190
|
-
sendDraft: deps.sendDraft,
|
|
191
|
-
canSend: deps.canSend,
|
|
192
|
-
maxDraftId: deps.maxDraftId,
|
|
193
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
132
|
export interface TelegramAssistantPreviewRuntimeDeps<
|
|
198
133
|
TMessage,
|
|
199
134
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
200
|
-
> extends
|
|
135
|
+
> extends TelegramPreviewControllerDeps {
|
|
201
136
|
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
202
137
|
isAssistantMessage: (message: TMessage) => boolean;
|
|
203
138
|
getMessageText: (message: TMessage) => string;
|
|
@@ -209,12 +144,17 @@ export interface TelegramAssistantPreviewRuntimeDeps<
|
|
|
209
144
|
) => Promise<number | undefined>;
|
|
210
145
|
}
|
|
211
146
|
|
|
147
|
+
type TelegramMarkdownFinalizer<TReplyMarkup> = (
|
|
148
|
+
chatId: number,
|
|
149
|
+
markdown: string,
|
|
150
|
+
replyToMessageId?: number,
|
|
151
|
+
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
152
|
+
) => Promise<boolean>;
|
|
153
|
+
|
|
212
154
|
export interface TelegramPreparedPreviewDelivery<TReplyMarkup = unknown> {
|
|
213
155
|
clearPreview: TelegramPreviewController["clear"];
|
|
214
156
|
setPreviewPendingText: TelegramPreviewController["setPendingText"];
|
|
215
|
-
finalizeMarkdownPreview:
|
|
216
|
-
typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>
|
|
217
|
-
>;
|
|
157
|
+
finalizeMarkdownPreview: TelegramMarkdownFinalizer<TReplyMarkup>;
|
|
218
158
|
}
|
|
219
159
|
|
|
220
160
|
export type TelegramAssistantPreviewRuntime<
|
|
@@ -225,237 +165,627 @@ export type TelegramAssistantPreviewRuntime<
|
|
|
225
165
|
prepareDelivery: (
|
|
226
166
|
isDeliveryActive: () => boolean,
|
|
227
167
|
) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
|
|
228
|
-
finalizeMarkdown:
|
|
229
|
-
chatId: number,
|
|
230
|
-
markdown: string,
|
|
231
|
-
replyToMessageId?: number,
|
|
232
|
-
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
233
|
-
) => Promise<boolean>;
|
|
168
|
+
finalizeMarkdown: TelegramMarkdownFinalizer<TReplyMarkup>;
|
|
234
169
|
};
|
|
235
170
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (!state) return;
|
|
240
|
-
state.sealed = true;
|
|
241
|
-
state.flushRequested = false;
|
|
242
|
-
if (state.flushTimer) clearTimeout(state.flushTimer);
|
|
243
|
-
state.flushTimer = undefined;
|
|
171
|
+
interface TelegramPreviewFlushRequest {
|
|
172
|
+
chatId: number;
|
|
173
|
+
target?: TelegramTarget;
|
|
244
174
|
}
|
|
245
175
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
clear: (
|
|
251
|
-
chatId: number,
|
|
252
|
-
options?: { awaitFlush?: boolean; target?: TelegramTarget },
|
|
253
|
-
) => Promise<void>;
|
|
254
|
-
discard?: () => void;
|
|
255
|
-
isDeliveryActive?: () => boolean;
|
|
256
|
-
sendMarkdownReply: (
|
|
257
|
-
chatId: number,
|
|
258
|
-
replyToMessageId: number | undefined,
|
|
259
|
-
markdown: string,
|
|
260
|
-
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
261
|
-
) => Promise<number | undefined>;
|
|
262
|
-
}): (
|
|
263
|
-
chatId: number,
|
|
264
|
-
markdown: string,
|
|
265
|
-
replyToMessageId?: number,
|
|
266
|
-
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
|
|
267
|
-
) => Promise<boolean> {
|
|
268
|
-
return (...args) =>
|
|
269
|
-
prepareTelegramNativeMarkdownPreviewFinalizer(deps)(...args);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function prepareTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(
|
|
273
|
-
deps: Parameters<
|
|
274
|
-
typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>
|
|
275
|
-
>[0],
|
|
276
|
-
): ReturnType<
|
|
277
|
-
typeof createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>
|
|
278
|
-
> {
|
|
279
|
-
const state = deps.getState();
|
|
280
|
-
sealTelegramPreviewState(state);
|
|
281
|
-
const inFlight = state?.flushPromise ?? state?.precedingFlush;
|
|
282
|
-
return async (chatId, markdown, replyToMessageId, options) => {
|
|
283
|
-
if (deps.isDeliveryActive?.() === false) return false;
|
|
284
|
-
await inFlight?.catch(() => {});
|
|
285
|
-
if (deps.getState() !== state || deps.isDeliveryActive?.() === false)
|
|
286
|
-
return false;
|
|
287
|
-
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
288
|
-
if (deps.getState() === state && deps.isDeliveryActive?.() !== false)
|
|
289
|
-
deps.discard?.();
|
|
290
|
-
return true;
|
|
291
|
-
};
|
|
176
|
+
interface TelegramPreviewMachineConfig {
|
|
177
|
+
maxDraftId: number;
|
|
178
|
+
maxMessageLength: number;
|
|
179
|
+
minDraftIntervalMs: number;
|
|
292
180
|
}
|
|
293
181
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
182
|
+
/** The whole preview state; only `transitionTelegramPreview` produces a new one. */
|
|
183
|
+
export interface TelegramPreviewMachineState {
|
|
184
|
+
readonly segment: TelegramPreviewSegment | null;
|
|
185
|
+
/** Bumped by reset and invalidation; a result issued under an older generation is inert. */
|
|
186
|
+
readonly generation: number;
|
|
187
|
+
readonly nextSegmentId: number;
|
|
188
|
+
readonly nextDraftId: number;
|
|
189
|
+
readonly nextDraftAt: number;
|
|
190
|
+
/** Segment that owns the current generation's issued draft; old-generation requests do not block it. */
|
|
191
|
+
readonly inFlight: number | null;
|
|
192
|
+
/** The one coalesced flush request waiting behind an in-flight draft or an open publication. */
|
|
193
|
+
readonly pending: TelegramPreviewFlushRequest | null;
|
|
194
|
+
readonly publications: readonly number[];
|
|
195
|
+
/** The segment the throttle timer is armed for; the timer belongs to that segment alone. */
|
|
196
|
+
readonly timerSegmentId: number | null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Impure inputs the runner samples at dispatch time, so transitions stay pure. */
|
|
200
|
+
interface TelegramPreviewClock {
|
|
201
|
+
now: number;
|
|
202
|
+
canSend: boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export type TelegramPreviewEvent =
|
|
206
|
+
| { type: "reset" }
|
|
207
|
+
| { type: "invalidated" }
|
|
208
|
+
| { type: "segmentStarted" }
|
|
209
|
+
| { type: "segmentEnsured" }
|
|
210
|
+
| { type: "sealed" }
|
|
211
|
+
| { type: "discarded" }
|
|
212
|
+
| { type: "textSet"; text: string }
|
|
213
|
+
| { type: "draftWindowOpened"; at: number }
|
|
214
|
+
| ({
|
|
215
|
+
type: "flushRequested";
|
|
216
|
+
request: TelegramPreviewFlushRequest;
|
|
217
|
+
} & TelegramPreviewClock)
|
|
218
|
+
| ({
|
|
219
|
+
type: "timerFired";
|
|
220
|
+
request: TelegramPreviewFlushRequest;
|
|
221
|
+
segmentId: number;
|
|
222
|
+
} & TelegramPreviewClock)
|
|
223
|
+
| ({
|
|
224
|
+
type: "draftSettled";
|
|
225
|
+
segmentId: number;
|
|
226
|
+
generation: number;
|
|
227
|
+
/** Present only when Telegram accepted the draft. */
|
|
228
|
+
text?: string;
|
|
229
|
+
} & TelegramPreviewClock)
|
|
230
|
+
| { type: "publicationOpened"; id: number }
|
|
231
|
+
| ({ type: "publicationSettled"; id: number } & TelegramPreviewClock);
|
|
232
|
+
|
|
233
|
+
export type TelegramPreviewEffect =
|
|
234
|
+
| {
|
|
235
|
+
type: "sendDraft";
|
|
236
|
+
request: TelegramPreviewFlushRequest;
|
|
237
|
+
draftId: number;
|
|
238
|
+
text: string;
|
|
239
|
+
segmentId: number;
|
|
240
|
+
generation: number;
|
|
241
|
+
}
|
|
242
|
+
| {
|
|
243
|
+
type: "armTimer";
|
|
244
|
+
delay: number;
|
|
245
|
+
request: TelegramPreviewFlushRequest;
|
|
246
|
+
segmentId: number;
|
|
247
|
+
}
|
|
248
|
+
| { type: "cancelTimer" }
|
|
249
|
+
| { type: "clearClosedGate"; request: TelegramPreviewFlushRequest };
|
|
250
|
+
|
|
251
|
+
export interface TelegramPreviewStep {
|
|
252
|
+
state: TelegramPreviewMachineState;
|
|
253
|
+
effects: readonly TelegramPreviewEffect[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createTelegramPreviewMachineState(): TelegramPreviewMachineState {
|
|
257
|
+
return {
|
|
258
|
+
segment: null,
|
|
259
|
+
generation: 0,
|
|
260
|
+
nextSegmentId: 0,
|
|
261
|
+
nextDraftId: 0,
|
|
262
|
+
nextDraftAt: 0,
|
|
263
|
+
inFlight: null,
|
|
264
|
+
pending: null,
|
|
265
|
+
publications: [],
|
|
266
|
+
timerSegmentId: null,
|
|
306
267
|
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const step = (
|
|
271
|
+
state: TelegramPreviewMachineState,
|
|
272
|
+
...effects: TelegramPreviewEffect[]
|
|
273
|
+
): TelegramPreviewStep => ({ state, effects });
|
|
274
|
+
|
|
275
|
+
/** Drops the armed throttle timer, if any; every caller replaces or seals its segment. */
|
|
276
|
+
const withoutTimer = (
|
|
277
|
+
state: TelegramPreviewMachineState,
|
|
278
|
+
): TelegramPreviewStep =>
|
|
279
|
+
state.timerSegmentId === null
|
|
280
|
+
? step(state)
|
|
281
|
+
: step({ ...state, timerSegmentId: null }, { type: "cancelTimer" });
|
|
282
|
+
|
|
283
|
+
/** A replaced segment takes its pending request and its throttle timer with it. */
|
|
284
|
+
function replaceSegment(
|
|
285
|
+
state: TelegramPreviewMachineState,
|
|
286
|
+
segment: TelegramPreviewSegment | null,
|
|
287
|
+
): TelegramPreviewStep {
|
|
288
|
+
return withoutTimer({ ...state, segment, pending: null });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function startSegment(
|
|
292
|
+
state: TelegramPreviewMachineState,
|
|
293
|
+
carry: Partial<TelegramPreviewState> = {},
|
|
294
|
+
): TelegramPreviewStep {
|
|
295
|
+
const id = state.nextSegmentId + 1;
|
|
296
|
+
return replaceSegment(
|
|
297
|
+
{ ...state, nextSegmentId: id },
|
|
298
|
+
{
|
|
299
|
+
id,
|
|
300
|
+
mode: "draft",
|
|
301
|
+
pendingText: "",
|
|
302
|
+
lastSentText: "",
|
|
303
|
+
sealed: false,
|
|
304
|
+
...carry,
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** One flush attempt for the current segment: gate, throttle, safe snapshot and draft id. */
|
|
310
|
+
function attemptFlush(
|
|
311
|
+
state: TelegramPreviewMachineState,
|
|
312
|
+
request: TelegramPreviewFlushRequest,
|
|
313
|
+
clock: TelegramPreviewClock,
|
|
314
|
+
config: TelegramPreviewMachineConfig,
|
|
315
|
+
): TelegramPreviewStep {
|
|
316
|
+
const segment = state.segment;
|
|
317
|
+
if (!segment || segment.sealed) return step(state);
|
|
318
|
+
if (!clock.canSend) return step(state, { type: "clearClosedGate", request });
|
|
319
|
+
const delay = state.nextDraftAt - clock.now;
|
|
320
|
+
if (delay > 0) {
|
|
321
|
+
if (state.timerSegmentId !== null) return step(state);
|
|
322
|
+
return step(
|
|
323
|
+
{ ...state, timerSegmentId: segment.id },
|
|
324
|
+
{ type: "armTimer", delay, request, segmentId: segment.id },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const idle = withoutTimer(state);
|
|
328
|
+
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
329
|
+
segment,
|
|
330
|
+
config.maxMessageLength,
|
|
331
|
+
);
|
|
332
|
+
if (!snapshot) return idle;
|
|
333
|
+
const draftId =
|
|
334
|
+
segment.draftId ??
|
|
335
|
+
allocateTelegramDraftId(state.nextDraftId, config.maxDraftId);
|
|
307
336
|
return {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
setPreviewPendingText(text) {
|
|
315
|
-
if (controller.getState() === state && isDeliveryActive())
|
|
316
|
-
controller.setPendingText(text);
|
|
317
|
-
},
|
|
318
|
-
clearPreview: async (chatId, options) => {
|
|
319
|
-
if (controller.getState() !== state || !isDeliveryActive()) return;
|
|
320
|
-
await controller.prepareClear(chatId, {
|
|
321
|
-
...options,
|
|
322
|
-
isDeliveryActive,
|
|
323
|
-
})();
|
|
324
|
-
},
|
|
325
|
-
finalizeMarkdownPreview: prepareTelegramNativeMarkdownPreviewFinalizer({
|
|
326
|
-
...finalizerDeps,
|
|
327
|
-
isDeliveryActive,
|
|
328
|
-
}),
|
|
329
|
-
};
|
|
337
|
+
state: {
|
|
338
|
+
...idle.state,
|
|
339
|
+
segment: { ...segment, draftId },
|
|
340
|
+
nextDraftId: segment.draftId === undefined ? draftId : state.nextDraftId,
|
|
341
|
+
nextDraftAt: clock.now + config.minDraftIntervalMs,
|
|
342
|
+
inFlight: segment.id,
|
|
330
343
|
},
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
344
|
+
effects: [
|
|
345
|
+
...idle.effects,
|
|
346
|
+
{
|
|
347
|
+
type: "sendDraft",
|
|
348
|
+
request,
|
|
349
|
+
draftId,
|
|
350
|
+
text: snapshot.text,
|
|
351
|
+
segmentId: segment.id,
|
|
352
|
+
generation: state.generation,
|
|
353
|
+
},
|
|
354
|
+
],
|
|
342
355
|
};
|
|
343
356
|
}
|
|
344
357
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
358
|
+
/** Single flight: a request during an in-flight draft or behind a publication waits as the one pending request. */
|
|
359
|
+
function requestFlush(
|
|
360
|
+
state: TelegramPreviewMachineState,
|
|
361
|
+
request: TelegramPreviewFlushRequest,
|
|
362
|
+
clock: TelegramPreviewClock,
|
|
363
|
+
config: TelegramPreviewMachineConfig,
|
|
364
|
+
): TelegramPreviewStep {
|
|
365
|
+
// Only a live, unsealed segment accepts flush requests, whatever the caller checked.
|
|
366
|
+
if (!state.segment || state.segment.sealed) return step(state);
|
|
367
|
+
if (state.inFlight !== null || state.publications.length > 0)
|
|
368
|
+
return step({ ...state, pending: request });
|
|
369
|
+
return attemptFlush(state, request, clock, config);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A coalesced request runs once the in-flight draft and every open publication have settled. */
|
|
373
|
+
function drainPending(
|
|
374
|
+
state: TelegramPreviewMachineState,
|
|
375
|
+
clock: TelegramPreviewClock,
|
|
376
|
+
config: TelegramPreviewMachineConfig,
|
|
377
|
+
): TelegramPreviewStep {
|
|
378
|
+
const request = state.pending;
|
|
379
|
+
if (!request || state.inFlight !== null || state.publications.length > 0)
|
|
380
|
+
return step(state);
|
|
381
|
+
return attemptFlush({ ...state, pending: null }, request, clock, config);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function transitionTelegramPreview(
|
|
385
|
+
state: TelegramPreviewMachineState,
|
|
386
|
+
event: TelegramPreviewEvent,
|
|
387
|
+
config: TelegramPreviewMachineConfig,
|
|
388
|
+
): TelegramPreviewStep {
|
|
389
|
+
switch (event.type) {
|
|
390
|
+
case "reset":
|
|
391
|
+
case "invalidated": {
|
|
392
|
+
const replacement = {
|
|
393
|
+
...state,
|
|
394
|
+
generation: state.generation + 1,
|
|
395
|
+
inFlight: null,
|
|
396
|
+
publications: [],
|
|
397
|
+
nextDraftAt: 0,
|
|
398
|
+
};
|
|
399
|
+
return event.type === "reset"
|
|
400
|
+
? startSegment(replacement)
|
|
401
|
+
: replaceSegment(replacement, null);
|
|
402
|
+
}
|
|
403
|
+
case "segmentStarted": {
|
|
404
|
+
// Carry the previous delivery boundary; permanent text remains with its publication owner.
|
|
405
|
+
const draftId = state.segment?.draftId;
|
|
406
|
+
return startSegment(state, draftId === undefined ? {} : { draftId });
|
|
407
|
+
}
|
|
408
|
+
case "segmentEnsured":
|
|
409
|
+
return state.segment ? step(state) : startSegment(state);
|
|
410
|
+
case "sealed": {
|
|
411
|
+
const segment = state.segment;
|
|
412
|
+
return withoutTimer({
|
|
413
|
+
...state,
|
|
414
|
+
segment:
|
|
415
|
+
segment && !segment.sealed ? { ...segment, sealed: true } : segment,
|
|
416
|
+
pending: null,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
case "discarded":
|
|
420
|
+
return replaceSegment(state, null);
|
|
421
|
+
case "textSet":
|
|
422
|
+
return state.segment
|
|
423
|
+
? step({
|
|
424
|
+
...state,
|
|
425
|
+
segment: { ...state.segment, pendingText: event.text },
|
|
426
|
+
})
|
|
427
|
+
: step(state);
|
|
428
|
+
case "draftWindowOpened":
|
|
429
|
+
return step({
|
|
430
|
+
...state,
|
|
431
|
+
nextDraftAt: Math.max(state.nextDraftAt, event.at),
|
|
432
|
+
});
|
|
433
|
+
case "flushRequested":
|
|
434
|
+
return requestFlush(state, event.request, event, config);
|
|
435
|
+
case "timerFired": {
|
|
436
|
+
if (
|
|
437
|
+
state.timerSegmentId !== event.segmentId ||
|
|
438
|
+
state.segment?.id !== event.segmentId
|
|
439
|
+
)
|
|
440
|
+
return step(state);
|
|
441
|
+
return requestFlush(
|
|
442
|
+
{ ...state, timerSegmentId: null },
|
|
443
|
+
event.request,
|
|
444
|
+
event,
|
|
445
|
+
config,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
case "draftSettled": {
|
|
449
|
+
if (
|
|
450
|
+
state.generation !== event.generation ||
|
|
451
|
+
state.inFlight !== event.segmentId
|
|
452
|
+
)
|
|
453
|
+
return step(state);
|
|
454
|
+
const segment = state.segment;
|
|
455
|
+
// A delivered draft updates only the segment and generation that issued it.
|
|
456
|
+
const applies =
|
|
457
|
+
event.text !== undefined &&
|
|
458
|
+
segment?.id === event.segmentId &&
|
|
459
|
+
event.canSend;
|
|
460
|
+
return drainPending(
|
|
461
|
+
{
|
|
462
|
+
...state,
|
|
463
|
+
inFlight: null,
|
|
464
|
+
segment:
|
|
465
|
+
applies && segment
|
|
466
|
+
? { ...segment, lastSentText: event.text! }
|
|
467
|
+
: segment,
|
|
468
|
+
},
|
|
469
|
+
event,
|
|
470
|
+
config,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
case "publicationOpened":
|
|
474
|
+
return step({
|
|
475
|
+
...state,
|
|
476
|
+
publications: [...state.publications, event.id],
|
|
477
|
+
});
|
|
478
|
+
case "publicationSettled":
|
|
479
|
+
if (!state.publications.includes(event.id)) return step(state);
|
|
480
|
+
return drainPending(
|
|
481
|
+
{
|
|
482
|
+
...state,
|
|
483
|
+
publications: state.publications.filter((id) => id !== event.id),
|
|
484
|
+
},
|
|
485
|
+
event,
|
|
486
|
+
config,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** True while a draft request is in flight, or, when publications block, a pending request waits behind one. */
|
|
492
|
+
export function isTelegramPreviewBusy(
|
|
493
|
+
state: TelegramPreviewMachineState,
|
|
494
|
+
blockedByPublication = true,
|
|
495
|
+
): boolean {
|
|
496
|
+
return (
|
|
497
|
+
state.inFlight !== null ||
|
|
498
|
+
(blockedByPublication &&
|
|
499
|
+
state.pending !== null &&
|
|
500
|
+
state.publications.length > 0)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The imperative shell around `transitionTelegramPreview`: it samples the clock and send gate into events, runs
|
|
506
|
+
* effects, records every transport failure in one place and resolves promise-shaped ports through `waitFor`.
|
|
507
|
+
*/
|
|
508
|
+
function createTelegramPreviewModel(deps: TelegramPreviewControllerDeps) {
|
|
509
|
+
const config: TelegramPreviewMachineConfig = {
|
|
510
|
+
maxDraftId: deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX,
|
|
511
|
+
maxMessageLength: deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS,
|
|
512
|
+
minDraftIntervalMs: deps.minDraftIntervalMs ?? TELEGRAM_DRAFT_INTERVAL_MS,
|
|
360
513
|
};
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
):
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
maxMessageLength,
|
|
367
|
-
minDraftIntervalMs: TELEGRAM_DRAFT_INTERVAL_MS,
|
|
368
|
-
getDraftSupport: () => draftSupport,
|
|
369
|
-
setDraftSupport: (support) => {
|
|
370
|
-
draftSupport = support;
|
|
371
|
-
},
|
|
372
|
-
allocateDraftId: () => {
|
|
373
|
-
nextDraftId = allocateTelegramDraftId(nextDraftId, maxDraftId);
|
|
374
|
-
return nextDraftId;
|
|
375
|
-
},
|
|
376
|
-
sendDraft: deps.sendDraft,
|
|
377
|
-
canSend: () =>
|
|
378
|
-
operationGeneration === generation && (deps.canSend?.() ?? true),
|
|
379
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
514
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
515
|
+
let publicationIds = 0;
|
|
516
|
+
const clock = (): TelegramPreviewClock => ({
|
|
517
|
+
now: Date.now(),
|
|
518
|
+
canSend: deps.canSend?.() !== false,
|
|
380
519
|
});
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
520
|
+
const machine = Machine.createTelegramMachineRunner<
|
|
521
|
+
TelegramPreviewMachineState,
|
|
522
|
+
TelegramPreviewEvent,
|
|
523
|
+
TelegramPreviewEffect
|
|
524
|
+
>({
|
|
525
|
+
initial: createTelegramPreviewMachineState(),
|
|
526
|
+
transition: (state, event) =>
|
|
527
|
+
transitionTelegramPreview(state, event, config),
|
|
528
|
+
run: (effect) => run(effect),
|
|
529
|
+
});
|
|
530
|
+
const { dispatch, waitFor } = machine;
|
|
531
|
+
const whenIdle = (): Promise<void> => {
|
|
532
|
+
const generation = machine.state.generation;
|
|
533
|
+
return waitFor(
|
|
534
|
+
(current) =>
|
|
535
|
+
current.generation !== generation || !isTelegramPreviewBusy(current),
|
|
536
|
+
);
|
|
537
|
+
};
|
|
538
|
+
/** Captures predecessors only: a later draft/publication must never extend the boundary that blocks it. */
|
|
539
|
+
const whenBoundarySettles = (includePublications = true): Promise<void> => {
|
|
540
|
+
const { generation, inFlight, publications } = machine.state;
|
|
541
|
+
return waitFor(
|
|
542
|
+
(current) =>
|
|
543
|
+
current.generation !== generation ||
|
|
544
|
+
((inFlight === null || current.inFlight !== inFlight) &&
|
|
545
|
+
(!includePublications ||
|
|
546
|
+
publications.every((id) => !current.publications.includes(id)))),
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
const canSendFor = (generation: number): boolean =>
|
|
550
|
+
machine.state.generation === generation && deps.canSend?.() !== false;
|
|
551
|
+
|
|
552
|
+
/** The one place preview transport failures become diagnostics; a failed call reports `false`. */
|
|
553
|
+
const callTransport = async (
|
|
554
|
+
phase: "draft" | "clear-draft",
|
|
555
|
+
request: TelegramPreviewFlushRequest,
|
|
556
|
+
draftId: number,
|
|
557
|
+
text: string | undefined,
|
|
558
|
+
): Promise<unknown> => {
|
|
559
|
+
try {
|
|
560
|
+
return await deps.sendDraft(request.chatId, draftId, text, {
|
|
561
|
+
...getTelegramTargetThreadParams(
|
|
562
|
+
request.target ?? { chatId: request.chatId },
|
|
563
|
+
),
|
|
393
564
|
});
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
seal: () => sealTelegramPreviewState(state),
|
|
400
|
-
preparePublication: () => {
|
|
401
|
-
if (!state) return undefined;
|
|
402
|
-
sealTelegramPreviewState(state);
|
|
403
|
-
const prior =
|
|
404
|
-
state.publicationPromise ?? state.flushPromise ?? state.precedingFlush;
|
|
405
|
-
let settle!: () => void;
|
|
406
|
-
state.publicationPromise = new Promise<void>((resolve) => {
|
|
407
|
-
settle = resolve;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
567
|
+
phase,
|
|
568
|
+
chatId: request.chatId,
|
|
569
|
+
draftId,
|
|
408
570
|
});
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
const sendDraft = async (
|
|
575
|
+
effect: Extract<TelegramPreviewEffect, { type: "sendDraft" }>,
|
|
576
|
+
): Promise<void> => {
|
|
577
|
+
const delivered = await callTransport(
|
|
578
|
+
"draft",
|
|
579
|
+
effect.request,
|
|
580
|
+
effect.draftId,
|
|
581
|
+
normalizeTelegramNativeMarkdown(effect.text),
|
|
582
|
+
);
|
|
583
|
+
dispatch({
|
|
584
|
+
type: "draftSettled",
|
|
585
|
+
segmentId: effect.segmentId,
|
|
586
|
+
generation: effect.generation,
|
|
587
|
+
...(delivered === false ? {} : { text: effect.text }),
|
|
588
|
+
...clock(),
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
const run = (effect: TelegramPreviewEffect): void => {
|
|
592
|
+
switch (effect.type) {
|
|
593
|
+
case "sendDraft":
|
|
594
|
+
void sendDraft(effect);
|
|
595
|
+
return;
|
|
596
|
+
case "armTimer":
|
|
597
|
+
timer = setTimeout(() => {
|
|
598
|
+
timer = undefined;
|
|
599
|
+
dispatch({
|
|
600
|
+
type: "timerFired",
|
|
601
|
+
request: effect.request,
|
|
602
|
+
segmentId: effect.segmentId,
|
|
603
|
+
...clock(),
|
|
604
|
+
});
|
|
605
|
+
}, effect.delay);
|
|
606
|
+
timer.unref?.();
|
|
607
|
+
return;
|
|
608
|
+
case "cancelTimer":
|
|
609
|
+
if (timer) clearTimeout(timer);
|
|
610
|
+
timer = undefined;
|
|
611
|
+
return;
|
|
612
|
+
case "clearClosedGate":
|
|
613
|
+
void clear(effect.request.chatId, {
|
|
614
|
+
awaitFlush: false,
|
|
615
|
+
target: effect.request.target,
|
|
616
|
+
});
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const seal = (): void => {
|
|
622
|
+
dispatch({ type: "sealed" });
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
const clear = async (
|
|
626
|
+
chatId: number,
|
|
627
|
+
options: TelegramPreviewTargetOptions & {
|
|
628
|
+
awaitFlush?: boolean;
|
|
629
|
+
isDeliveryActive?: () => boolean;
|
|
630
|
+
} = {},
|
|
631
|
+
generation = machine.state.generation,
|
|
632
|
+
): Promise<void> => {
|
|
633
|
+
const segment = machine.state.segment;
|
|
634
|
+
if (!segment || options.isDeliveryActive?.() === false) return;
|
|
635
|
+
seal();
|
|
636
|
+
if (machine.state.inFlight !== null && options.awaitFlush !== false) {
|
|
637
|
+
await whenBoundarySettles(false);
|
|
638
|
+
if (machine.state.segment?.id !== segment.id) return;
|
|
639
|
+
}
|
|
640
|
+
if (options.isDeliveryActive?.() === false) return;
|
|
641
|
+
const issued = machine.state.segment ?? segment;
|
|
642
|
+
dispatch({ type: "discarded" });
|
|
643
|
+
if (issued.draftId !== undefined && canSendFor(generation))
|
|
644
|
+
await callTransport(
|
|
645
|
+
"clear-draft",
|
|
646
|
+
{ chatId, target: options.target },
|
|
647
|
+
issued.draftId,
|
|
648
|
+
undefined,
|
|
649
|
+
);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const flush = async (
|
|
653
|
+
chatId: number,
|
|
654
|
+
options: TelegramPreviewTargetOptions = {},
|
|
655
|
+
): Promise<void> => {
|
|
656
|
+
if (!machine.state.segment || machine.state.segment.sealed) return;
|
|
657
|
+
dispatch({
|
|
658
|
+
type: "flushRequested",
|
|
659
|
+
request: { chatId, target: options.target },
|
|
660
|
+
...clock(),
|
|
661
|
+
});
|
|
662
|
+
await whenIdle();
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
const prepareFinalizer = <TReplyMarkup>(input: {
|
|
666
|
+
sendMarkdownReply: TelegramAssistantPreviewRuntimeDeps<
|
|
667
|
+
unknown,
|
|
668
|
+
TReplyMarkup
|
|
669
|
+
>["sendMarkdownReply"];
|
|
670
|
+
isDeliveryActive?: () => boolean;
|
|
671
|
+
}): TelegramMarkdownFinalizer<TReplyMarkup> => {
|
|
672
|
+
const segmentId = machine.state.segment?.id;
|
|
673
|
+
const generation = machine.state.generation;
|
|
674
|
+
seal();
|
|
675
|
+
const predecessors = whenBoundarySettles();
|
|
676
|
+
return async (chatId, markdown, replyToMessageId, options) => {
|
|
677
|
+
if (input.isDeliveryActive?.() === false) return false;
|
|
678
|
+
await predecessors;
|
|
679
|
+
if (
|
|
680
|
+
machine.state.generation !== generation ||
|
|
681
|
+
machine.state.segment?.id !== segmentId ||
|
|
682
|
+
input.isDeliveryActive?.() === false
|
|
683
|
+
)
|
|
684
|
+
return false;
|
|
685
|
+
await input.sendMarkdownReply(
|
|
686
|
+
chatId,
|
|
687
|
+
replyToMessageId,
|
|
688
|
+
markdown,
|
|
689
|
+
options,
|
|
690
|
+
);
|
|
691
|
+
if (
|
|
692
|
+
machine.state.segment?.id === segmentId &&
|
|
693
|
+
input.isDeliveryActive?.() !== false
|
|
694
|
+
)
|
|
695
|
+
dispatch({ type: "discarded" });
|
|
696
|
+
return true;
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const controller: TelegramPreviewController = {
|
|
701
|
+
getState: () => machine.state.segment ?? undefined,
|
|
702
|
+
isFlushing: () => isTelegramPreviewBusy(machine.state),
|
|
703
|
+
seal,
|
|
704
|
+
setPendingText: (text) => dispatch({ type: "textSet", text }),
|
|
705
|
+
resetState: () => dispatch({ type: "reset" }),
|
|
706
|
+
invalidate: () => dispatch({ type: "invalidated" }),
|
|
707
|
+
preparePublication: () => {
|
|
708
|
+
if (!machine.state.segment) return undefined;
|
|
709
|
+
seal();
|
|
710
|
+
const id = (publicationIds += 1);
|
|
711
|
+
const prior = whenBoundarySettles();
|
|
712
|
+
dispatch({ type: "publicationOpened", id });
|
|
713
|
+
let settled = false;
|
|
409
714
|
return {
|
|
410
|
-
wait:
|
|
411
|
-
|
|
715
|
+
wait: () => prior,
|
|
716
|
+
settle: () => {
|
|
717
|
+
if (settled) return;
|
|
718
|
+
settled = true;
|
|
719
|
+
dispatch({ type: "publicationSettled", id, ...clock() });
|
|
412
720
|
},
|
|
413
|
-
settle,
|
|
414
721
|
};
|
|
415
722
|
},
|
|
416
723
|
prepareClear: (chatId, options) => {
|
|
417
|
-
const
|
|
418
|
-
const
|
|
724
|
+
const admitted = machine.state.segment?.id;
|
|
725
|
+
const generation = machine.state.generation;
|
|
419
726
|
return async () => {
|
|
420
|
-
if (state !==
|
|
421
|
-
await
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
727
|
+
if (machine.state.segment?.id !== admitted) return;
|
|
728
|
+
await clear(
|
|
729
|
+
chatId,
|
|
730
|
+
{
|
|
731
|
+
...options,
|
|
732
|
+
isDeliveryActive: () =>
|
|
733
|
+
canSendFor(generation) && options?.isDeliveryActive?.() !== false,
|
|
734
|
+
},
|
|
735
|
+
generation,
|
|
736
|
+
);
|
|
427
737
|
};
|
|
428
738
|
},
|
|
429
|
-
clear: (chatId, options) =>
|
|
430
|
-
|
|
431
|
-
flush: (chatId, options) =>
|
|
432
|
-
flushTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
739
|
+
clear: (chatId, options) => clear(chatId, options),
|
|
740
|
+
flush,
|
|
433
741
|
scheduleFlush: (chatId, options) => {
|
|
434
|
-
if (!state) return;
|
|
435
|
-
void
|
|
742
|
+
if (!machine.state.segment) return;
|
|
743
|
+
void flush(chatId, options);
|
|
744
|
+
},
|
|
745
|
+
finalize: async (chatId, _replyToMessageId, options) => {
|
|
746
|
+
const segment = machine.state.segment;
|
|
747
|
+
if (!segment) return false;
|
|
748
|
+
if (!canSendFor(machine.state.generation)) {
|
|
749
|
+
await clear(chatId, options);
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
await flush(chatId, options);
|
|
753
|
+
const current = machine.state.segment;
|
|
754
|
+
if (current?.id !== segment.id) return false;
|
|
755
|
+
const finalText = buildTelegramPreviewFinalText(current);
|
|
756
|
+
if (!finalText) {
|
|
757
|
+
await clear(chatId, options);
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
dispatch({ type: "discarded" });
|
|
761
|
+
return false;
|
|
436
762
|
},
|
|
437
|
-
finalize: (chatId, _replyToMessageId, options) =>
|
|
438
|
-
finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
|
|
439
763
|
};
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function createTelegramAssistantMessagePreviewHooks<TMessage>(
|
|
443
|
-
deps: TelegramAssistantMessagePreviewHookDeps<TMessage>,
|
|
444
|
-
): TelegramAssistantMessagePreviewHooks<TMessage> {
|
|
445
764
|
return {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
):
|
|
449
|
-
|
|
765
|
+
controller,
|
|
766
|
+
prepareFinalizer,
|
|
767
|
+
startSegment: (): void => {
|
|
768
|
+
dispatch({ type: "segmentStarted" });
|
|
769
|
+
},
|
|
770
|
+
ensureSegment: (): TelegramPreviewSegment => {
|
|
771
|
+
dispatch({ type: "segmentEnsured" });
|
|
772
|
+
return machine.state.segment!;
|
|
450
773
|
},
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
774
|
+
openDraftWindow: (at: number): void => {
|
|
775
|
+
dispatch({ type: "draftWindowOpened", at });
|
|
776
|
+
},
|
|
777
|
+
discard: (): void => {
|
|
778
|
+
dispatch({ type: "discarded" });
|
|
455
779
|
},
|
|
456
780
|
};
|
|
457
781
|
}
|
|
458
782
|
|
|
783
|
+
export function createTelegramPreviewController(
|
|
784
|
+
deps: TelegramPreviewControllerDeps,
|
|
785
|
+
): TelegramPreviewController {
|
|
786
|
+
return createTelegramPreviewModel(deps).controller;
|
|
787
|
+
}
|
|
788
|
+
|
|
459
789
|
/**
|
|
460
790
|
* Returns true when the active turn is a Telegram Guest Mode query. A guest
|
|
461
791
|
* query allows exactly one answer within a limited Telegram response window,
|
|
@@ -467,72 +797,84 @@ export function shouldSuppressPreviewForGuestTurn(
|
|
|
467
797
|
return !!turn?.guestQueryId;
|
|
468
798
|
}
|
|
469
799
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
800
|
+
export function createTelegramAssistantPreviewRuntime<
|
|
801
|
+
TMessage,
|
|
802
|
+
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
803
|
+
>(
|
|
804
|
+
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
|
|
805
|
+
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
|
|
806
|
+
const model = createTelegramPreviewModel(deps);
|
|
807
|
+
const { controller } = model;
|
|
808
|
+
const suppressed = (turn: TelegramPreviewActiveTurn): boolean =>
|
|
809
|
+
shouldSuppressPreviewForVoice(turn) ||
|
|
810
|
+
shouldSuppressPreviewForGuestTurn(turn);
|
|
811
|
+
return {
|
|
812
|
+
...controller,
|
|
813
|
+
finalizeMarkdown: (...args) =>
|
|
814
|
+
model.prepareFinalizer<TReplyMarkup>({
|
|
815
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
816
|
+
})(...args),
|
|
817
|
+
prepareDelivery(isDeliveryActive) {
|
|
818
|
+
const admitted = controller.getState()?.id;
|
|
819
|
+
return {
|
|
820
|
+
setPreviewPendingText(text) {
|
|
821
|
+
if (controller.getState()?.id === admitted && isDeliveryActive())
|
|
822
|
+
controller.setPendingText(text);
|
|
823
|
+
},
|
|
824
|
+
clearPreview: async (chatId, options) => {
|
|
825
|
+
if (controller.getState()?.id !== admitted || !isDeliveryActive())
|
|
826
|
+
return;
|
|
827
|
+
await controller.prepareClear(chatId, {
|
|
828
|
+
...options,
|
|
829
|
+
isDeliveryActive,
|
|
830
|
+
})();
|
|
831
|
+
},
|
|
832
|
+
finalizeMarkdownPreview: model.prepareFinalizer<TReplyMarkup>({
|
|
833
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
834
|
+
isDeliveryActive,
|
|
835
|
+
}),
|
|
836
|
+
};
|
|
837
|
+
},
|
|
838
|
+
onMessageStart: async ({ message }) => {
|
|
839
|
+
const turn = deps.getActiveTurn();
|
|
840
|
+
if (!turn || !deps.isAssistantMessage(message)) return;
|
|
841
|
+
if ((deps.canSend && !deps.canSend()) || suppressed(turn)) {
|
|
842
|
+
model.discard();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
model.startSegment();
|
|
846
|
+
},
|
|
847
|
+
onMessageUpdate: async ({ message }) => {
|
|
848
|
+
const turn = deps.getActiveTurn();
|
|
849
|
+
if (!turn || !deps.isAssistantMessage(message)) return;
|
|
850
|
+
if (deps.canSend && !deps.canSend()) {
|
|
851
|
+
model.discard();
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (suppressed(turn)) return;
|
|
855
|
+
const segment = model.ensureSegment();
|
|
856
|
+
if (segment.sealed) return;
|
|
857
|
+
const hadVisibleText = segment.pendingText.length > 0;
|
|
858
|
+
const pendingText = stripTelegramCommentMarkupForPreview(
|
|
859
|
+
deps.getMessageText(message),
|
|
860
|
+
);
|
|
861
|
+
controller.setPendingText(pendingText);
|
|
862
|
+
// The first visible text of a preview segment opens an initial accumulation
|
|
863
|
+
// window, so the segment's first frame cannot ship as a single word even
|
|
864
|
+
// when the previous cadence boundary has already passed (fresh turn, slow
|
|
865
|
+
// first token, or message rollover after tool work). Later deltas keep the
|
|
866
|
+
// trailing deadline instead of sliding it on every update.
|
|
867
|
+
const interval = deps.minDraftIntervalMs ?? TELEGRAM_DRAFT_INTERVAL_MS;
|
|
868
|
+
if (
|
|
869
|
+
interval > 0 &&
|
|
870
|
+
!hadVisibleText &&
|
|
871
|
+
!segment.lastSentText &&
|
|
872
|
+
pendingText.length > 0
|
|
873
|
+
)
|
|
874
|
+
model.openDraftWindow(Date.now() + interval);
|
|
875
|
+
controller.scheduleFlush(turn.chatId, { target: turn.target });
|
|
876
|
+
},
|
|
877
|
+
};
|
|
536
878
|
}
|
|
537
879
|
|
|
538
880
|
export function buildTelegramPreviewFinalText(
|
|
@@ -543,14 +885,6 @@ export function buildTelegramPreviewFinalText(
|
|
|
543
885
|
return state.lastSentText.trim() || undefined;
|
|
544
886
|
}
|
|
545
887
|
|
|
546
|
-
export function createTelegramPreviewRuntimeState(): TelegramPreviewRuntimeState {
|
|
547
|
-
return {
|
|
548
|
-
mode: "draft",
|
|
549
|
-
pendingText: "",
|
|
550
|
-
lastSentText: "",
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
|
|
554
888
|
export function allocateTelegramDraftId(
|
|
555
889
|
currentDraftId: number,
|
|
556
890
|
maxDraftId: number,
|
|
@@ -558,56 +892,6 @@ export function allocateTelegramDraftId(
|
|
|
558
892
|
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
|
559
893
|
}
|
|
560
894
|
|
|
561
|
-
interface TelegramNativeMarkdownPreviewSnapshot {
|
|
562
|
-
text: string;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
export function shouldUseTelegramDraftPreview(_options: {
|
|
566
|
-
draftSupport: TelegramDraftSupport;
|
|
567
|
-
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
|
|
568
|
-
}): boolean {
|
|
569
|
-
return true;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
export async function clearTelegramPreview(
|
|
573
|
-
chatId: number,
|
|
574
|
-
deps: TelegramPreviewRuntimeDeps,
|
|
575
|
-
options: {
|
|
576
|
-
awaitFlush?: boolean;
|
|
577
|
-
target?: TelegramTarget;
|
|
578
|
-
isDeliveryActive?: () => boolean;
|
|
579
|
-
} = {},
|
|
580
|
-
): Promise<void> {
|
|
581
|
-
const state = deps.getState();
|
|
582
|
-
if (!state || options.isDeliveryActive?.() === false) return;
|
|
583
|
-
sealTelegramPreviewState(state);
|
|
584
|
-
const inFlight = state.flushPromise ?? state.precedingFlush;
|
|
585
|
-
if (inFlight && options.awaitFlush !== false) {
|
|
586
|
-
state.flushRequested = false;
|
|
587
|
-
await inFlight.catch(() => {});
|
|
588
|
-
if (deps.getState() !== state) return;
|
|
589
|
-
}
|
|
590
|
-
if (options.isDeliveryActive?.() === false) return;
|
|
591
|
-
deps.setState(undefined);
|
|
592
|
-
if (
|
|
593
|
-
state.mode === "draft" &&
|
|
594
|
-
state.draftId !== undefined &&
|
|
595
|
-
deps.canSend?.() !== false
|
|
596
|
-
) {
|
|
597
|
-
try {
|
|
598
|
-
await deps.sendDraft(chatId, state.draftId, undefined, {
|
|
599
|
-
...getTelegramTargetThreadParams(options.target ?? { chatId }),
|
|
600
|
-
});
|
|
601
|
-
} catch (error) {
|
|
602
|
-
deps.recordRuntimeEvent?.("preview", error, {
|
|
603
|
-
phase: "clear-draft",
|
|
604
|
-
chatId,
|
|
605
|
-
draftId: state.draftId,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
895
|
interface TelegramDraftInlineState {
|
|
612
896
|
codeTicks: number;
|
|
613
897
|
htmlComment: boolean;
|
|
@@ -852,7 +1136,7 @@ export function getSafeTelegramRichMarkdownDraftPrefix(
|
|
|
852
1136
|
function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
853
1137
|
state: TelegramPreviewState,
|
|
854
1138
|
maxMessageLength: number,
|
|
855
|
-
):
|
|
1139
|
+
): { text: string } | undefined {
|
|
856
1140
|
const safeText = getSafeTelegramRichMarkdownDraftPrefix(
|
|
857
1141
|
state.pendingText,
|
|
858
1142
|
maxMessageLength,
|
|
@@ -860,138 +1144,3 @@ function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
|
860
1144
|
if (!safeText || safeText === state.lastSentText) return undefined;
|
|
861
1145
|
return { text: safeText };
|
|
862
1146
|
}
|
|
863
|
-
|
|
864
|
-
async function performTelegramPreviewFlush(
|
|
865
|
-
chatId: number,
|
|
866
|
-
state: TelegramPreviewRuntimeState,
|
|
867
|
-
deps: TelegramPreviewRuntimeDeps,
|
|
868
|
-
options: { target?: TelegramTarget } = {},
|
|
869
|
-
): Promise<void> {
|
|
870
|
-
if (deps.canSend && !deps.canSend()) {
|
|
871
|
-
await clearTelegramPreview(chatId, deps, {
|
|
872
|
-
awaitFlush: false,
|
|
873
|
-
target: options.target,
|
|
874
|
-
});
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
878
|
-
state,
|
|
879
|
-
deps.maxMessageLength,
|
|
880
|
-
);
|
|
881
|
-
if (!snapshot) return;
|
|
882
|
-
if (
|
|
883
|
-
shouldUseTelegramDraftPreview({
|
|
884
|
-
draftSupport: deps.getDraftSupport(),
|
|
885
|
-
snapshot,
|
|
886
|
-
})
|
|
887
|
-
) {
|
|
888
|
-
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
889
|
-
state.draftId = draftId;
|
|
890
|
-
state.nextDraftAt = Date.now() + (deps.minDraftIntervalMs ?? 0);
|
|
891
|
-
try {
|
|
892
|
-
const delivered = await deps.sendDraft(
|
|
893
|
-
chatId,
|
|
894
|
-
draftId,
|
|
895
|
-
normalizeTelegramNativeMarkdown(snapshot.text),
|
|
896
|
-
{ ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
|
|
897
|
-
);
|
|
898
|
-
if (
|
|
899
|
-
delivered === false ||
|
|
900
|
-
deps.getState() !== state ||
|
|
901
|
-
deps.canSend?.() === false
|
|
902
|
-
)
|
|
903
|
-
return;
|
|
904
|
-
deps.setDraftSupport("supported");
|
|
905
|
-
state.mode = "draft";
|
|
906
|
-
state.lastSentText = snapshot.text;
|
|
907
|
-
return;
|
|
908
|
-
} catch (error) {
|
|
909
|
-
deps.recordRuntimeEvent?.("preview", error, {
|
|
910
|
-
phase: "draft",
|
|
911
|
-
chatId,
|
|
912
|
-
draftId,
|
|
913
|
-
});
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
export async function flushTelegramPreview(
|
|
920
|
-
chatId: number,
|
|
921
|
-
deps: TelegramPreviewRuntimeDeps,
|
|
922
|
-
options: { target?: TelegramTarget } = {},
|
|
923
|
-
): Promise<void> {
|
|
924
|
-
const state = deps.getState();
|
|
925
|
-
if (!state || state.sealed) return;
|
|
926
|
-
if (state.flushPromise) {
|
|
927
|
-
state.flushRequested = true;
|
|
928
|
-
await state.flushPromise;
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
state.flushPromise = (async () => {
|
|
932
|
-
if (state.precedingFlush) {
|
|
933
|
-
await state.precedingFlush.catch(() => {});
|
|
934
|
-
state.precedingFlush = undefined;
|
|
935
|
-
if (deps.getState() !== state || state.sealed) return;
|
|
936
|
-
}
|
|
937
|
-
do {
|
|
938
|
-
state.flushRequested = false;
|
|
939
|
-
const delay = (state.nextDraftAt ?? 0) - Date.now();
|
|
940
|
-
if (delay > 0) {
|
|
941
|
-
if (!state.flushTimer) {
|
|
942
|
-
state.flushTimer = setTimeout(() => {
|
|
943
|
-
state.flushTimer = undefined;
|
|
944
|
-
if (deps.getState() === state && !state.sealed)
|
|
945
|
-
void flushTelegramPreview(chatId, deps, options);
|
|
946
|
-
}, delay);
|
|
947
|
-
state.flushTimer.unref?.();
|
|
948
|
-
}
|
|
949
|
-
break;
|
|
950
|
-
}
|
|
951
|
-
if (state.flushTimer) clearTimeout(state.flushTimer);
|
|
952
|
-
state.flushTimer = undefined;
|
|
953
|
-
try {
|
|
954
|
-
await performTelegramPreviewFlush(chatId, state, deps, options);
|
|
955
|
-
} catch (error) {
|
|
956
|
-
deps.recordRuntimeEvent?.("preview", error, {
|
|
957
|
-
phase: "flush",
|
|
958
|
-
chatId,
|
|
959
|
-
draftId: state.draftId,
|
|
960
|
-
});
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
} while (
|
|
964
|
-
deps.getState() === state &&
|
|
965
|
-
!state.sealed &&
|
|
966
|
-
state.flushRequested
|
|
967
|
-
);
|
|
968
|
-
})();
|
|
969
|
-
try {
|
|
970
|
-
await state.flushPromise;
|
|
971
|
-
} finally {
|
|
972
|
-
if (deps.getState() === state) {
|
|
973
|
-
state.flushPromise = undefined;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
export async function finalizeTelegramPreview(
|
|
979
|
-
chatId: number,
|
|
980
|
-
deps: TelegramPreviewRuntimeDeps,
|
|
981
|
-
options: { target?: TelegramTarget } = {},
|
|
982
|
-
): Promise<boolean> {
|
|
983
|
-
const state = deps.getState();
|
|
984
|
-
if (!state) return false;
|
|
985
|
-
if (deps.canSend && !deps.canSend()) {
|
|
986
|
-
await clearTelegramPreview(chatId, deps, options);
|
|
987
|
-
return false;
|
|
988
|
-
}
|
|
989
|
-
await flushTelegramPreview(chatId, deps, options);
|
|
990
|
-
const finalText = buildTelegramPreviewFinalText(state);
|
|
991
|
-
if (!finalText) {
|
|
992
|
-
await clearTelegramPreview(chatId, deps, options);
|
|
993
|
-
return false;
|
|
994
|
-
}
|
|
995
|
-
deps.setState(undefined);
|
|
996
|
-
return false;
|
|
997
|
-
}
|