@llblab/pi-telegram 0.45.11 → 0.47.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/BACKLOG.md +5 -1
- package/CHANGELOG.md +12 -0
- package/README.md +6 -6
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1608 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +116 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1006 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +17 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +11 -3
- package/lib/bus-leader.ts +57 -20
- package/lib/bus.ts +12 -2
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +26 -1
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +21 -5
- package/lib/outbound-markup.ts +16 -7
- package/lib/pi.ts +4 -0
- package/lib/sync.ts +3 -0
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-cleanup-manager.ts +36 -6
- package/lib/thread-display.ts +96 -18
- package/lib/threads.ts +150 -30
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,1196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram polling runtime domain helpers
|
|
3
|
+
* Zones: telegram transport, polling runtime
|
|
4
|
+
* Owns polling request builders, stop conditions, and the long-poll loop runtime for Telegram updates
|
|
5
|
+
*/
|
|
6
|
+
const TELEGRAM_INITIAL_SYNC_OFFSET = -1;
|
|
7
|
+
const TELEGRAM_INITIAL_SYNC_LIMIT = 1;
|
|
8
|
+
const TELEGRAM_INITIAL_SYNC_TIMEOUT_SECONDS = 0;
|
|
9
|
+
const TELEGRAM_LONG_POLL_LIMIT = 10;
|
|
10
|
+
const TELEGRAM_LONG_POLL_TIMEOUT_SECONDS = 30;
|
|
11
|
+
const TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS = 2_500;
|
|
12
|
+
const TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES = 2;
|
|
13
|
+
export const TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT = 10;
|
|
14
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT = 3;
|
|
15
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS = 1_000;
|
|
16
|
+
const TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS = 3_000;
|
|
17
|
+
const TELEGRAM_POLLING_RETRY_MS = 3_000;
|
|
18
|
+
export const TELEGRAM_GET_UPDATES_GRACE_MS = 10_000;
|
|
19
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events,
|
|
20
|
+
// so queue removal stays reaction-driven while delete-like business updates remain defensive-only.
|
|
21
|
+
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
22
|
+
"message",
|
|
23
|
+
"edited_message",
|
|
24
|
+
"callback_query",
|
|
25
|
+
"message_reaction",
|
|
26
|
+
"guest_message",
|
|
27
|
+
];
|
|
28
|
+
export function buildTelegramInitialSyncRequest() {
|
|
29
|
+
return {
|
|
30
|
+
offset: TELEGRAM_INITIAL_SYNC_OFFSET,
|
|
31
|
+
limit: TELEGRAM_INITIAL_SYNC_LIMIT,
|
|
32
|
+
timeout: TELEGRAM_INITIAL_SYNC_TIMEOUT_SECONDS,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function buildTelegramLongPollRequest(lastUpdateId) {
|
|
36
|
+
return {
|
|
37
|
+
offset: lastUpdateId !== undefined ? lastUpdateId + 1 : undefined,
|
|
38
|
+
limit: TELEGRAM_LONG_POLL_LIMIT,
|
|
39
|
+
timeout: TELEGRAM_LONG_POLL_TIMEOUT_SECONDS,
|
|
40
|
+
allowed_updates: TELEGRAM_ALLOWED_UPDATES,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function getLatestTelegramUpdateId(updates) {
|
|
44
|
+
return updates.at(-1)?.update_id;
|
|
45
|
+
}
|
|
46
|
+
export class TelegramPersistentGetUpdatesConflictError extends Error {
|
|
47
|
+
count;
|
|
48
|
+
constructor(count) {
|
|
49
|
+
super(`Telegram polling stopped after ${count} consecutive getUpdates conflicts.`);
|
|
50
|
+
this.name = "TelegramPersistentGetUpdatesConflictError";
|
|
51
|
+
this.count = count;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export class TelegramGetUpdatesTimeoutError extends Error {
|
|
55
|
+
timeoutMs;
|
|
56
|
+
constructor(timeoutMs) {
|
|
57
|
+
super(`Telegram getUpdates timed out after ${timeoutMs} ms.`);
|
|
58
|
+
this.name = "TelegramGetUpdatesTimeoutError";
|
|
59
|
+
this.timeoutMs = timeoutMs;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function getTelegramGetUpdatesRequestBudgetMs(body, graceMs = TELEGRAM_GET_UPDATES_GRACE_MS) {
|
|
63
|
+
const timeoutSeconds = typeof body.timeout === "number" &&
|
|
64
|
+
Number.isFinite(body.timeout) &&
|
|
65
|
+
body.timeout >= 0
|
|
66
|
+
? body.timeout
|
|
67
|
+
: 0;
|
|
68
|
+
const normalizedGraceMs = Number.isFinite(graceMs) && graceMs > 0
|
|
69
|
+
? Math.floor(graceMs)
|
|
70
|
+
: TELEGRAM_GET_UPDATES_GRACE_MS;
|
|
71
|
+
return Math.floor(timeoutSeconds * 1_000) + normalizedGraceMs;
|
|
72
|
+
}
|
|
73
|
+
export function shouldStopTelegramPolling(signalAborted, error) {
|
|
74
|
+
return (signalAborted ||
|
|
75
|
+
(error instanceof DOMException && error.name === "AbortError"));
|
|
76
|
+
}
|
|
77
|
+
export function createTelegramPollingControllerState() {
|
|
78
|
+
return {
|
|
79
|
+
phase: "stopped",
|
|
80
|
+
stopReason: "not-started",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function getTelegramPollingStateSnapshot(state) {
|
|
84
|
+
return {
|
|
85
|
+
phase: state.phase,
|
|
86
|
+
phaseStartedAtMs: state.phaseStartedAtMs,
|
|
87
|
+
currentUpdateId: state.currentUpdateId,
|
|
88
|
+
startedAtMs: state.startedAtMs,
|
|
89
|
+
stoppedAtMs: state.stoppedAtMs,
|
|
90
|
+
lastSuccessfulResponseAtMs: state.lastSuccessfulResponseAtMs,
|
|
91
|
+
lastSuccessfulResponseUpdateCount: state.lastSuccessfulResponseUpdateCount,
|
|
92
|
+
stopReason: state.stopReason,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function createTelegramPollingStateReader(state) {
|
|
96
|
+
return () => getTelegramPollingStateSnapshot(state);
|
|
97
|
+
}
|
|
98
|
+
export function isTelegramPollingControllerActive(state) {
|
|
99
|
+
return !!state.pollingPromise;
|
|
100
|
+
}
|
|
101
|
+
export function createTelegramPollingActivityReader(state) {
|
|
102
|
+
return () => isTelegramPollingControllerActive(state);
|
|
103
|
+
}
|
|
104
|
+
export function createTelegramPollingAdmissionRuntime(deps) {
|
|
105
|
+
let generation = 0;
|
|
106
|
+
return {
|
|
107
|
+
isActive: deps.polling.isActive,
|
|
108
|
+
async start(ctx) {
|
|
109
|
+
if (!(deps.canStart?.(ctx) ?? true))
|
|
110
|
+
return;
|
|
111
|
+
const expectedGeneration = ++generation;
|
|
112
|
+
const isCurrent = () => expectedGeneration === generation && (deps.canStart?.(ctx) ?? true);
|
|
113
|
+
if (!isCurrent())
|
|
114
|
+
return;
|
|
115
|
+
await deps.prepareStart?.();
|
|
116
|
+
if (!isCurrent())
|
|
117
|
+
return;
|
|
118
|
+
deps.validateStart?.();
|
|
119
|
+
await deps.worker.onSessionStart(ctx);
|
|
120
|
+
if (!isCurrent())
|
|
121
|
+
return;
|
|
122
|
+
deps.polling.start(ctx);
|
|
123
|
+
},
|
|
124
|
+
async stop() {
|
|
125
|
+
generation += 1;
|
|
126
|
+
await deps.polling.stop();
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Own journal-first polling assembly and cursor bootstrap validation. */
|
|
131
|
+
export function createTelegramDurablePollingRuntimeAssembly(deps) {
|
|
132
|
+
const controller = createTelegramPollingControllerRuntime({
|
|
133
|
+
...deps,
|
|
134
|
+
appendUpdateBatch(updates, cursor) {
|
|
135
|
+
const result = deps.journal.appendBatch(updates, cursor);
|
|
136
|
+
if (!deps.prepareUpdateBatch)
|
|
137
|
+
return result;
|
|
138
|
+
// No await: an already-draining worker must not observe the new batch before preparation.
|
|
139
|
+
try {
|
|
140
|
+
const included = new Set(result.nonExcludedUpdateIds);
|
|
141
|
+
let batch = [];
|
|
142
|
+
for (const update of updates) {
|
|
143
|
+
if (included.has(update.update_id))
|
|
144
|
+
batch.push(update);
|
|
145
|
+
else if (batch.length > 0) {
|
|
146
|
+
deps.prepareUpdateBatch(batch);
|
|
147
|
+
batch = [];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (batch.length > 0)
|
|
151
|
+
deps.prepareUpdateBatch(batch);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "batch-preparation", updateCount: updates.length });
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Already-published input must still reach the worker if diagnostics fail.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
},
|
|
163
|
+
getAcceptedThroughUpdateId: deps.journal.getAcceptedThroughUpdateId,
|
|
164
|
+
getJournalEntryCount: deps.journal.getEntryCount,
|
|
165
|
+
signalUpdateWorker: deps.journal.signalWorker,
|
|
166
|
+
});
|
|
167
|
+
const admission = createTelegramPollingAdmissionRuntime({
|
|
168
|
+
polling: controller,
|
|
169
|
+
prepareStart: deps.journal.prepareCursorCutover,
|
|
170
|
+
canStart: deps.canStart,
|
|
171
|
+
validateStart() {
|
|
172
|
+
if (deps.journal.getAcceptedThroughUpdateId() !== undefined)
|
|
173
|
+
return;
|
|
174
|
+
if (deps.journal.getBootstrapEntryCount() === 0)
|
|
175
|
+
return;
|
|
176
|
+
throw new TelegramPollingCursorBootstrapError("Telegram polling cursor is missing while the durable update journal is non-empty.");
|
|
177
|
+
},
|
|
178
|
+
worker: deps.journal,
|
|
179
|
+
});
|
|
180
|
+
return { controller, admission };
|
|
181
|
+
}
|
|
182
|
+
function notifyTelegramPollingStateChange(deps) {
|
|
183
|
+
try {
|
|
184
|
+
deps.onPollingStateChange?.();
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
188
|
+
phase: "state-observer",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function transitionTelegramPollingState(state, phase, nowMs, currentUpdateId) {
|
|
193
|
+
state.phase = phase;
|
|
194
|
+
state.phaseStartedAtMs = nowMs;
|
|
195
|
+
state.currentUpdateId = currentUpdateId;
|
|
196
|
+
}
|
|
197
|
+
export function createTelegramPollingControllerRuntime(deps) {
|
|
198
|
+
const state = deps.state ?? createTelegramPollingControllerState();
|
|
199
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
200
|
+
const notifyStateChange = () => notifyTelegramPollingStateChange({
|
|
201
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
202
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
203
|
+
});
|
|
204
|
+
return createTelegramPollingController({
|
|
205
|
+
state,
|
|
206
|
+
hasBotToken: deps.hasBotToken,
|
|
207
|
+
stopTypingLoop: deps.stopTypingLoop,
|
|
208
|
+
runPollLoop: createTelegramPollLoopRunner({
|
|
209
|
+
getConfig: deps.getConfig,
|
|
210
|
+
deleteWebhook: deps.deleteWebhook,
|
|
211
|
+
getUpdates: deps.getUpdates,
|
|
212
|
+
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
213
|
+
persistConfig: deps.persistConfig,
|
|
214
|
+
appendUpdateBatch: deps.appendUpdateBatch,
|
|
215
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
216
|
+
getJournalEntryCount: deps.getJournalEntryCount,
|
|
217
|
+
signalUpdateWorker: deps.signalUpdateWorker,
|
|
218
|
+
updateStatus: deps.updateStatus,
|
|
219
|
+
sleep: deps.sleep,
|
|
220
|
+
onPhaseChange(phase, currentUpdateId) {
|
|
221
|
+
transitionTelegramPollingState(state, phase, getNowMs(), currentUpdateId);
|
|
222
|
+
notifyStateChange();
|
|
223
|
+
},
|
|
224
|
+
onSuccessfulResponse(updateCount) {
|
|
225
|
+
state.lastSuccessfulResponseAtMs = getNowMs();
|
|
226
|
+
state.lastSuccessfulResponseUpdateCount = updateCount;
|
|
227
|
+
notifyStateChange();
|
|
228
|
+
},
|
|
229
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
230
|
+
}),
|
|
231
|
+
updateStatus: deps.updateStatus,
|
|
232
|
+
createAbortController: deps.createAbortController,
|
|
233
|
+
getNowMs,
|
|
234
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
235
|
+
onPersistentConflict: deps.onPersistentConflict,
|
|
236
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
export function createTelegramPollingController(deps) {
|
|
240
|
+
const state = deps.state ?? createTelegramPollingControllerState();
|
|
241
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
242
|
+
const notifyStateChange = () => notifyTelegramPollingStateChange({
|
|
243
|
+
onPollingStateChange: deps.onPollingStateChange,
|
|
244
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
245
|
+
});
|
|
246
|
+
const runtimeDeps = {
|
|
247
|
+
...deps,
|
|
248
|
+
getPollingPromise: () => state.pollingPromise,
|
|
249
|
+
setPollingPromise: (promise) => {
|
|
250
|
+
state.pollingPromise = promise;
|
|
251
|
+
},
|
|
252
|
+
getPollingController: () => state.pollingController,
|
|
253
|
+
setPollingController: (controller) => {
|
|
254
|
+
state.pollingController = controller;
|
|
255
|
+
},
|
|
256
|
+
onPollingStarted: () => {
|
|
257
|
+
const nowMs = getNowMs();
|
|
258
|
+
transitionTelegramPollingState(state, "starting", nowMs);
|
|
259
|
+
state.startedAtMs = nowMs;
|
|
260
|
+
state.stoppedAtMs = undefined;
|
|
261
|
+
state.lastSuccessfulResponseAtMs = undefined;
|
|
262
|
+
state.lastSuccessfulResponseUpdateCount = undefined;
|
|
263
|
+
state.stopReason = undefined;
|
|
264
|
+
notifyStateChange();
|
|
265
|
+
deps.onPollingStarted?.();
|
|
266
|
+
},
|
|
267
|
+
onPollingStopped: (reason) => {
|
|
268
|
+
const nowMs = getNowMs();
|
|
269
|
+
transitionTelegramPollingState(state, "stopped", nowMs);
|
|
270
|
+
state.stoppedAtMs = nowMs;
|
|
271
|
+
state.stopReason = reason;
|
|
272
|
+
notifyStateChange();
|
|
273
|
+
deps.onPollingStopped?.(reason);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
isActive: () => isTelegramPollingControllerActive(state),
|
|
278
|
+
start: (ctx) => startTelegramPollingRuntime(ctx, runtimeDeps),
|
|
279
|
+
stop: () => stopTelegramPollingRuntime(runtimeDeps),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export function shouldStartTelegramPolling(state) {
|
|
283
|
+
return state.hasBotToken && !state.hasPollingPromise;
|
|
284
|
+
}
|
|
285
|
+
export async function stopTelegramPollingRuntime(deps) {
|
|
286
|
+
const pollingPromise = deps.getPollingPromise();
|
|
287
|
+
const pollingController = deps.getPollingController();
|
|
288
|
+
try {
|
|
289
|
+
deps.stopTypingLoop();
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "typing-stop" });
|
|
293
|
+
}
|
|
294
|
+
pollingController?.abort();
|
|
295
|
+
await pollingPromise?.catch(() => undefined);
|
|
296
|
+
let cleared = false;
|
|
297
|
+
if (deps.getPollingPromise() === pollingPromise) {
|
|
298
|
+
deps.setPollingPromise(undefined);
|
|
299
|
+
cleared = pollingPromise !== undefined;
|
|
300
|
+
}
|
|
301
|
+
if (deps.getPollingController() === pollingController) {
|
|
302
|
+
deps.setPollingController(undefined);
|
|
303
|
+
cleared = cleared || pollingController !== undefined;
|
|
304
|
+
}
|
|
305
|
+
if (cleared)
|
|
306
|
+
deps.onPollingStopped?.("requested");
|
|
307
|
+
}
|
|
308
|
+
function updateTelegramPollingStatusSafely(updateStatus, ctx, options = {}) {
|
|
309
|
+
try {
|
|
310
|
+
updateStatus(ctx, options.message);
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
// The polling loop can outlive the session context it captured.
|
|
314
|
+
options.recordRuntimeEvent?.("polling", error, { phase: "status-update" });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
export function startTelegramPollingRuntime(ctx, deps) {
|
|
318
|
+
if (!shouldStartTelegramPolling({
|
|
319
|
+
hasBotToken: deps.hasBotToken(),
|
|
320
|
+
hasPollingPromise: !!deps.getPollingPromise(),
|
|
321
|
+
})) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const controller = deps.createAbortController?.() ?? new AbortController();
|
|
325
|
+
deps.setPollingController(controller);
|
|
326
|
+
deps.onPollingStarted?.();
|
|
327
|
+
let failed = false;
|
|
328
|
+
let persistentConflict;
|
|
329
|
+
let runPromise;
|
|
330
|
+
try {
|
|
331
|
+
runPromise = deps.runPollLoop(ctx, controller.signal);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
runPromise = Promise.reject(error);
|
|
335
|
+
}
|
|
336
|
+
let promise;
|
|
337
|
+
promise = runPromise
|
|
338
|
+
.catch((error) => {
|
|
339
|
+
if (shouldStopTelegramPolling(controller.signal.aborted, error))
|
|
340
|
+
return;
|
|
341
|
+
if (error instanceof TelegramPersistentGetUpdatesConflictError) {
|
|
342
|
+
persistentConflict = error;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
failed = true;
|
|
346
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
347
|
+
phase: "controller",
|
|
348
|
+
});
|
|
349
|
+
})
|
|
350
|
+
.finally(async () => {
|
|
351
|
+
const ownsPromise = deps.getPollingPromise() === promise;
|
|
352
|
+
const ownsController = deps.getPollingController() === controller;
|
|
353
|
+
if (ownsPromise)
|
|
354
|
+
deps.setPollingPromise(undefined);
|
|
355
|
+
if (ownsController)
|
|
356
|
+
deps.setPollingController(undefined);
|
|
357
|
+
if (!ownsPromise && !ownsController)
|
|
358
|
+
return;
|
|
359
|
+
deps.onPollingStopped?.(controller.signal.aborted ? "requested" : persistentConflict
|
|
360
|
+
? "persistent-conflict" : failed ? "failed" : "completed");
|
|
361
|
+
// Detach the inner promise before outer teardown calls polling.stop().
|
|
362
|
+
if (persistentConflict && !controller.signal.aborted) {
|
|
363
|
+
try {
|
|
364
|
+
if (deps.onPersistentConflict) {
|
|
365
|
+
await deps.onPersistentConflict(ctx, persistentConflict.count);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
deps.stopTypingLoop();
|
|
369
|
+
deps.recordRuntimeEvent?.("polling", persistentConflict, {
|
|
370
|
+
phase: "persistent-conflict", count: persistentConflict.count,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
deps.recordRuntimeEvent?.("polling", error, { phase: "conflict-stand-down" });
|
|
376
|
+
}
|
|
377
|
+
if (deps.getPollingController() || deps.getPollingPromise())
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
381
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
deps.setPollingPromise(promise);
|
|
385
|
+
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
386
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
export function createTelegramThreadTargetObservationBinding() {
|
|
390
|
+
let handler;
|
|
391
|
+
return {
|
|
392
|
+
async handle(ctx) {
|
|
393
|
+
await handler?.(ctx);
|
|
394
|
+
},
|
|
395
|
+
set(nextHandler) {
|
|
396
|
+
handler = nextHandler;
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
export function createTelegramThreadCapabilityStateRuntime() {
|
|
401
|
+
let busPollingStarted = false;
|
|
402
|
+
let topicModeUnavailable = false;
|
|
403
|
+
let forceFreshLeaderThread = false;
|
|
404
|
+
let requestedThreadName;
|
|
405
|
+
return {
|
|
406
|
+
isBusPollingStarted: () => busPollingStarted,
|
|
407
|
+
setBusPollingStarted(started) {
|
|
408
|
+
busPollingStarted = started;
|
|
409
|
+
},
|
|
410
|
+
isTopicModeUnavailable: () => topicModeUnavailable,
|
|
411
|
+
setTopicModeUnavailable(unavailable) {
|
|
412
|
+
topicModeUnavailable = unavailable;
|
|
413
|
+
},
|
|
414
|
+
isBusRuntimeEnabled: () => !topicModeUnavailable,
|
|
415
|
+
shouldForceFreshLeaderThread: () => forceFreshLeaderThread,
|
|
416
|
+
setForceFreshLeaderThread(forceFresh) {
|
|
417
|
+
forceFreshLeaderThread = forceFresh;
|
|
418
|
+
},
|
|
419
|
+
getRequestedThreadName: () => requestedThreadName,
|
|
420
|
+
setRequestedThreadName(threadName) {
|
|
421
|
+
requestedThreadName = threadName;
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
export function createTelegramThreadCapabilityOrchestration(deps) {
|
|
426
|
+
let generation = 0;
|
|
427
|
+
const lifecycle = {
|
|
428
|
+
capture() {
|
|
429
|
+
const expected = generation;
|
|
430
|
+
return () => expected === generation;
|
|
431
|
+
},
|
|
432
|
+
invalidate() { generation++; },
|
|
433
|
+
};
|
|
434
|
+
const capabilityDeps = {
|
|
435
|
+
lifecycle,
|
|
436
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
437
|
+
callApi: deps.callApi,
|
|
438
|
+
topicTargetStore: deps.topicTargetStore,
|
|
439
|
+
ownsLock: deps.ownsLock,
|
|
440
|
+
isFollowerRegistered: deps.isFollowerRegistered,
|
|
441
|
+
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
442
|
+
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
443
|
+
setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
|
|
444
|
+
suspendLiveThreadTarget: deps.suspendLiveThreadTarget,
|
|
445
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
446
|
+
startClassicPolling: deps.startClassicPolling,
|
|
447
|
+
stopClassicPolling: deps.stopClassicPolling,
|
|
448
|
+
startBusPolling: deps.startBusLeaderPolling,
|
|
449
|
+
stopBusPolling: deps.stopBusLeaderPolling,
|
|
450
|
+
startLeaderHealth: deps.startLeaderHealth,
|
|
451
|
+
stopLeaderHealth: deps.stopLeaderHealth,
|
|
452
|
+
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
453
|
+
updateStatus: deps.updateStatus,
|
|
454
|
+
recordEvent: deps.recordEvent,
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
monitor: createTelegramThreadCapabilityMonitor(capabilityDeps),
|
|
458
|
+
observeTarget: createTelegramThreadTargetObservationHandler(capabilityDeps),
|
|
459
|
+
pollingPorts: createTelegramThreadAwarePollingPorts({
|
|
460
|
+
lifecycle,
|
|
461
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
462
|
+
callApi: deps.callApi,
|
|
463
|
+
topicTargetStore: deps.topicTargetStore,
|
|
464
|
+
isBusRuntimeEnabled: deps.isBusRuntimeEnabled,
|
|
465
|
+
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
466
|
+
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
467
|
+
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
468
|
+
setForceFreshLeaderThreadOnNextStart: deps.state.setForceFreshLeaderThread,
|
|
469
|
+
startClassicPolling: deps.startClassicPolling,
|
|
470
|
+
stopClassicPolling: deps.stopClassicPolling,
|
|
471
|
+
startBusLeaderPolling: deps.startBusLeaderPolling,
|
|
472
|
+
stopBusLeaderPolling: deps.stopBusLeaderPolling,
|
|
473
|
+
startLeaderHealth: deps.startLeaderHealth,
|
|
474
|
+
stopLeaderHealth: deps.stopLeaderHealth,
|
|
475
|
+
registerFollowerWithLeader: deps.registerFollowerWithLeader,
|
|
476
|
+
restoreFollowerWithLeader: deps.restoreFollowerWithLeader,
|
|
477
|
+
hasRememberedWorkspaceBinding: deps.hasRememberedWorkspaceBinding,
|
|
478
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
479
|
+
recordEvent: deps.recordEvent,
|
|
480
|
+
setTopicModeUnavailable: deps.state.setTopicModeUnavailable,
|
|
481
|
+
}),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
export async function readTelegramThreadCapability(deps) {
|
|
485
|
+
const bot = await deps.callApi("getMe", {});
|
|
486
|
+
if (bot.has_topics_enabled === true)
|
|
487
|
+
return true;
|
|
488
|
+
if (bot.has_topics_enabled === false)
|
|
489
|
+
return false;
|
|
490
|
+
return undefined;
|
|
491
|
+
}
|
|
492
|
+
export async function probeTelegramStartupThreadCapability(deps, isCurrent = () => true) {
|
|
493
|
+
if (!isCurrent())
|
|
494
|
+
return;
|
|
495
|
+
const threadModeEnabled = await readTelegramThreadCapability(deps);
|
|
496
|
+
if (!isCurrent())
|
|
497
|
+
return;
|
|
498
|
+
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
499
|
+
if (threadModeEnabled === false) {
|
|
500
|
+
deps.topicTargetStore.setBotState({
|
|
501
|
+
threadMode: "disabled",
|
|
502
|
+
updatedAtMs: nowMs,
|
|
503
|
+
lastReconcileAction: "startup-bot-topics-disabled",
|
|
504
|
+
});
|
|
505
|
+
await deps.topicTargetStore.persist();
|
|
506
|
+
if (!isCurrent())
|
|
507
|
+
return;
|
|
508
|
+
deps.recordEvent("bus", "Telegram Threaded Mode unavailable on startup", {
|
|
509
|
+
phase: "startup-bot-topics-disabled",
|
|
510
|
+
});
|
|
511
|
+
deps.setTopicModeUnavailable(true);
|
|
512
|
+
return threadModeEnabled;
|
|
513
|
+
}
|
|
514
|
+
if (threadModeEnabled === true) {
|
|
515
|
+
deps.topicTargetStore.setBotState({
|
|
516
|
+
...deps.topicTargetStore.getBotState(),
|
|
517
|
+
threadMode: "enabled",
|
|
518
|
+
updatedAtMs: nowMs,
|
|
519
|
+
lastReconcileAction: "startup-bot-topics-enabled",
|
|
520
|
+
});
|
|
521
|
+
await deps.topicTargetStore.persist();
|
|
522
|
+
if (!isCurrent())
|
|
523
|
+
return;
|
|
524
|
+
deps.setTopicModeUnavailable(false);
|
|
525
|
+
}
|
|
526
|
+
return threadModeEnabled;
|
|
527
|
+
}
|
|
528
|
+
function hasTelegramClassicRestoreFailure(state) {
|
|
529
|
+
return (state.lastReconcileAction?.endsWith("-classic-restore-failed") ?? false);
|
|
530
|
+
}
|
|
531
|
+
function hasTelegramThreadCapabilityBindings(store) {
|
|
532
|
+
return (store.list?.().some((record) => {
|
|
533
|
+
return (typeof record.target?.chatId === "number" &&
|
|
534
|
+
typeof record.target.threadId === "number" &&
|
|
535
|
+
record.status !== "deleted" &&
|
|
536
|
+
record.status !== "offline" &&
|
|
537
|
+
record.status !== "stale");
|
|
538
|
+
}) ?? false);
|
|
539
|
+
}
|
|
540
|
+
export async function applyTelegramThreadCapability(ctx, threadModeEnabled, phase, deps, isCurrent = deps.lifecycle?.capture() ?? (() => true)) {
|
|
541
|
+
if (!isCurrent())
|
|
542
|
+
return;
|
|
543
|
+
await deps.topicTargetStore.load();
|
|
544
|
+
if (!isCurrent())
|
|
545
|
+
return;
|
|
546
|
+
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
547
|
+
const previousBotState = deps.topicTargetStore.getBotState();
|
|
548
|
+
if (!threadModeEnabled) {
|
|
549
|
+
if (hasTelegramThreadCapabilityBindings(deps.topicTargetStore) &&
|
|
550
|
+
!phase.endsWith("-confirmed")) {
|
|
551
|
+
deps.recordEvent("bus", "Telegram Threaded Mode probe deferred", {
|
|
552
|
+
phase,
|
|
553
|
+
reason: "active-thread-bindings-present",
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
deps.topicTargetStore.setBotState({
|
|
558
|
+
threadMode: "disabled",
|
|
559
|
+
updatedAtMs: nowMs,
|
|
560
|
+
lastReconcileAction: phase,
|
|
561
|
+
});
|
|
562
|
+
await deps.topicTargetStore.persist();
|
|
563
|
+
if (!isCurrent())
|
|
564
|
+
return;
|
|
565
|
+
deps.setTopicModeUnavailable(true);
|
|
566
|
+
deps.stopFollowerRegistration();
|
|
567
|
+
const hadLiveThreadTransport = deps.getPollingStartedWithTelegramBus();
|
|
568
|
+
if (hadLiveThreadTransport ||
|
|
569
|
+
hasTelegramClassicRestoreFailure(previousBotState)) {
|
|
570
|
+
deps.stopLeaderHealth();
|
|
571
|
+
await deps.stopBusPolling();
|
|
572
|
+
if (!isCurrent())
|
|
573
|
+
return;
|
|
574
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
575
|
+
if (hadLiveThreadTransport)
|
|
576
|
+
deps.suspendLiveThreadTarget?.();
|
|
577
|
+
try {
|
|
578
|
+
await deps.startClassicPolling(ctx);
|
|
579
|
+
if (!isCurrent())
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
catch (classicError) {
|
|
583
|
+
if (!isCurrent())
|
|
584
|
+
return;
|
|
585
|
+
deps.topicTargetStore.setBotState({
|
|
586
|
+
threadMode: "disabled",
|
|
587
|
+
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
588
|
+
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
589
|
+
});
|
|
590
|
+
await deps.topicTargetStore.persist();
|
|
591
|
+
if (!isCurrent())
|
|
592
|
+
return;
|
|
593
|
+
deps.recordEvent("bus", classicError, {
|
|
594
|
+
phase: `${phase}-classic-restore`,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
deps.updateStatus(ctx);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
deps.topicTargetStore.setBotState({
|
|
602
|
+
...deps.topicTargetStore.getBotState(),
|
|
603
|
+
threadMode: "enabled",
|
|
604
|
+
updatedAtMs: nowMs,
|
|
605
|
+
lastReconcileAction: phase,
|
|
606
|
+
});
|
|
607
|
+
await deps.topicTargetStore.persist();
|
|
608
|
+
if (!isCurrent())
|
|
609
|
+
return;
|
|
610
|
+
deps.setTopicModeUnavailable(false);
|
|
611
|
+
if (!deps.getPollingStartedWithTelegramBus() && deps.ownsLock(ctx)) {
|
|
612
|
+
await deps.stopClassicPolling();
|
|
613
|
+
if (!isCurrent())
|
|
614
|
+
return;
|
|
615
|
+
deps.setPollingStartedWithTelegramBus(true);
|
|
616
|
+
try {
|
|
617
|
+
await deps.startBusPolling(ctx);
|
|
618
|
+
if (!isCurrent())
|
|
619
|
+
return;
|
|
620
|
+
deps.startLeaderHealth();
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
if (!isCurrent())
|
|
624
|
+
return;
|
|
625
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
626
|
+
const threadModeUnavailable = deps.isTopicModeUnavailableError?.(error) === true;
|
|
627
|
+
if (threadModeUnavailable) {
|
|
628
|
+
deps.topicTargetStore.setBotState({
|
|
629
|
+
threadMode: "disabled",
|
|
630
|
+
updatedAtMs: nowMs,
|
|
631
|
+
lastReconcileAction: `${phase}-unavailable`,
|
|
632
|
+
});
|
|
633
|
+
await deps.topicTargetStore.persist();
|
|
634
|
+
if (!isCurrent())
|
|
635
|
+
return;
|
|
636
|
+
deps.setTopicModeUnavailable(true);
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
await deps.startClassicPolling(ctx);
|
|
640
|
+
if (!isCurrent())
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
catch (classicError) {
|
|
644
|
+
if (!isCurrent())
|
|
645
|
+
return;
|
|
646
|
+
deps.topicTargetStore.setBotState({
|
|
647
|
+
threadMode: "disabled",
|
|
648
|
+
updatedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
649
|
+
lastReconcileAction: `${phase}-classic-restore-failed`,
|
|
650
|
+
});
|
|
651
|
+
await deps.topicTargetStore.persist();
|
|
652
|
+
if (!isCurrent())
|
|
653
|
+
return;
|
|
654
|
+
deps.recordEvent("bus", classicError, {
|
|
655
|
+
phase: `${phase}-classic-restore`,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
deps.updateStatus(ctx);
|
|
659
|
+
if (threadModeUnavailable)
|
|
660
|
+
return;
|
|
661
|
+
throw error;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
deps.updateStatus(ctx);
|
|
665
|
+
}
|
|
666
|
+
export function createTelegramThreadAwarePollingPorts(deps) {
|
|
667
|
+
let generation = 0;
|
|
668
|
+
const startPolling = async (ctx, options) => {
|
|
669
|
+
const expectedGeneration = ++generation;
|
|
670
|
+
deps.lifecycle?.invalidate();
|
|
671
|
+
const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
672
|
+
const isCurrent = () => expectedGeneration === generation && isLifecycleCurrent();
|
|
673
|
+
await deps.topicTargetStore.load();
|
|
674
|
+
if (!isCurrent())
|
|
675
|
+
return;
|
|
676
|
+
let startupThreadCapability;
|
|
677
|
+
try {
|
|
678
|
+
startupThreadCapability = await probeTelegramStartupThreadCapability(deps, isCurrent);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
if (!isCurrent())
|
|
682
|
+
return;
|
|
683
|
+
deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
|
|
684
|
+
}
|
|
685
|
+
if (!isCurrent())
|
|
686
|
+
return;
|
|
687
|
+
deps.setTopicModeUnavailable(startupThreadCapability !== true);
|
|
688
|
+
if (deps.isBusRuntimeEnabled()) {
|
|
689
|
+
deps.setTopicModeUnavailable(false);
|
|
690
|
+
try {
|
|
691
|
+
deps.setPollingStartedWithTelegramBus(true);
|
|
692
|
+
deps.setForceFreshLeaderThreadOnNextStart(!!options?.forceFreshLeaderThread);
|
|
693
|
+
await deps.startBusLeaderPolling(ctx);
|
|
694
|
+
if (!isCurrent())
|
|
695
|
+
return;
|
|
696
|
+
deps.startLeaderHealth();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
if (!isCurrent())
|
|
701
|
+
return;
|
|
702
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
703
|
+
if (!deps.isTopicModeUnavailableError(error))
|
|
704
|
+
throw error;
|
|
705
|
+
deps.setTopicModeUnavailable(true);
|
|
706
|
+
await deps.topicTargetStore.load();
|
|
707
|
+
if (!isCurrent())
|
|
708
|
+
return;
|
|
709
|
+
deps.topicTargetStore.setBotState({
|
|
710
|
+
threadMode: "disabled",
|
|
711
|
+
updatedAtMs: Date.now(),
|
|
712
|
+
lastReconcileAction: "thread-mode-unavailable",
|
|
713
|
+
});
|
|
714
|
+
await deps.topicTargetStore.persist();
|
|
715
|
+
if (!isCurrent())
|
|
716
|
+
return;
|
|
717
|
+
deps.recordEvent("bus", error, { phase: "thread-mode-unavailable" });
|
|
718
|
+
}
|
|
719
|
+
finally {
|
|
720
|
+
if (isCurrent())
|
|
721
|
+
deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
725
|
+
await deps.startClassicPolling(ctx);
|
|
726
|
+
};
|
|
727
|
+
const stopPolling = async () => {
|
|
728
|
+
const expectedGeneration = ++generation;
|
|
729
|
+
deps.lifecycle?.invalidate();
|
|
730
|
+
deps.setForceFreshLeaderThreadOnNextStart(false);
|
|
731
|
+
deps.stopLeaderHealth();
|
|
732
|
+
if (deps.getPollingStartedWithTelegramBus()) {
|
|
733
|
+
await deps.stopBusLeaderPolling();
|
|
734
|
+
if (expectedGeneration === generation)
|
|
735
|
+
deps.setPollingStartedWithTelegramBus(false);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await deps.stopClassicPolling();
|
|
739
|
+
};
|
|
740
|
+
const refreshFollowerState = async () => {
|
|
741
|
+
if (deps.topicTargetStore.refresh) {
|
|
742
|
+
await deps.topicTargetStore.refresh();
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
await deps.topicTargetStore.load();
|
|
746
|
+
}
|
|
747
|
+
return deps.topicTargetStore.getBotState().threadMode === "enabled";
|
|
748
|
+
};
|
|
749
|
+
const registerFollowerWithOwner = async (ctx, owner) => {
|
|
750
|
+
if (!(await refreshFollowerState()))
|
|
751
|
+
return undefined;
|
|
752
|
+
return deps.registerFollowerWithLeader(ctx, owner);
|
|
753
|
+
};
|
|
754
|
+
const restoreFollowerWithOwner = async (ctx, owner) => {
|
|
755
|
+
if (!(await refreshFollowerState()))
|
|
756
|
+
return undefined;
|
|
757
|
+
if (!deps.hasRememberedWorkspaceBinding?.(ctx))
|
|
758
|
+
return undefined;
|
|
759
|
+
return deps.restoreFollowerWithLeader?.(ctx, owner);
|
|
760
|
+
};
|
|
761
|
+
return {
|
|
762
|
+
startPolling,
|
|
763
|
+
stopPolling,
|
|
764
|
+
registerFollowerWithOwner,
|
|
765
|
+
restoreFollowerWithOwner,
|
|
766
|
+
stopFollowerRegistration: deps.stopFollowerRegistration,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
export function createTelegramThreadTargetObservationHandler(deps) {
|
|
770
|
+
let transitionPending = false;
|
|
771
|
+
return async (ctx) => {
|
|
772
|
+
if (transitionPending)
|
|
773
|
+
return;
|
|
774
|
+
if (deps.topicTargetStore.getBotState().threadMode === "enabled")
|
|
775
|
+
return;
|
|
776
|
+
transitionPending = true;
|
|
777
|
+
const isCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
778
|
+
try {
|
|
779
|
+
await applyTelegramThreadCapability(ctx, true, "thread-target-observed", deps, isCurrent);
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
if (!isCurrent())
|
|
783
|
+
return;
|
|
784
|
+
deps.recordEvent("bus", error, { phase: "thread-target-observed" });
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
transitionPending = false;
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
export function canProbeTelegramThreadCapability(ctx, deps) {
|
|
792
|
+
return deps.ownsLock(ctx) || deps.isFollowerRegistered?.() === true;
|
|
793
|
+
}
|
|
794
|
+
export function createTelegramThreadCapabilityMonitor(deps) {
|
|
795
|
+
const intervalMs = deps.intervalMs ?? TELEGRAM_THREAD_CAPABILITY_MONITOR_INTERVAL_MS;
|
|
796
|
+
let interval;
|
|
797
|
+
let generation = 0;
|
|
798
|
+
let transitionPromise;
|
|
799
|
+
let consecutiveDisabledProbes = 0;
|
|
800
|
+
const stop = () => {
|
|
801
|
+
generation += 1;
|
|
802
|
+
if (interval)
|
|
803
|
+
clearInterval(interval);
|
|
804
|
+
interval = undefined;
|
|
805
|
+
deps.lifecycle?.invalidate();
|
|
806
|
+
};
|
|
807
|
+
const check = (ctx) => {
|
|
808
|
+
if (transitionPromise || !canProbeTelegramThreadCapability(ctx, deps)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const expectedGeneration = generation;
|
|
812
|
+
const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
|
|
813
|
+
const isCurrent = () => generation === expectedGeneration && isLifecycleCurrent();
|
|
814
|
+
let tracked;
|
|
815
|
+
tracked = readTelegramThreadCapability(deps)
|
|
816
|
+
.then(async (threadModeEnabled) => {
|
|
817
|
+
if (!isCurrent())
|
|
818
|
+
return;
|
|
819
|
+
if (threadModeEnabled === undefined) {
|
|
820
|
+
if (deps.topicTargetStore.getBotState().threadMode !== "enabled" &&
|
|
821
|
+
!deps.getPollingStartedWithTelegramBus() &&
|
|
822
|
+
deps.ownsLock(ctx)) {
|
|
823
|
+
await applyTelegramThreadCapability(ctx, true, "capability-monitor-retry", deps, isCurrent);
|
|
824
|
+
}
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
if (threadModeEnabled)
|
|
828
|
+
consecutiveDisabledProbes = 0;
|
|
829
|
+
const botState = deps.topicTargetStore.getBotState();
|
|
830
|
+
const current = botState.threadMode;
|
|
831
|
+
if (threadModeEnabled && current === "enabled")
|
|
832
|
+
return;
|
|
833
|
+
if (!threadModeEnabled && current === "disabled") {
|
|
834
|
+
if (!deps.ownsLock(ctx) ||
|
|
835
|
+
!hasTelegramClassicRestoreFailure(botState)) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
await applyTelegramThreadCapability(ctx, false, "capability-monitor-disabled-confirmed", deps, isCurrent);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
if (!threadModeEnabled &&
|
|
842
|
+
hasTelegramThreadCapabilityBindings(deps.topicTargetStore)) {
|
|
843
|
+
consecutiveDisabledProbes += 1;
|
|
844
|
+
if (consecutiveDisabledProbes <
|
|
845
|
+
TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES) {
|
|
846
|
+
deps.recordEvent("bus", "Telegram Threaded Mode probe deferred", {
|
|
847
|
+
phase: "capability-monitor-disabled",
|
|
848
|
+
reason: "active-thread-bindings-present",
|
|
849
|
+
consecutiveDisabledProbes,
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
await applyTelegramThreadCapability(ctx, threadModeEnabled, threadModeEnabled
|
|
855
|
+
? "capability-monitor-enabled"
|
|
856
|
+
: consecutiveDisabledProbes >=
|
|
857
|
+
TELEGRAM_THREAD_CAPABILITY_DISABLED_CONFIRMATION_PROBES
|
|
858
|
+
? "capability-monitor-disabled-confirmed"
|
|
859
|
+
: "capability-monitor-disabled", deps, isCurrent);
|
|
860
|
+
})
|
|
861
|
+
.catch((error) => {
|
|
862
|
+
if (!isCurrent())
|
|
863
|
+
return;
|
|
864
|
+
try {
|
|
865
|
+
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
866
|
+
}
|
|
867
|
+
catch {
|
|
868
|
+
// Monitor diagnostics cannot create an unhandled interval rejection.
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
.finally(() => {
|
|
872
|
+
if (transitionPromise === tracked)
|
|
873
|
+
transitionPromise = undefined;
|
|
874
|
+
});
|
|
875
|
+
transitionPromise = tracked;
|
|
876
|
+
};
|
|
877
|
+
return {
|
|
878
|
+
start(ctx) {
|
|
879
|
+
stop();
|
|
880
|
+
const expectedGeneration = generation;
|
|
881
|
+
interval = setInterval(() => {
|
|
882
|
+
if (generation !== expectedGeneration)
|
|
883
|
+
return;
|
|
884
|
+
try {
|
|
885
|
+
check(ctx);
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
try {
|
|
889
|
+
stop();
|
|
890
|
+
}
|
|
891
|
+
catch { /* Timer shutdown must not escape the callback. */ }
|
|
892
|
+
try {
|
|
893
|
+
deps.recordEvent("bus", error, { phase: "capability-monitor" });
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
// Monitor diagnostics cannot create an uncaught interval exception.
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}, intervalMs);
|
|
900
|
+
interval.unref?.();
|
|
901
|
+
},
|
|
902
|
+
stop,
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
export class TelegramPollingBatchValidationError extends Error {
|
|
906
|
+
constructor(message) {
|
|
907
|
+
super(message);
|
|
908
|
+
this.name = "TelegramPollingBatchValidationError";
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
export class TelegramPollingCursorBootstrapError extends Error {
|
|
912
|
+
constructor(message) {
|
|
913
|
+
super(message);
|
|
914
|
+
this.name = "TelegramPollingCursorBootstrapError";
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/** Transfer one legacy config cursor into journal authority before deleting it. */
|
|
918
|
+
export async function cutOverTelegramPollingCursor(deps) {
|
|
919
|
+
const legacyCursor = deps.getLegacyCursor();
|
|
920
|
+
if (legacyCursor === undefined)
|
|
921
|
+
return;
|
|
922
|
+
const snapshot = deps.readJournal();
|
|
923
|
+
if (snapshot.acceptedThroughUpdateId === undefined) {
|
|
924
|
+
const provenEntryCursor = snapshot.entries.at(-1)?.updateId;
|
|
925
|
+
await deps.publishJournalCursor(Math.max(legacyCursor, provenEntryCursor ?? legacyCursor));
|
|
926
|
+
}
|
|
927
|
+
await deps.removeLegacyCursor();
|
|
928
|
+
}
|
|
929
|
+
function validateTelegramPollingBatch(updates, lastUpdateId) {
|
|
930
|
+
let previousUpdateId = lastUpdateId;
|
|
931
|
+
for (const update of updates) {
|
|
932
|
+
if (!Number.isSafeInteger(update.update_id) ||
|
|
933
|
+
update.update_id < 0 ||
|
|
934
|
+
(previousUpdateId !== undefined && update.update_id <= previousUpdateId)) {
|
|
935
|
+
throw new TelegramPollingBatchValidationError(`Telegram getUpdates returned non-monotonic update id ${String(update.update_id)} after ${String(previousUpdateId)}`);
|
|
936
|
+
}
|
|
937
|
+
previousUpdateId = update.update_id;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
export async function admitTelegramPollingUpdateBatch(deps) {
|
|
941
|
+
if (deps.updates.length === 0)
|
|
942
|
+
return { updateCount: 0 };
|
|
943
|
+
const acceptedThroughUpdateId = deps.getAcceptedThroughUpdateId?.();
|
|
944
|
+
validateTelegramPollingBatch(deps.updates, acceptedThroughUpdateId);
|
|
945
|
+
const latestUpdateId = getLatestTelegramUpdateId(deps.updates);
|
|
946
|
+
if (latestUpdateId === undefined)
|
|
947
|
+
return { updateCount: 0 };
|
|
948
|
+
reportTelegramPollingPhase(deps, "persisting-journal", deps.updates[0]?.update_id);
|
|
949
|
+
await deps.appendBatch(deps.updates, latestUpdateId);
|
|
950
|
+
try {
|
|
951
|
+
deps.signalWorker();
|
|
952
|
+
}
|
|
953
|
+
catch (error) {
|
|
954
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
955
|
+
phase: "worker-signal",
|
|
956
|
+
updateCount: deps.updates.length,
|
|
957
|
+
latestUpdateId,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
return { updateCount: deps.updates.length, latestUpdateId };
|
|
961
|
+
}
|
|
962
|
+
export function sleepTelegramPollingRetry(ms, signal) {
|
|
963
|
+
if (ms <= 0 || signal?.aborted)
|
|
964
|
+
return Promise.resolve();
|
|
965
|
+
return new Promise((resolve) => {
|
|
966
|
+
let settled = false;
|
|
967
|
+
let timer;
|
|
968
|
+
const finish = () => {
|
|
969
|
+
if (settled)
|
|
970
|
+
return;
|
|
971
|
+
settled = true;
|
|
972
|
+
signal?.removeEventListener("abort", onAbort);
|
|
973
|
+
resolve();
|
|
974
|
+
};
|
|
975
|
+
const onAbort = () => {
|
|
976
|
+
clearTimeout(timer);
|
|
977
|
+
finish();
|
|
978
|
+
};
|
|
979
|
+
timer = setTimeout(finish, ms);
|
|
980
|
+
timer.unref?.();
|
|
981
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
982
|
+
if (signal?.aborted)
|
|
983
|
+
onAbort();
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
export function createTelegramPollLoopRunner(deps) {
|
|
987
|
+
const sleep = deps.sleep ?? sleepTelegramPollingRetry;
|
|
988
|
+
return (ctx, signal) => runTelegramPollLoop({
|
|
989
|
+
ctx,
|
|
990
|
+
signal,
|
|
991
|
+
config: deps.getConfig(),
|
|
992
|
+
deleteWebhook: deps.deleteWebhook,
|
|
993
|
+
getUpdates: deps.getUpdates,
|
|
994
|
+
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
995
|
+
persistConfig: deps.persistConfig,
|
|
996
|
+
appendUpdateBatch: deps.appendUpdateBatch,
|
|
997
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
998
|
+
getJournalEntryCount: deps.getJournalEntryCount,
|
|
999
|
+
signalUpdateWorker: deps.signalUpdateWorker,
|
|
1000
|
+
onErrorStatus: (message) => {
|
|
1001
|
+
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
1002
|
+
message,
|
|
1003
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1004
|
+
});
|
|
1005
|
+
},
|
|
1006
|
+
onStatusReset: () => {
|
|
1007
|
+
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
1008
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1009
|
+
});
|
|
1010
|
+
},
|
|
1011
|
+
sleep,
|
|
1012
|
+
onPhaseChange: deps.onPhaseChange,
|
|
1013
|
+
onSuccessfulResponse: deps.onSuccessfulResponse,
|
|
1014
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
function getTelegramPollingErrorMessage(error) {
|
|
1018
|
+
return error instanceof Error ? error.message : String(error);
|
|
1019
|
+
}
|
|
1020
|
+
export function isTelegramGetUpdatesConflictError(error) {
|
|
1021
|
+
return getTelegramPollingErrorMessage(error).includes("Conflict: terminated by other getUpdates request");
|
|
1022
|
+
}
|
|
1023
|
+
function reportTelegramPollingPhase(deps, phase, currentUpdateId) {
|
|
1024
|
+
try {
|
|
1025
|
+
deps.onPhaseChange?.(phase, currentUpdateId);
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1029
|
+
phase: "phase-observer",
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function reportTelegramPollingResponse(deps, updateCount) {
|
|
1034
|
+
try {
|
|
1035
|
+
deps.onSuccessfulResponse?.(updateCount);
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1039
|
+
phase: "response-observer",
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function getTelegramPollingAbortReason(signal) {
|
|
1044
|
+
return signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
1045
|
+
}
|
|
1046
|
+
async function requestTelegramUpdatesWithinBudget(deps, body) {
|
|
1047
|
+
if (deps.signal.aborted)
|
|
1048
|
+
throw getTelegramPollingAbortReason(deps.signal);
|
|
1049
|
+
const configuredBudgetMs = deps.getUpdatesRequestBudgetMs?.(body);
|
|
1050
|
+
const timeoutMs = typeof configuredBudgetMs === "number" &&
|
|
1051
|
+
Number.isFinite(configuredBudgetMs) &&
|
|
1052
|
+
configuredBudgetMs > 0
|
|
1053
|
+
? Math.floor(configuredBudgetMs)
|
|
1054
|
+
: getTelegramGetUpdatesRequestBudgetMs(body);
|
|
1055
|
+
const controller = new AbortController();
|
|
1056
|
+
const abortFromOwner = () => {
|
|
1057
|
+
controller.abort(getTelegramPollingAbortReason(deps.signal));
|
|
1058
|
+
};
|
|
1059
|
+
deps.signal.addEventListener("abort", abortFromOwner, { once: true });
|
|
1060
|
+
if (deps.signal.aborted)
|
|
1061
|
+
abortFromOwner();
|
|
1062
|
+
const timeout = setTimeout(() => {
|
|
1063
|
+
controller.abort(new TelegramGetUpdatesTimeoutError(timeoutMs));
|
|
1064
|
+
}, timeoutMs);
|
|
1065
|
+
let onRequestAbort;
|
|
1066
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
1067
|
+
onRequestAbort = () => reject(getTelegramPollingAbortReason(controller.signal));
|
|
1068
|
+
controller.signal.addEventListener("abort", onRequestAbort, {
|
|
1069
|
+
once: true,
|
|
1070
|
+
});
|
|
1071
|
+
if (controller.signal.aborted)
|
|
1072
|
+
onRequestAbort();
|
|
1073
|
+
});
|
|
1074
|
+
const operation = Promise.resolve()
|
|
1075
|
+
.then(() => {
|
|
1076
|
+
if (controller.signal.aborted) {
|
|
1077
|
+
throw getTelegramPollingAbortReason(controller.signal);
|
|
1078
|
+
}
|
|
1079
|
+
return deps.getUpdates(body, controller.signal);
|
|
1080
|
+
})
|
|
1081
|
+
.catch((error) => {
|
|
1082
|
+
if (controller.signal.aborted) {
|
|
1083
|
+
throw getTelegramPollingAbortReason(controller.signal);
|
|
1084
|
+
}
|
|
1085
|
+
throw error;
|
|
1086
|
+
});
|
|
1087
|
+
try {
|
|
1088
|
+
return await Promise.race([operation, aborted]);
|
|
1089
|
+
}
|
|
1090
|
+
finally {
|
|
1091
|
+
clearTimeout(timeout);
|
|
1092
|
+
deps.signal.removeEventListener("abort", abortFromOwner);
|
|
1093
|
+
if (onRequestAbort) {
|
|
1094
|
+
controller.signal.removeEventListener("abort", onRequestAbort);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
export async function runTelegramPollLoop(deps) {
|
|
1099
|
+
if (!deps.config.botToken)
|
|
1100
|
+
return;
|
|
1101
|
+
let consecutiveGetUpdatesConflicts = 0;
|
|
1102
|
+
const retryConflict = async () => {
|
|
1103
|
+
consecutiveGetUpdatesConflicts += 1;
|
|
1104
|
+
if (consecutiveGetUpdatesConflicts >= TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT) {
|
|
1105
|
+
throw new TelegramPersistentGetUpdatesConflictError(consecutiveGetUpdatesConflicts);
|
|
1106
|
+
}
|
|
1107
|
+
await deps.sleep(consecutiveGetUpdatesConflicts < TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
|
|
1108
|
+
? TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS
|
|
1109
|
+
: TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS, deps.signal);
|
|
1110
|
+
};
|
|
1111
|
+
try {
|
|
1112
|
+
await deps.deleteWebhook(deps.signal);
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
// ignore
|
|
1116
|
+
}
|
|
1117
|
+
if (deps.getAcceptedThroughUpdateId?.() === undefined &&
|
|
1118
|
+
deps.getJournalEntryCount() > 0) {
|
|
1119
|
+
throw new TelegramPollingCursorBootstrapError("Telegram polling cursor is missing while the durable update journal is non-empty.");
|
|
1120
|
+
}
|
|
1121
|
+
if (deps.getAcceptedThroughUpdateId?.() === undefined) {
|
|
1122
|
+
try {
|
|
1123
|
+
const request = buildTelegramInitialSyncRequest();
|
|
1124
|
+
reportTelegramPollingPhase(deps, "long-poll");
|
|
1125
|
+
const updates = await requestTelegramUpdatesWithinBudget(deps, request);
|
|
1126
|
+
reportTelegramPollingResponse(deps, updates.length);
|
|
1127
|
+
const lastUpdateId = getLatestTelegramUpdateId(updates);
|
|
1128
|
+
if (lastUpdateId !== undefined) {
|
|
1129
|
+
reportTelegramPollingPhase(deps, "persisting-offset", lastUpdateId);
|
|
1130
|
+
await deps.appendUpdateBatch([], lastUpdateId);
|
|
1131
|
+
deps.recordRuntimeEvent?.("polling", new Error("Initialized Telegram cursor without executing history."), { phase: "cursor-bootstrap", lastUpdateId });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
catch (error) {
|
|
1135
|
+
if (shouldStopTelegramPolling(deps.signal.aborted, error))
|
|
1136
|
+
return;
|
|
1137
|
+
reportTelegramPollingPhase(deps, "retrying");
|
|
1138
|
+
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1139
|
+
await retryConflict();
|
|
1140
|
+
}
|
|
1141
|
+
else
|
|
1142
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1143
|
+
phase: "initial-sync",
|
|
1144
|
+
...(error instanceof TelegramGetUpdatesTimeoutError
|
|
1145
|
+
? { timeoutMs: error.timeoutMs }
|
|
1146
|
+
: {}),
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
let currentUpdateId;
|
|
1151
|
+
while (!deps.signal.aborted) {
|
|
1152
|
+
try {
|
|
1153
|
+
currentUpdateId = undefined;
|
|
1154
|
+
const request = buildTelegramLongPollRequest(deps.getAcceptedThroughUpdateId?.());
|
|
1155
|
+
reportTelegramPollingPhase(deps, "long-poll");
|
|
1156
|
+
const updates = await requestTelegramUpdatesWithinBudget(deps, request);
|
|
1157
|
+
reportTelegramPollingResponse(deps, updates.length);
|
|
1158
|
+
consecutiveGetUpdatesConflicts = 0;
|
|
1159
|
+
currentUpdateId = updates[0]?.update_id;
|
|
1160
|
+
await admitTelegramPollingUpdateBatch({
|
|
1161
|
+
updates,
|
|
1162
|
+
config: deps.config,
|
|
1163
|
+
appendBatch: deps.appendUpdateBatch,
|
|
1164
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
1165
|
+
persistConfig: deps.persistConfig,
|
|
1166
|
+
signalWorker: deps.signalUpdateWorker,
|
|
1167
|
+
onPhaseChange: deps.onPhaseChange,
|
|
1168
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1169
|
+
});
|
|
1170
|
+
currentUpdateId = undefined;
|
|
1171
|
+
}
|
|
1172
|
+
catch (error) {
|
|
1173
|
+
if (shouldStopTelegramPolling(deps.signal.aborted, error))
|
|
1174
|
+
return;
|
|
1175
|
+
reportTelegramPollingPhase(deps, "retrying", currentUpdateId);
|
|
1176
|
+
if (isTelegramGetUpdatesConflictError(error)) {
|
|
1177
|
+
await retryConflict();
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
deps.recordRuntimeEvent?.("polling", error, {
|
|
1181
|
+
phase: error instanceof TelegramGetUpdatesTimeoutError
|
|
1182
|
+
? "long-poll"
|
|
1183
|
+
: "loop",
|
|
1184
|
+
...(error instanceof TelegramGetUpdatesTimeoutError
|
|
1185
|
+
? { timeoutMs: error.timeoutMs }
|
|
1186
|
+
: {}),
|
|
1187
|
+
});
|
|
1188
|
+
consecutiveGetUpdatesConflicts = 0;
|
|
1189
|
+
deps.onErrorStatus(getTelegramPollingErrorMessage(error));
|
|
1190
|
+
await deps.sleep(TELEGRAM_POLLING_RETRY_MS, deps.signal);
|
|
1191
|
+
if (deps.signal.aborted)
|
|
1192
|
+
return;
|
|
1193
|
+
deps.onStatusReset();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|