@llblab/pi-telegram 0.27.12 → 0.28.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 +150 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +393 -443
- package/README.md +9 -6
- package/api/updates.ts +5 -0
- package/docs/architecture.md +71 -26
- package/docs/multi-instance-bus.md +23 -5
- package/docs/public-api.md +7 -6
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +356 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -2
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +4 -2
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +17 -0
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +3 -3
- package/scripts/check-downgrade.mjs +435 -0
package/lib/polling.ts
CHANGED
|
@@ -22,11 +22,11 @@ const TELEGRAM_LONG_POLL_LIMIT = 10;
|
|
|
22
22
|
const TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30;
|
|
23
23
|
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS = 2_500;
|
|
24
24
|
const TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES = 2;
|
|
25
|
-
const TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES = 3;
|
|
26
25
|
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
|
|
27
26
|
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
28
27
|
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
29
28
|
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
29
|
+
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
30
30
|
|
|
31
31
|
// Standard Telegram DM polling does not expose ordinary message-deletion events,
|
|
32
32
|
// so queue removal stays reaction-driven while delete-like business updates remain defensive-only.
|
|
@@ -65,11 +65,38 @@ export function buildTelegramLongPollRequest(lastUpdateId?: number): {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
export function getLatestTelegramUpdateId(
|
|
68
|
-
updates: TelegramUpdate[],
|
|
68
|
+
updates: readonly TelegramUpdate[],
|
|
69
69
|
): number | undefined {
|
|
70
70
|
return updates.at(-1)?.update_id;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export class TelegramGetUpdatesTimeoutError extends Error {
|
|
74
|
+
readonly timeoutMs: number;
|
|
75
|
+
|
|
76
|
+
constructor(timeoutMs: number) {
|
|
77
|
+
super(`Telegram getUpdates timed out after ${timeoutMs} ms.`);
|
|
78
|
+
this.name = "TelegramGetUpdatesTimeoutError";
|
|
79
|
+
this.timeoutMs = timeoutMs;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function getTelegramGetUpdatesRequestBudgetMs(
|
|
84
|
+
body: Record<string, unknown>,
|
|
85
|
+
graceMs = TELEGRAM_GET_UPDATES_GRACE_MS,
|
|
86
|
+
): number {
|
|
87
|
+
const timeoutSeconds =
|
|
88
|
+
typeof body.timeout === "number" &&
|
|
89
|
+
Number.isFinite(body.timeout) &&
|
|
90
|
+
body.timeout >= 0
|
|
91
|
+
? body.timeout
|
|
92
|
+
: 0;
|
|
93
|
+
const normalizedGraceMs =
|
|
94
|
+
Number.isFinite(graceMs) && graceMs > 0
|
|
95
|
+
? Math.floor(graceMs)
|
|
96
|
+
: TELEGRAM_GET_UPDATES_GRACE_MS;
|
|
97
|
+
return Math.floor(timeoutSeconds * 1_000) + normalizedGraceMs;
|
|
98
|
+
}
|
|
99
|
+
|
|
73
100
|
export function shouldStopTelegramPolling(
|
|
74
101
|
signalAborted: boolean,
|
|
75
102
|
error: unknown,
|
|
@@ -85,13 +112,67 @@ export interface TelegramPollingStartState {
|
|
|
85
112
|
hasPollingPromise: boolean;
|
|
86
113
|
}
|
|
87
114
|
|
|
88
|
-
export
|
|
115
|
+
export type TelegramPollingWorkPhase =
|
|
116
|
+
| "long-poll"
|
|
117
|
+
| "persisting-journal"
|
|
118
|
+
| "persisting-offset"
|
|
119
|
+
| "retrying";
|
|
120
|
+
|
|
121
|
+
export type TelegramPollingPhase =
|
|
122
|
+
| "stopped"
|
|
123
|
+
| "starting"
|
|
124
|
+
| TelegramPollingWorkPhase;
|
|
125
|
+
|
|
126
|
+
export type TelegramPollingStopReason =
|
|
127
|
+
| "not-started"
|
|
128
|
+
| "requested"
|
|
129
|
+
| "completed"
|
|
130
|
+
| "failed";
|
|
131
|
+
|
|
132
|
+
export interface TelegramPollingStateSnapshot {
|
|
133
|
+
phase: TelegramPollingPhase;
|
|
134
|
+
phaseStartedAtMs?: number;
|
|
135
|
+
currentUpdateId?: number;
|
|
136
|
+
startedAtMs?: number;
|
|
137
|
+
stoppedAtMs?: number;
|
|
138
|
+
lastSuccessfulResponseAtMs?: number;
|
|
139
|
+
lastSuccessfulResponseUpdateCount?: number;
|
|
140
|
+
stopReason?: TelegramPollingStopReason;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface TelegramPollingControllerState
|
|
144
|
+
extends TelegramPollingStateSnapshot {
|
|
89
145
|
pollingPromise?: Promise<void>;
|
|
90
146
|
pollingController?: AbortController;
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
export function createTelegramPollingControllerState(): TelegramPollingControllerState {
|
|
94
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
phase: "stopped",
|
|
152
|
+
stopReason: "not-started",
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getTelegramPollingStateSnapshot(
|
|
157
|
+
state: TelegramPollingControllerState,
|
|
158
|
+
): TelegramPollingStateSnapshot {
|
|
159
|
+
return {
|
|
160
|
+
phase: state.phase,
|
|
161
|
+
phaseStartedAtMs: state.phaseStartedAtMs,
|
|
162
|
+
currentUpdateId: state.currentUpdateId,
|
|
163
|
+
startedAtMs: state.startedAtMs,
|
|
164
|
+
stoppedAtMs: state.stoppedAtMs,
|
|
165
|
+
lastSuccessfulResponseAtMs: state.lastSuccessfulResponseAtMs,
|
|
166
|
+
lastSuccessfulResponseUpdateCount:
|
|
167
|
+
state.lastSuccessfulResponseUpdateCount,
|
|
168
|
+
stopReason: state.stopReason,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createTelegramPollingStateReader(
|
|
173
|
+
state: TelegramPollingControllerState,
|
|
174
|
+
): () => TelegramPollingStateSnapshot {
|
|
175
|
+
return () => getTelegramPollingStateSnapshot(state);
|
|
95
176
|
}
|
|
96
177
|
|
|
97
178
|
export function isTelegramPollingControllerActive(
|
|
@@ -118,6 +199,10 @@ export interface TelegramPollingRuntimeDeps<
|
|
|
118
199
|
runPollLoop: (ctx: TContext, signal: AbortSignal) => Promise<void>;
|
|
119
200
|
updateStatus: (ctx: TContext, message?: string) => void;
|
|
120
201
|
createAbortController?: () => AbortController;
|
|
202
|
+
getNowMs?: () => number;
|
|
203
|
+
onPollingStateChange?: () => void;
|
|
204
|
+
onPollingStarted?: () => void;
|
|
205
|
+
onPollingStopped?: (reason: TelegramPollingStopReason) => void;
|
|
121
206
|
}
|
|
122
207
|
|
|
123
208
|
export type TelegramPollingControllerDeps<TContext> = Omit<
|
|
@@ -134,14 +219,117 @@ export interface TelegramPollingController<TContext> {
|
|
|
134
219
|
stop: () => Promise<void>;
|
|
135
220
|
}
|
|
136
221
|
|
|
137
|
-
export interface
|
|
222
|
+
export interface TelegramPollingAdmissionRuntime<TContext> {
|
|
223
|
+
isActive: () => boolean;
|
|
224
|
+
start: (ctx: TContext) => Promise<void>;
|
|
225
|
+
stop: () => Promise<void>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function createTelegramPollingAdmissionRuntime<TContext>(deps: {
|
|
229
|
+
polling: TelegramPollingController<TContext>;
|
|
230
|
+
validateStart?: () => void;
|
|
231
|
+
worker: {
|
|
232
|
+
onSessionStart: (ctx: TContext) => Promise<void>;
|
|
233
|
+
};
|
|
234
|
+
}): TelegramPollingAdmissionRuntime<TContext> {
|
|
235
|
+
return {
|
|
236
|
+
isActive: deps.polling.isActive,
|
|
237
|
+
async start(ctx) {
|
|
238
|
+
deps.validateStart?.();
|
|
239
|
+
await deps.worker.onSessionStart(ctx);
|
|
240
|
+
await deps.polling.start(ctx);
|
|
241
|
+
},
|
|
242
|
+
stop: deps.polling.stop,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface TelegramDurablePollingRuntimeAssembly<TContext> {
|
|
247
|
+
controller: TelegramPollingController<TContext>;
|
|
248
|
+
admission: TelegramPollingAdmissionRuntime<TContext>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export type TelegramDurablePollingRuntimeAssemblyDeps<
|
|
252
|
+
TUpdate extends TelegramUpdate,
|
|
253
|
+
TContext,
|
|
254
|
+
> = Omit<
|
|
255
|
+
TelegramPollingControllerRuntimeDeps<TUpdate, TContext>,
|
|
256
|
+
"appendUpdateBatch" | "getJournalEntryCount" | "signalUpdateWorker"
|
|
257
|
+
> & {
|
|
258
|
+
journal: {
|
|
259
|
+
appendBatch: (updates: readonly TUpdate[]) => MaybePromise<unknown>;
|
|
260
|
+
getEntryCount: () => number;
|
|
261
|
+
signalWorker: () => void;
|
|
262
|
+
getBootstrapEntryCount: () => number;
|
|
263
|
+
onSessionStart: (ctx: TContext) => Promise<void>;
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/** Own journal-first polling assembly and cursor bootstrap validation. */
|
|
268
|
+
export function createTelegramDurablePollingRuntimeAssembly<
|
|
269
|
+
TUpdate extends TelegramUpdate,
|
|
270
|
+
TContext,
|
|
271
|
+
>(
|
|
272
|
+
deps: TelegramDurablePollingRuntimeAssemblyDeps<TUpdate, TContext>,
|
|
273
|
+
): TelegramDurablePollingRuntimeAssembly<TContext> {
|
|
274
|
+
const controller = createTelegramPollingControllerRuntime({
|
|
275
|
+
...deps,
|
|
276
|
+
appendUpdateBatch: deps.journal.appendBatch,
|
|
277
|
+
getJournalEntryCount: deps.journal.getEntryCount,
|
|
278
|
+
signalUpdateWorker: deps.journal.signalWorker,
|
|
279
|
+
});
|
|
280
|
+
const admission = createTelegramPollingAdmissionRuntime({
|
|
281
|
+
polling: controller,
|
|
282
|
+
validateStart() {
|
|
283
|
+
if (deps.getConfig().lastUpdateId !== undefined) return;
|
|
284
|
+
if (deps.journal.getBootstrapEntryCount() === 0) return;
|
|
285
|
+
throw new TelegramPollingCursorBootstrapError(
|
|
286
|
+
"Telegram polling cursor is missing while the durable update journal is non-empty.",
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
worker: deps.journal,
|
|
290
|
+
});
|
|
291
|
+
return { controller, admission };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export type TelegramPollingControllerRuntimeDeps<
|
|
138
295
|
TUpdate extends TelegramUpdate,
|
|
139
296
|
TContext = unknown,
|
|
140
|
-
>
|
|
297
|
+
> = Omit<
|
|
298
|
+
TelegramPollLoopRunnerDeps<TUpdate, TContext>,
|
|
299
|
+
"onPhaseChange" | "onSuccessfulResponse"
|
|
300
|
+
> & {
|
|
141
301
|
state?: TelegramPollingControllerState;
|
|
142
302
|
hasBotToken: () => boolean;
|
|
143
303
|
stopTypingLoop: () => unknown;
|
|
144
304
|
createAbortController?: () => AbortController;
|
|
305
|
+
getNowMs?: () => number;
|
|
306
|
+
onPollingStateChange?: () => void;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
function notifyTelegramPollingStateChange(
|
|
310
|
+
deps: Pick<
|
|
311
|
+
TelegramPollingRuntimeDeps<unknown>,
|
|
312
|
+
"onPollingStateChange" | "recordRuntimeEvent"
|
|
313
|
+
>,
|
|
314
|
+
): void {
|
|
315
|
+
try {
|
|
316
|
+
deps.onPollingStateChange?.();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
319
|
+
phase: "state-observer",
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function transitionTelegramPollingState(
|
|
325
|
+
state: TelegramPollingControllerState,
|
|
326
|
+
phase: TelegramPollingPhase,
|
|
327
|
+
nowMs: number,
|
|
328
|
+
currentUpdateId?: number,
|
|
329
|
+
): void {
|
|
330
|
+
state.phase = phase;
|
|
331
|
+
state.phaseStartedAtMs = nowMs;
|
|
332
|
+
state.currentUpdateId = currentUpdateId;
|
|
145
333
|
}
|
|
146
334
|
|
|
147
335
|
export function createTelegramPollingControllerRuntime<
|
|
@@ -150,24 +338,49 @@ export function createTelegramPollingControllerRuntime<
|
|
|
150
338
|
>(
|
|
151
339
|
deps: TelegramPollingControllerRuntimeDeps<TUpdate, TContext>,
|
|
152
340
|
): TelegramPollingController<TContext> {
|
|
341
|
+
const state = deps.state ?? createTelegramPollingControllerState();
|
|
342
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
343
|
+
const notifyStateChange = () =>
|
|
344
|
+
notifyTelegramPollingStateChange({
|
|
345
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
346
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
347
|
+
});
|
|
153
348
|
return createTelegramPollingController({
|
|
154
|
-
state
|
|
349
|
+
state,
|
|
155
350
|
hasBotToken: deps.hasBotToken,
|
|
156
351
|
stopTypingLoop: deps.stopTypingLoop,
|
|
157
352
|
runPollLoop: createTelegramPollLoopRunner<TUpdate, TContext>({
|
|
158
353
|
getConfig: deps.getConfig,
|
|
159
354
|
deleteWebhook: deps.deleteWebhook,
|
|
160
355
|
getUpdates: deps.getUpdates,
|
|
356
|
+
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
161
357
|
persistConfig: deps.persistConfig,
|
|
162
|
-
|
|
358
|
+
appendUpdateBatch: deps.appendUpdateBatch,
|
|
359
|
+
getJournalEntryCount: deps.getJournalEntryCount,
|
|
360
|
+
signalUpdateWorker: deps.signalUpdateWorker,
|
|
163
361
|
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
164
362
|
updateStatus: deps.updateStatus,
|
|
165
363
|
sleep: deps.sleep,
|
|
166
|
-
|
|
364
|
+
onPhaseChange(phase, currentUpdateId) {
|
|
365
|
+
transitionTelegramPollingState(
|
|
366
|
+
state,
|
|
367
|
+
phase,
|
|
368
|
+
getNowMs(),
|
|
369
|
+
currentUpdateId,
|
|
370
|
+
);
|
|
371
|
+
notifyStateChange();
|
|
372
|
+
},
|
|
373
|
+
onSuccessfulResponse(updateCount) {
|
|
374
|
+
state.lastSuccessfulResponseAtMs = getNowMs();
|
|
375
|
+
state.lastSuccessfulResponseUpdateCount = updateCount;
|
|
376
|
+
notifyStateChange();
|
|
377
|
+
},
|
|
167
378
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
168
379
|
}),
|
|
169
380
|
updateStatus: deps.updateStatus,
|
|
170
381
|
createAbortController: deps.createAbortController,
|
|
382
|
+
getNowMs,
|
|
383
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
171
384
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
172
385
|
});
|
|
173
386
|
}
|
|
@@ -176,6 +389,12 @@ export function createTelegramPollingController<TContext>(
|
|
|
176
389
|
deps: TelegramPollingControllerDeps<TContext>,
|
|
177
390
|
): TelegramPollingController<TContext> {
|
|
178
391
|
const state = deps.state ?? createTelegramPollingControllerState();
|
|
392
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
393
|
+
const notifyStateChange = () =>
|
|
394
|
+
notifyTelegramPollingStateChange({
|
|
395
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
396
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
397
|
+
});
|
|
179
398
|
const runtimeDeps: TelegramPollingRuntimeDeps<TContext> = {
|
|
180
399
|
...deps,
|
|
181
400
|
getPollingPromise: () => state.pollingPromise,
|
|
@@ -186,6 +405,25 @@ export function createTelegramPollingController<TContext>(
|
|
|
186
405
|
setPollingController: (controller) => {
|
|
187
406
|
state.pollingController = controller;
|
|
188
407
|
},
|
|
408
|
+
onPollingStarted: () => {
|
|
409
|
+
const nowMs = getNowMs();
|
|
410
|
+
transitionTelegramPollingState(state, "starting", nowMs);
|
|
411
|
+
state.startedAtMs = nowMs;
|
|
412
|
+
state.stoppedAtMs = undefined;
|
|
413
|
+
state.lastSuccessfulResponseAtMs = undefined;
|
|
414
|
+
state.lastSuccessfulResponseUpdateCount = undefined;
|
|
415
|
+
state.stopReason = undefined;
|
|
416
|
+
notifyStateChange();
|
|
417
|
+
deps.onPollingStarted?.();
|
|
418
|
+
},
|
|
419
|
+
onPollingStopped: (reason) => {
|
|
420
|
+
const nowMs = getNowMs();
|
|
421
|
+
transitionTelegramPollingState(state, "stopped", nowMs);
|
|
422
|
+
state.stoppedAtMs = nowMs;
|
|
423
|
+
state.stopReason = reason;
|
|
424
|
+
notifyStateChange();
|
|
425
|
+
deps.onPollingStopped?.(reason);
|
|
426
|
+
},
|
|
189
427
|
};
|
|
190
428
|
return {
|
|
191
429
|
isActive: () => isTelegramPollingControllerActive(state),
|
|
@@ -212,12 +450,16 @@ export async function stopTelegramPollingRuntime<TContext>(
|
|
|
212
450
|
}
|
|
213
451
|
pollingController?.abort();
|
|
214
452
|
await pollingPromise?.catch(() => undefined);
|
|
453
|
+
let cleared = false;
|
|
215
454
|
if (deps.getPollingPromise() === pollingPromise) {
|
|
216
455
|
deps.setPollingPromise(undefined);
|
|
456
|
+
cleared = pollingPromise !== undefined;
|
|
217
457
|
}
|
|
218
458
|
if (deps.getPollingController() === pollingController) {
|
|
219
459
|
deps.setPollingController(undefined);
|
|
460
|
+
cleared = cleared || pollingController !== undefined;
|
|
220
461
|
}
|
|
462
|
+
if (cleared) deps.onPollingStopped?.("requested");
|
|
221
463
|
}
|
|
222
464
|
|
|
223
465
|
function updateTelegramPollingStatusSafely<TContext>(
|
|
@@ -250,16 +492,41 @@ export function startTelegramPollingRuntime<TContext>(
|
|
|
250
492
|
}
|
|
251
493
|
const controller = deps.createAbortController?.() ?? new AbortController();
|
|
252
494
|
deps.setPollingController(controller);
|
|
495
|
+
deps.onPollingStarted?.();
|
|
496
|
+
let failed = false;
|
|
497
|
+
let runPromise: Promise<void>;
|
|
498
|
+
try {
|
|
499
|
+
runPromise = deps.runPollLoop(ctx, controller.signal);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
runPromise = Promise.reject(error);
|
|
502
|
+
}
|
|
253
503
|
let promise: Promise<void>;
|
|
254
|
-
promise =
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
504
|
+
promise = runPromise
|
|
505
|
+
.catch((error) => {
|
|
506
|
+
if (shouldStopTelegramPolling(controller.signal.aborted, error)) return;
|
|
507
|
+
failed = true;
|
|
508
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
509
|
+
phase: "controller",
|
|
510
|
+
});
|
|
511
|
+
})
|
|
512
|
+
.finally(() => {
|
|
513
|
+
const ownsPromise = deps.getPollingPromise() === promise;
|
|
514
|
+
const ownsController = deps.getPollingController() === controller;
|
|
515
|
+
if (ownsPromise) deps.setPollingPromise(undefined);
|
|
516
|
+
if (ownsController) deps.setPollingController(undefined);
|
|
517
|
+
if (ownsPromise || ownsController) {
|
|
518
|
+
deps.onPollingStopped?.(
|
|
519
|
+
failed
|
|
520
|
+
? "failed"
|
|
521
|
+
: controller.signal.aborted
|
|
522
|
+
? "requested"
|
|
523
|
+
: "completed",
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
527
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
528
|
+
});
|
|
261
529
|
});
|
|
262
|
-
});
|
|
263
530
|
deps.setPollingPromise(promise);
|
|
264
531
|
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
265
532
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
@@ -290,6 +557,7 @@ export interface TelegramThreadCapabilityRecordView {
|
|
|
290
557
|
|
|
291
558
|
export interface TelegramThreadCapabilityStore {
|
|
292
559
|
load: () => Promise<void>;
|
|
560
|
+
refresh?: () => Promise<void>;
|
|
293
561
|
persist: () => Promise<void>;
|
|
294
562
|
getBotState: () => TelegramThreadCapabilityState;
|
|
295
563
|
setBotState: (state: TelegramThreadCapabilityState) => void;
|
|
@@ -320,6 +588,7 @@ export interface TelegramThreadCapabilityRuntimeDeps<
|
|
|
320
588
|
> extends TelegramThreadCapabilityReaderDeps {
|
|
321
589
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
322
590
|
ownsLock: (ctx: TContext) => boolean;
|
|
591
|
+
isFollowerRegistered?: () => boolean;
|
|
323
592
|
getPollingStartedWithTelegramBus: () => boolean;
|
|
324
593
|
setPollingStartedWithTelegramBus: (started: boolean) => void;
|
|
325
594
|
setTopicModeUnavailable: (unavailable: boolean) => void;
|
|
@@ -420,6 +689,7 @@ export interface TelegramThreadCapabilityOrchestrationDeps<
|
|
|
420
689
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
421
690
|
isBusRuntimeEnabled: () => boolean;
|
|
422
691
|
ownsLock: (ctx: TContext) => boolean;
|
|
692
|
+
isFollowerRegistered?: () => boolean;
|
|
423
693
|
startClassicPolling: (ctx: TContext) => MaybePromise<void>;
|
|
424
694
|
stopClassicPolling: () => Promise<void>;
|
|
425
695
|
startBusLeaderPolling: (ctx: TContext) => Promise<void>;
|
|
@@ -475,6 +745,7 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
|
475
745
|
callApi: deps.callApi,
|
|
476
746
|
topicTargetStore: deps.topicTargetStore,
|
|
477
747
|
ownsLock: deps.ownsLock,
|
|
748
|
+
isFollowerRegistered: deps.isFollowerRegistered,
|
|
478
749
|
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
479
750
|
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
480
751
|
setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
|
|
@@ -737,24 +1008,14 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
737
1008
|
ctx: TContext,
|
|
738
1009
|
owner: TOwner,
|
|
739
1010
|
): Promise<boolean | undefined> => {
|
|
740
|
-
|
|
1011
|
+
if (deps.topicTargetStore.refresh) {
|
|
1012
|
+
await deps.topicTargetStore.refresh();
|
|
1013
|
+
} else {
|
|
1014
|
+
await deps.topicTargetStore.load();
|
|
1015
|
+
}
|
|
741
1016
|
if (deps.topicTargetStore.getBotState().threadMode !== "enabled") {
|
|
742
|
-
if (hasTelegramThreadCapabilityBindings(deps.topicTargetStore)) {
|
|
743
|
-
deps.recordEvent(
|
|
744
|
-
"bus",
|
|
745
|
-
"Telegram Threaded Mode disabled; follower takeover blocked",
|
|
746
|
-
{
|
|
747
|
-
phase: "follower-register-thread-mode-disabled",
|
|
748
|
-
reason: "active-thread-bindings-present",
|
|
749
|
-
},
|
|
750
|
-
);
|
|
751
|
-
throw new Error(
|
|
752
|
-
"Telegram Threaded Mode is disabled; the current leader remains the classic polling owner.",
|
|
753
|
-
);
|
|
754
|
-
}
|
|
755
1017
|
return undefined;
|
|
756
1018
|
}
|
|
757
|
-
if (!deps.isBusRuntimeEnabled()) return undefined;
|
|
758
1019
|
return deps.registerFollowerWithLeader(ctx, owner);
|
|
759
1020
|
};
|
|
760
1021
|
return {
|
|
@@ -788,24 +1049,40 @@ export function createTelegramThreadTargetObservationHandler<TContext>(
|
|
|
788
1049
|
};
|
|
789
1050
|
}
|
|
790
1051
|
|
|
1052
|
+
export function canProbeTelegramThreadCapability<TContext>(
|
|
1053
|
+
ctx: TContext,
|
|
1054
|
+
deps: Pick<
|
|
1055
|
+
TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
1056
|
+
"ownsLock" | "isFollowerRegistered"
|
|
1057
|
+
>,
|
|
1058
|
+
): boolean {
|
|
1059
|
+
return deps.ownsLock(ctx) || deps.isFollowerRegistered?.() === true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
791
1062
|
export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
792
1063
|
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
793
1064
|
): TelegramThreadCapabilityMonitor<TContext> {
|
|
794
1065
|
const intervalMs =
|
|
795
1066
|
deps.intervalMs ?? TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS;
|
|
796
1067
|
let interval: ReturnType<typeof setInterval> | undefined;
|
|
797
|
-
let
|
|
1068
|
+
let generation = 0;
|
|
1069
|
+
let transitionPromise: Promise<void> | undefined;
|
|
798
1070
|
let consecutiveDisabledProbes = 0;
|
|
799
1071
|
const stop = (): void => {
|
|
800
|
-
|
|
801
|
-
clearInterval(interval);
|
|
1072
|
+
generation += 1;
|
|
1073
|
+
if (interval) clearInterval(interval);
|
|
802
1074
|
interval = undefined;
|
|
803
1075
|
};
|
|
804
1076
|
const check = (ctx: TContext): void => {
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
1077
|
+
if (transitionPromise || !canProbeTelegramThreadCapability(ctx, deps)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const expectedGeneration = generation;
|
|
1081
|
+
const isCurrent = (): boolean => generation === expectedGeneration;
|
|
1082
|
+
let tracked: Promise<void>;
|
|
1083
|
+
tracked = readTelegramThreadCapability(deps)
|
|
808
1084
|
.then(async (threadModeEnabled) => {
|
|
1085
|
+
if (!isCurrent()) return;
|
|
809
1086
|
if (threadModeEnabled === undefined) {
|
|
810
1087
|
if (
|
|
811
1088
|
deps.topicTargetStore.getBotState().threadMode !== "enabled" &&
|
|
@@ -870,11 +1147,17 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
870
1147
|
);
|
|
871
1148
|
})
|
|
872
1149
|
.catch((error) => {
|
|
873
|
-
|
|
1150
|
+
if (!isCurrent()) return;
|
|
1151
|
+
try {
|
|
1152
|
+
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
1153
|
+
} catch {
|
|
1154
|
+
// Monitor diagnostics cannot create an unhandled interval rejection.
|
|
1155
|
+
}
|
|
874
1156
|
})
|
|
875
1157
|
.finally(() => {
|
|
876
|
-
|
|
1158
|
+
if (transitionPromise === tracked) transitionPromise = undefined;
|
|
877
1159
|
});
|
|
1160
|
+
transitionPromise = tracked;
|
|
878
1161
|
};
|
|
879
1162
|
return {
|
|
880
1163
|
start(ctx) {
|
|
@@ -888,6 +1171,96 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
888
1171
|
};
|
|
889
1172
|
}
|
|
890
1173
|
|
|
1174
|
+
export class TelegramPollingBatchValidationError extends Error {
|
|
1175
|
+
constructor(message: string) {
|
|
1176
|
+
super(message);
|
|
1177
|
+
this.name = "TelegramPollingBatchValidationError";
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
export class TelegramPollingCursorBootstrapError extends Error {
|
|
1182
|
+
constructor(message: string) {
|
|
1183
|
+
super(message);
|
|
1184
|
+
this.name = "TelegramPollingCursorBootstrapError";
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export interface TelegramPollingBatchAdmissionResult {
|
|
1189
|
+
updateCount: number;
|
|
1190
|
+
latestUpdateId?: number;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
export interface TelegramPollingBatchAdmissionDeps<
|
|
1194
|
+
TUpdate extends TelegramUpdate,
|
|
1195
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
1196
|
+
updates: readonly TUpdate[];
|
|
1197
|
+
config: TelegramPollingConfig;
|
|
1198
|
+
appendBatch: (updates: readonly TUpdate[]) => MaybePromise<unknown>;
|
|
1199
|
+
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
1200
|
+
signalWorker: () => void;
|
|
1201
|
+
onPhaseChange?: (
|
|
1202
|
+
phase: TelegramPollingWorkPhase,
|
|
1203
|
+
currentUpdateId?: number,
|
|
1204
|
+
) => void;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function validateTelegramPollingBatch(
|
|
1208
|
+
updates: readonly TelegramUpdate[],
|
|
1209
|
+
lastUpdateId?: number,
|
|
1210
|
+
): void {
|
|
1211
|
+
let previousUpdateId = lastUpdateId;
|
|
1212
|
+
for (const update of updates) {
|
|
1213
|
+
if (
|
|
1214
|
+
!Number.isSafeInteger(update.update_id) ||
|
|
1215
|
+
update.update_id < 0 ||
|
|
1216
|
+
(previousUpdateId !== undefined && update.update_id <= previousUpdateId)
|
|
1217
|
+
) {
|
|
1218
|
+
throw new TelegramPollingBatchValidationError(
|
|
1219
|
+
`Telegram getUpdates returned non-monotonic update id ${String(update.update_id)} after ${String(previousUpdateId)}`,
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
previousUpdateId = update.update_id;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
export async function admitTelegramPollingUpdateBatch<
|
|
1227
|
+
TUpdate extends TelegramUpdate,
|
|
1228
|
+
>(
|
|
1229
|
+
deps: TelegramPollingBatchAdmissionDeps<TUpdate>,
|
|
1230
|
+
): Promise<TelegramPollingBatchAdmissionResult> {
|
|
1231
|
+
if (deps.updates.length === 0) return { updateCount: 0 };
|
|
1232
|
+
validateTelegramPollingBatch(deps.updates, deps.config.lastUpdateId);
|
|
1233
|
+
const latestUpdateId = getLatestTelegramUpdateId(deps.updates);
|
|
1234
|
+
if (latestUpdateId === undefined) return { updateCount: 0 };
|
|
1235
|
+
reportTelegramPollingPhase(
|
|
1236
|
+
deps,
|
|
1237
|
+
"persisting-journal",
|
|
1238
|
+
deps.updates[0]?.update_id,
|
|
1239
|
+
);
|
|
1240
|
+
await deps.appendBatch(deps.updates);
|
|
1241
|
+
reportTelegramPollingPhase(deps, "persisting-offset", latestUpdateId);
|
|
1242
|
+
const previousUpdateId = deps.config.lastUpdateId;
|
|
1243
|
+
deps.config.lastUpdateId = latestUpdateId;
|
|
1244
|
+
try {
|
|
1245
|
+
await deps.persistConfig(deps.config);
|
|
1246
|
+
} catch (error) {
|
|
1247
|
+
if (deps.config.lastUpdateId === latestUpdateId) {
|
|
1248
|
+
deps.config.lastUpdateId = previousUpdateId;
|
|
1249
|
+
}
|
|
1250
|
+
throw error;
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
deps.signalWorker();
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1256
|
+
phase: "worker-signal",
|
|
1257
|
+
updateCount: deps.updates.length,
|
|
1258
|
+
latestUpdateId,
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return { updateCount: deps.updates.length, latestUpdateId };
|
|
1262
|
+
}
|
|
1263
|
+
|
|
891
1264
|
export interface TelegramPollLoopDeps<
|
|
892
1265
|
TUpdate extends TelegramUpdate,
|
|
893
1266
|
TContext = unknown,
|
|
@@ -900,13 +1273,20 @@ export interface TelegramPollLoopDeps<
|
|
|
900
1273
|
body: Record<string, unknown>,
|
|
901
1274
|
signal: AbortSignal,
|
|
902
1275
|
) => Promise<TUpdate[]>;
|
|
1276
|
+
getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
|
|
903
1277
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
904
|
-
|
|
1278
|
+
appendUpdateBatch: (updates: readonly TUpdate[]) => MaybePromise<unknown>;
|
|
1279
|
+
getJournalEntryCount: () => number;
|
|
1280
|
+
signalUpdateWorker: () => void;
|
|
905
1281
|
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
906
1282
|
onErrorStatus: (message: string) => void;
|
|
907
1283
|
onStatusReset: () => void;
|
|
908
1284
|
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
909
|
-
|
|
1285
|
+
onPhaseChange?: (
|
|
1286
|
+
phase: TelegramPollingWorkPhase,
|
|
1287
|
+
currentUpdateId?: number,
|
|
1288
|
+
) => void;
|
|
1289
|
+
onSuccessfulResponse?: (updateCount: number) => void;
|
|
910
1290
|
}
|
|
911
1291
|
|
|
912
1292
|
export interface TelegramPollLoopRunnerDeps<
|
|
@@ -919,12 +1299,19 @@ export interface TelegramPollLoopRunnerDeps<
|
|
|
919
1299
|
body: Record<string, unknown>,
|
|
920
1300
|
signal: AbortSignal,
|
|
921
1301
|
) => Promise<TUpdate[]>;
|
|
1302
|
+
getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
|
|
922
1303
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
923
|
-
|
|
1304
|
+
appendUpdateBatch: (updates: readonly TUpdate[]) => MaybePromise<unknown>;
|
|
1305
|
+
getJournalEntryCount: () => number;
|
|
1306
|
+
signalUpdateWorker: () => void;
|
|
924
1307
|
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
925
1308
|
updateStatus: (ctx: TContext, message?: string) => void;
|
|
926
1309
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
927
|
-
|
|
1310
|
+
onPhaseChange?: (
|
|
1311
|
+
phase: TelegramPollingWorkPhase,
|
|
1312
|
+
currentUpdateId?: number,
|
|
1313
|
+
) => void;
|
|
1314
|
+
onSuccessfulResponse?: (updateCount: number) => void;
|
|
928
1315
|
}
|
|
929
1316
|
|
|
930
1317
|
export function sleepTelegramPollingRetry(
|
|
@@ -966,8 +1353,11 @@ export function createTelegramPollLoopRunner<
|
|
|
966
1353
|
config: deps.getConfig(),
|
|
967
1354
|
deleteWebhook: deps.deleteWebhook,
|
|
968
1355
|
getUpdates: deps.getUpdates,
|
|
1356
|
+
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
969
1357
|
persistConfig: deps.persistConfig,
|
|
970
|
-
|
|
1358
|
+
appendUpdateBatch: deps.appendUpdateBatch,
|
|
1359
|
+
getJournalEntryCount: deps.getJournalEntryCount,
|
|
1360
|
+
signalUpdateWorker: deps.signalUpdateWorker,
|
|
971
1361
|
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
972
1362
|
onErrorStatus: (message) => {
|
|
973
1363
|
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
@@ -981,7 +1371,8 @@ export function createTelegramPollLoopRunner<
|
|
|
981
1371
|
});
|
|
982
1372
|
},
|
|
983
1373
|
sleep,
|
|
984
|
-
|
|
1374
|
+
onPhaseChange: deps.onPhaseChange,
|
|
1375
|
+
onSuccessfulResponse: deps.onSuccessfulResponse,
|
|
985
1376
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
986
1377
|
});
|
|
987
1378
|
}
|
|
@@ -996,6 +1387,102 @@ export function isTelegramGetUpdatesConflictError(error: unknown): boolean {
|
|
|
996
1387
|
);
|
|
997
1388
|
}
|
|
998
1389
|
|
|
1390
|
+
function reportTelegramPollingPhase(
|
|
1391
|
+
deps: TelegramRuntimeEventRecorderPort & {
|
|
1392
|
+
onPhaseChange?: (
|
|
1393
|
+
phase: TelegramPollingWorkPhase,
|
|
1394
|
+
currentUpdateId?: number,
|
|
1395
|
+
) => void;
|
|
1396
|
+
},
|
|
1397
|
+
phase: TelegramPollingWorkPhase,
|
|
1398
|
+
currentUpdateId?: number,
|
|
1399
|
+
): void {
|
|
1400
|
+
try {
|
|
1401
|
+
deps.onPhaseChange?.(phase, currentUpdateId);
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1404
|
+
phase: "phase-observer",
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function reportTelegramPollingResponse<
|
|
1410
|
+
TUpdate extends TelegramUpdate,
|
|
1411
|
+
TContext,
|
|
1412
|
+
>(
|
|
1413
|
+
deps: TelegramPollLoopDeps<TUpdate, TContext>,
|
|
1414
|
+
updateCount: number,
|
|
1415
|
+
): void {
|
|
1416
|
+
try {
|
|
1417
|
+
deps.onSuccessfulResponse?.(updateCount);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1420
|
+
phase: "response-observer",
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function getTelegramPollingAbortReason(signal: AbortSignal): unknown {
|
|
1426
|
+
return signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function requestTelegramUpdatesWithinBudget<
|
|
1430
|
+
TUpdate extends TelegramUpdate,
|
|
1431
|
+
TContext,
|
|
1432
|
+
>(
|
|
1433
|
+
deps: TelegramPollLoopDeps<TUpdate, TContext>,
|
|
1434
|
+
body: Record<string, unknown>,
|
|
1435
|
+
): Promise<TUpdate[]> {
|
|
1436
|
+
if (deps.signal.aborted) throw getTelegramPollingAbortReason(deps.signal);
|
|
1437
|
+
const configuredBudgetMs = deps.getUpdatesRequestBudgetMs?.(body);
|
|
1438
|
+
const timeoutMs =
|
|
1439
|
+
typeof configuredBudgetMs === "number" &&
|
|
1440
|
+
Number.isFinite(configuredBudgetMs) &&
|
|
1441
|
+
configuredBudgetMs > 0
|
|
1442
|
+
? Math.floor(configuredBudgetMs)
|
|
1443
|
+
: getTelegramGetUpdatesRequestBudgetMs(body);
|
|
1444
|
+
const controller = new AbortController();
|
|
1445
|
+
const abortFromOwner = () => {
|
|
1446
|
+
controller.abort(getTelegramPollingAbortReason(deps.signal));
|
|
1447
|
+
};
|
|
1448
|
+
deps.signal.addEventListener("abort", abortFromOwner, { once: true });
|
|
1449
|
+
if (deps.signal.aborted) abortFromOwner();
|
|
1450
|
+
const timeout = setTimeout(() => {
|
|
1451
|
+
controller.abort(new TelegramGetUpdatesTimeoutError(timeoutMs));
|
|
1452
|
+
}, timeoutMs);
|
|
1453
|
+
let onRequestAbort: (() => void) | undefined;
|
|
1454
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
1455
|
+
onRequestAbort = () =>
|
|
1456
|
+
reject(getTelegramPollingAbortReason(controller.signal));
|
|
1457
|
+
controller.signal.addEventListener("abort", onRequestAbort, {
|
|
1458
|
+
once: true,
|
|
1459
|
+
});
|
|
1460
|
+
if (controller.signal.aborted) onRequestAbort();
|
|
1461
|
+
});
|
|
1462
|
+
const operation = Promise.resolve()
|
|
1463
|
+
.then(() => {
|
|
1464
|
+
if (controller.signal.aborted) {
|
|
1465
|
+
throw getTelegramPollingAbortReason(controller.signal);
|
|
1466
|
+
}
|
|
1467
|
+
return deps.getUpdates(body, controller.signal);
|
|
1468
|
+
})
|
|
1469
|
+
.catch((error) => {
|
|
1470
|
+
if (controller.signal.aborted) {
|
|
1471
|
+
throw getTelegramPollingAbortReason(controller.signal);
|
|
1472
|
+
}
|
|
1473
|
+
throw error;
|
|
1474
|
+
});
|
|
1475
|
+
try {
|
|
1476
|
+
return await Promise.race([operation, aborted]);
|
|
1477
|
+
} finally {
|
|
1478
|
+
clearTimeout(timeout);
|
|
1479
|
+
deps.signal.removeEventListener("abort", abortFromOwner);
|
|
1480
|
+
if (onRequestAbort) {
|
|
1481
|
+
controller.signal.removeEventListener("abort", onRequestAbort);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
999
1486
|
export async function runTelegramPollLoop<
|
|
1000
1487
|
TUpdate extends TelegramUpdate,
|
|
1001
1488
|
TContext = unknown,
|
|
@@ -1006,82 +1493,80 @@ export async function runTelegramPollLoop<
|
|
|
1006
1493
|
} catch {
|
|
1007
1494
|
// ignore
|
|
1008
1495
|
}
|
|
1496
|
+
if (
|
|
1497
|
+
deps.config.lastUpdateId === undefined &&
|
|
1498
|
+
deps.getJournalEntryCount() > 0
|
|
1499
|
+
) {
|
|
1500
|
+
throw new TelegramPollingCursorBootstrapError(
|
|
1501
|
+
"Telegram polling cursor is missing while the durable update journal is non-empty.",
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1009
1504
|
if (deps.config.lastUpdateId === undefined) {
|
|
1010
1505
|
try {
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
);
|
|
1506
|
+
const request = buildTelegramInitialSyncRequest();
|
|
1507
|
+
reportTelegramPollingPhase(deps, "long-poll");
|
|
1508
|
+
const updates = await requestTelegramUpdatesWithinBudget(deps, request);
|
|
1509
|
+
reportTelegramPollingResponse(deps, updates.length);
|
|
1015
1510
|
const lastUpdateId = getLatestTelegramUpdateId(updates);
|
|
1016
1511
|
if (lastUpdateId !== undefined) {
|
|
1512
|
+
reportTelegramPollingPhase(
|
|
1513
|
+
deps,
|
|
1514
|
+
"persisting-offset",
|
|
1515
|
+
lastUpdateId,
|
|
1516
|
+
);
|
|
1017
1517
|
deps.config.lastUpdateId = lastUpdateId;
|
|
1018
1518
|
await deps.persistConfig(deps.config);
|
|
1519
|
+
deps.recordRuntimeEvent?.(
|
|
1520
|
+
"polling",
|
|
1521
|
+
new Error("Initialized Telegram cursor without executing history."),
|
|
1522
|
+
{ phase: "cursor-bootstrap", lastUpdateId },
|
|
1523
|
+
);
|
|
1019
1524
|
}
|
|
1020
|
-
} catch {
|
|
1021
|
-
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
1527
|
+
reportTelegramPollingPhase(deps, "retrying");
|
|
1528
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1529
|
+
phase: "initial-sync",
|
|
1530
|
+
...(error instanceof TelegramGetUpdatesTimeoutError
|
|
1531
|
+
? { timeoutMs: error.timeoutMs }
|
|
1532
|
+
: {}),
|
|
1533
|
+
});
|
|
1022
1534
|
}
|
|
1023
1535
|
}
|
|
1024
|
-
const maxUpdateFailures = Math.max(
|
|
1025
|
-
1,
|
|
1026
|
-
deps.maxUpdateFailures ?? TELEGRAM_POLLING_DEFAULT_MAX_UPDATE_FAILURES,
|
|
1027
|
-
);
|
|
1028
|
-
const updateFailures = new Map<number, number>();
|
|
1029
|
-
const admittedUpdates = new Set<number>();
|
|
1030
|
-
let handledUpdateFailureRethrown = false;
|
|
1031
1536
|
let consecutiveGetUpdatesConflicts = 0;
|
|
1537
|
+
let currentUpdateId: number | undefined;
|
|
1032
1538
|
while (!deps.signal.aborted) {
|
|
1033
1539
|
try {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
);
|
|
1540
|
+
currentUpdateId = undefined;
|
|
1541
|
+
const request = buildTelegramLongPollRequest(deps.config.lastUpdateId);
|
|
1542
|
+
reportTelegramPollingPhase(deps, "long-poll");
|
|
1543
|
+
const updates = await requestTelegramUpdatesWithinBudget(deps, request);
|
|
1544
|
+
reportTelegramPollingResponse(deps, updates.length);
|
|
1038
1545
|
deps.prepareUpdateBatch?.(updates);
|
|
1039
1546
|
consecutiveGetUpdatesConflicts = 0;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
updateFailures.delete(update.update_id);
|
|
1052
|
-
await deps.persistConfig(deps.config);
|
|
1053
|
-
admittedUpdates.delete(update.update_id);
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
if (admittedUpdates.has(update.update_id)) throw error;
|
|
1056
|
-
const failureCount = (updateFailures.get(update.update_id) ?? 0) + 1;
|
|
1057
|
-
updateFailures.set(update.update_id, failureCount);
|
|
1058
|
-
deps.recordRuntimeEvent?.("polling", error, {
|
|
1059
|
-
phase: "handleUpdate",
|
|
1060
|
-
updateId: update.update_id,
|
|
1061
|
-
failureCount,
|
|
1062
|
-
});
|
|
1063
|
-
if (failureCount < maxUpdateFailures) {
|
|
1064
|
-
handledUpdateFailureRethrown = true;
|
|
1065
|
-
throw error;
|
|
1066
|
-
}
|
|
1067
|
-
const message = getTelegramPollingErrorMessage(error);
|
|
1068
|
-
deps.onErrorStatus(
|
|
1069
|
-
`skipping Telegram update ${update.update_id} after ${failureCount} failures: ${message}`,
|
|
1070
|
-
);
|
|
1071
|
-
admittedUpdates.add(update.update_id);
|
|
1072
|
-
deps.config.lastUpdateId = update.update_id;
|
|
1073
|
-
updateFailures.delete(update.update_id);
|
|
1074
|
-
await deps.persistConfig(deps.config);
|
|
1075
|
-
admittedUpdates.delete(update.update_id);
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1547
|
+
currentUpdateId = updates[0]?.update_id;
|
|
1548
|
+
await admitTelegramPollingUpdateBatch({
|
|
1549
|
+
updates,
|
|
1550
|
+
config: deps.config,
|
|
1551
|
+
appendBatch: deps.appendUpdateBatch,
|
|
1552
|
+
persistConfig: deps.persistConfig,
|
|
1553
|
+
signalWorker: deps.signalUpdateWorker,
|
|
1554
|
+
onPhaseChange: deps.onPhaseChange,
|
|
1555
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1556
|
+
});
|
|
1557
|
+
currentUpdateId = undefined;
|
|
1078
1558
|
} catch (error) {
|
|
1079
1559
|
if (shouldStopTelegramPolling(deps.signal.aborted, error)) return;
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1560
|
+
reportTelegramPollingPhase(deps, "retrying", currentUpdateId);
|
|
1561
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1562
|
+
phase:
|
|
1563
|
+
error instanceof TelegramGetUpdatesTimeoutError
|
|
1564
|
+
? "long-poll"
|
|
1565
|
+
: "loop",
|
|
1566
|
+
...(error instanceof TelegramGetUpdatesTimeoutError
|
|
1567
|
+
? { timeoutMs: error.timeoutMs }
|
|
1568
|
+
: {}),
|
|
1569
|
+
});
|
|
1085
1570
|
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1086
1571
|
consecutiveGetUpdatesConflicts += 1;
|
|
1087
1572
|
await deps.sleep(
|