@llblab/pi-telegram 0.43.1 → 0.44.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/lib/routing.ts CHANGED
@@ -448,7 +448,19 @@ async function deleteReservedTelegramTopicThroughReconciler(
448
448
  );
449
449
  }
450
450
 
451
- export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
451
+ export const TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS = 60 * 60_000;
452
+
453
+ export function isTelegramAllTabCommandExpired(
454
+ message: { date?: number; message_thread_id?: number },
455
+ nowMs = Date.now(),
456
+ ): boolean {
457
+ return message.message_thread_id === undefined &&
458
+ typeof message.date === "number" && Number.isFinite(message.date) &&
459
+ message.date > 0 && Number.isFinite(nowMs) &&
460
+ nowMs - message.date * 1000 >= TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
461
+ }
462
+
463
+ export type TelegramRoutedMessage = { date?: number } & Updates.TelegramUpdateMessage &
452
464
  Media.TelegramMediaMessage &
453
465
  Media.TelegramMediaGroupMessage &
454
466
  Commands.TelegramCommandRuntimeMessage &
@@ -738,6 +750,12 @@ export function createTelegramInboundRouteRuntime<
738
750
  messages: TMessage[];
739
751
  createdAtMs: number;
740
752
  dispatchKind: "prompt" | "command";
753
+ expiresAtMs?: number;
754
+ destinationSelected?: boolean;
755
+ selectionAttempted?: boolean;
756
+ dispatching?: boolean;
757
+ pauseExpiry?: () => void;
758
+ stopExpiry?: () => void;
741
759
  cleanup?: PendingRerouteCleanup;
742
760
  foreignRetry?: {
743
761
  instanceId: string;
@@ -822,24 +840,71 @@ export function createTelegramInboundRouteRuntime<
822
840
  ): void => {
823
841
  Updates.reportTelegramQueueAdmission(sources, receipts);
824
842
  };
843
+ const removePendingReroute = (id: string): void => {
844
+ pendingUnboundReroutes.get(id)?.stopExpiry?.();
845
+ pendingUnboundReroutes.delete(id);
846
+ };
847
+ const expirePendingCommand = (id: string, pending: PendingUnboundReroute): boolean => {
848
+ if (pending.destinationSelected || pending.expiresAtMs === undefined ||
849
+ Date.now() < pending.expiresAtMs) return false;
850
+ if (pendingUnboundReroutes.get(id) !== pending) return true;
851
+ for (const message of pending.messages) Updates.reportTelegramUpdateCompleted(message);
852
+ removePendingReroute(id);
853
+ return true;
854
+ };
855
+ const armPendingCommandExpiry = (id: string, pending: PendingUnboundReroute): void => {
856
+ if (pending.dispatchKind !== "command" || pending.sourceTarget.threadId !== undefined) return;
857
+ pending.stopExpiry?.();
858
+ const execution = Updates.getTelegramUpdateExecutionFence(pending.messages[0]);
859
+ const onAbort = () => removePendingReroute(id);
860
+ let timer: ReturnType<typeof setTimeout> | undefined;
861
+ let stopped = false;
862
+ pending.pauseExpiry = () => {
863
+ if (timer !== undefined) clearTimeout(timer);
864
+ timer = undefined;
865
+ };
866
+ pending.stopExpiry = () => {
867
+ stopped = true;
868
+ pending.pauseExpiry?.();
869
+ execution?.signal.removeEventListener("abort", onAbort);
870
+ };
871
+ execution?.signal.addEventListener("abort", onAbort, { once: true });
872
+ if (execution?.signal.aborted) {
873
+ onAbort();
874
+ return;
875
+ }
876
+ if (pending.expiresAtMs === undefined) {
877
+ const date = pending.messages[0]?.date;
878
+ if (typeof date !== "number" || !Number.isFinite(date) || date <= 0 || date * 1000 > Date.now()) return;
879
+ pending.expiresAtMs = date * 1000 + TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
880
+ }
881
+ const schedule = (): void => {
882
+ if (stopped || pending.destinationSelected || pendingUnboundReroutes.get(id) !== pending) return;
883
+ if (expirePendingCommand(id, pending)) return;
884
+ const delay = Math.min(TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS, pending.expiresAtMs! - Date.now());
885
+ timer = setTimeout(schedule, Math.max(1, delay));
886
+ timer.unref?.();
887
+ };
888
+ schedule();
889
+ };
825
890
  const prunePendingUnboundReroutes = () => {
826
891
  const nowMs = Date.now();
827
892
  for (const [id, entry] of pendingUnboundReroutes) {
828
- if (nowMs - entry.createdAtMs > 30 * 60_000) {
829
- pendingUnboundReroutes.delete(id);
893
+ if (entry.dispatchKind === "command" && entry.sourceTarget.threadId === undefined) {
894
+ expirePendingCommand(id, entry);
895
+ } else if (nowMs - entry.createdAtMs > 30 * 60_000) {
896
+ removePendingReroute(id);
830
897
  }
831
898
  }
832
- while (pendingUnboundReroutes.size > 100) {
833
- const oldest = pendingUnboundReroutes.keys().next().value;
834
- if (!oldest) break;
835
- pendingUnboundReroutes.delete(oldest);
836
- }
837
899
  };
838
900
  const storePendingUnboundReroute = (
839
901
  messages: TMessage[],
840
902
  dispatchKind: "prompt" | "command" = "prompt",
841
903
  ): string => {
842
904
  prunePendingUnboundReroutes();
905
+ if (pendingUnboundReroutes.size >= 100) {
906
+ throw new Error("Telegram route chooser capacity reached; source remains retryable.");
907
+ }
843
908
  nextUnboundRerouteId += 1;
844
909
  const id = nextUnboundRerouteId.toString(36);
845
910
  pendingUnboundReroutes.set(id, {
@@ -853,11 +918,33 @@ export function createTelegramInboundRouteRuntime<
853
918
  createdAtMs: Date.now(),
854
919
  dispatchKind,
855
920
  });
921
+ const pending = pendingUnboundReroutes.get(id)!;
922
+ armPendingCommandExpiry(id, pending);
856
923
  return id;
857
924
  };
858
925
  const rememberRerouteChooser = (id: string, messageId: number | undefined): void => {
859
926
  const pending = pendingUnboundReroutes.get(id);
860
- if (pending) pending.chooserMessageId = messageId;
927
+ if (!pending) return;
928
+ pending.chooserMessageId = messageId;
929
+ if (messageId === undefined || pending.dispatchKind !== "command" ||
930
+ pending.sourceTarget.threadId !== undefined || pending.selectionAttempted) return;
931
+ const source = pending.messages[0];
932
+ const text = source?.text?.trim();
933
+ const execution = Updates.getTelegramUpdateExecutionFence(source);
934
+ const sourceIds = Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages);
935
+ if (!text || Commands.parseTelegramCommand(text)?.name !== "start" ||
936
+ source?.from?.id === undefined || !execution?.isCurrent() || sourceIds.length !== 1 ||
937
+ expirePendingCommand(id, pending)) return;
938
+ for (const [oldId, old] of pendingUnboundReroutes) {
939
+ if (oldId === id || old.dispatchKind !== "command" || old.selectionAttempted || old.dispatching ||
940
+ old.sourceTarget.threadId !== undefined || old.sourceTarget.chatId !== pending.sourceTarget.chatId) continue;
941
+ const oldSource = old.messages[0];
942
+ const oldIds = Updates.collectTelegramAdmissionSourceUpdateIds(old.messages);
943
+ if (oldSource?.from?.id !== source.from.id || oldSource?.text?.trim() !== text ||
944
+ oldIds.length !== 1 || oldIds[0]! >= sourceIds[0]! ||
945
+ Updates.getTelegramUpdateExecutionFence(oldSource)?.signal !== execution.signal) continue;
946
+ if (Updates.reportTelegramUpdateCompleted(oldSource)) removePendingReroute(oldId);
947
+ }
861
948
  };
862
949
  const matchesRerouteChooser = (
863
950
  pending: PendingUnboundReroute,
@@ -1123,12 +1210,15 @@ export function createTelegramInboundRouteRuntime<
1123
1210
  | ((messages: TMessage[], ctx: TContext) => Promise<void>)
1124
1211
  | undefined;
1125
1212
  const dispatchPendingRerouteMessages = async (
1126
- pending: { dispatchKind: "prompt" | "command" },
1213
+ pending: Pick<PendingUnboundReroute, "dispatchKind" | "sourceTarget">,
1127
1214
  messages: TMessage[],
1128
1215
  ctx: TContext,
1129
1216
  ): Promise<void> => {
1130
1217
  if (pending.dispatchKind === "command" && dispatchReroutedCommandMessages) {
1131
1218
  await dispatchReroutedCommandMessages(messages, ctx);
1219
+ if (pending.sourceTarget.threadId === undefined) {
1220
+ for (const message of messages) Updates.reportTelegramUpdateCompleted(message);
1221
+ }
1132
1222
  return;
1133
1223
  }
1134
1224
  await promptEnqueue(messages, ctx);
@@ -1147,7 +1237,7 @@ export function createTelegramInboundRouteRuntime<
1147
1237
  );
1148
1238
  assertExecutionCurrent();
1149
1239
  if (dismissed) {
1150
- pendingUnboundReroutes.delete(rerouteId);
1240
+ removePendingReroute(rerouteId);
1151
1241
  await deps.answerCallbackQuery(query.id, successMessage);
1152
1242
  return;
1153
1243
  }
@@ -1184,6 +1274,9 @@ export function createTelegramInboundRouteRuntime<
1184
1274
  outcome?.status === "fulfilled" &&
1185
1275
  outcome.value.status === "accepted"
1186
1276
  ) {
1277
+ if (pending.dispatchKind === "command" && pending.sourceTarget.threadId === undefined) {
1278
+ Updates.reportTelegramUpdateCompleted(pending.messages[index]);
1279
+ }
1187
1280
  return false;
1188
1281
  }
1189
1282
  if (outcome?.status === "rejected") {
@@ -1216,7 +1309,8 @@ export function createTelegramInboundRouteRuntime<
1216
1309
  typeof messageId !== "number" ||
1217
1310
  !deps.threadStore ||
1218
1311
  !pending ||
1219
- !matchesRerouteChooser(pending, query)
1312
+ !matchesRerouteChooser(pending, query) ||
1313
+ expirePendingCommand(parsed.rerouteId, pending)
1220
1314
  ) {
1221
1315
  await deps.answerCallbackQuery(query.id, "Message route expired.");
1222
1316
  return true;
@@ -1258,7 +1352,7 @@ export function createTelegramInboundRouteRuntime<
1258
1352
  await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
1259
1353
  return true;
1260
1354
  };
1261
- const handleUnboundRerouteCallback = async (
1355
+ const executeUnboundRerouteCallback = async (
1262
1356
  query: TCallbackQuery,
1263
1357
  ctx: TContext,
1264
1358
  ): Promise<boolean> => {
@@ -1270,12 +1364,17 @@ export function createTelegramInboundRouteRuntime<
1270
1364
  const chatId = query.message?.chat?.id;
1271
1365
  const pending = pendingUnboundReroutes.get(parsed.rerouteId);
1272
1366
  if (typeof chatId !== "number" || !deps.threadStore || !pending ||
1273
- !matchesRerouteChooser(pending, query)) {
1367
+ !matchesRerouteChooser(pending, query) || expirePendingCommand(parsed.rerouteId, pending)) {
1274
1368
  await deps.answerCallbackQuery(query.id, "Message route expired.");
1275
1369
  return true;
1276
1370
  }
1277
1371
  await deps.threadStore.load();
1278
1372
  assertExecutionCurrent();
1373
+ if (pendingUnboundReroutes.get(parsed.rerouteId) !== pending ||
1374
+ expirePendingCommand(parsed.rerouteId, pending)) {
1375
+ await deps.answerCallbackQuery(query.id, "Message route expired.");
1376
+ return true;
1377
+ }
1279
1378
  if (pending.finalizeMessage) {
1280
1379
  await finalizePendingReroute(
1281
1380
  parsed.rerouteId,
@@ -1361,6 +1460,9 @@ export function createTelegramInboundRouteRuntime<
1361
1460
  await deps.answerCallbackQuery(query.id, "🚫 Thread restore source is already owned.");
1362
1461
  return true;
1363
1462
  }
1463
+ pending.destinationSelected = true;
1464
+ pending.selectionAttempted = true;
1465
+ pending.pauseExpiry?.();
1364
1466
  const currentInstanceId = deps.getCurrentInstanceId?.();
1365
1467
  const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
1366
1468
  const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
@@ -1657,6 +1759,33 @@ export function createTelegramInboundRouteRuntime<
1657
1759
  );
1658
1760
  return true;
1659
1761
  };
1762
+ const handleUnboundRerouteCallback = async (
1763
+ query: TCallbackQuery,
1764
+ ctx: TContext,
1765
+ ): Promise<boolean> => {
1766
+ const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
1767
+ const pending = parsed && pendingUnboundReroutes.get(parsed.rerouteId);
1768
+ if (!parsed || !pending || pending.dispatchKind !== "command" ||
1769
+ pending.sourceTarget.threadId !== undefined || !matchesRerouteChooser(pending, query)) {
1770
+ return executeUnboundRerouteCallback(query, ctx);
1771
+ }
1772
+ if (pending.dispatching) {
1773
+ await deps.answerCallbackQuery(query.id, "Command routing is already in progress.");
1774
+ return true;
1775
+ }
1776
+ pending.dispatching = true;
1777
+ try {
1778
+ return await executeUnboundRerouteCallback(query, ctx);
1779
+ } finally {
1780
+ pending.dispatching = false;
1781
+ if (pendingUnboundReroutes.get(parsed.rerouteId) === pending &&
1782
+ pending.destinationSelected && pending.messages.length > 0 &&
1783
+ !pending.cleanup && !pending.finalizeMessage) {
1784
+ pending.destinationSelected = false;
1785
+ armPendingCommandExpiry(parsed.rerouteId, pending);
1786
+ }
1787
+ }
1788
+ };
1660
1789
  const callbackHandler = async (
1661
1790
  query: TCallbackQuery,
1662
1791
  ctx: TContext,
@@ -2122,11 +2251,11 @@ export function createTelegramInboundRouteRuntime<
2122
2251
  deps.getLiveThreadTargets?.(),
2123
2252
  );
2124
2253
  if (activeRecords.length === 0) return false;
2125
- const commandMessage = {
2254
+ const commandMessage = Updates.carryTelegramUpdateExecutionFence(message, {
2126
2255
  ...message,
2127
2256
  text: commandText,
2128
2257
  caption: undefined,
2129
- } as TMessage;
2258
+ } as TMessage);
2130
2259
  const rerouteId = storePendingUnboundReroute([commandMessage], "command");
2131
2260
  Updates.reportTelegramUpdateDeferred(commandMessage);
2132
2261
  const text = formatTelegramAllTabMenuChooserText(command.name);
@@ -2135,49 +2264,52 @@ export function createTelegramInboundRouteRuntime<
2135
2264
  activeRecords,
2136
2265
  { canRestore: typeof message.message_thread_id === "number" },
2137
2266
  );
2138
- if (deps.sendInteractiveMessage) {
2139
- const chooserId = await deps.sendInteractiveMessage(
2140
- message.chat.id,
2141
- text,
2142
- "html",
2143
- replyMarkup,
2144
- options.target || options.replyToSource
2145
- ? {
2146
- ...(options.target ? { target: options.target } : {}),
2147
- ...(options.replyToSource
2148
- ? { replyToMessageId: message.message_id }
2149
- : {}),
2150
- }
2151
- : undefined,
2152
- );
2153
- rememberRerouteChooser(rerouteId, chooserId);
2154
- return true;
2155
- }
2156
- if (deps.callApi) {
2157
- const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
2158
- chat_id: message.chat.id,
2159
- text,
2160
- parse_mode: "HTML",
2161
- reply_markup: replyMarkup,
2162
- ...(typeof options.target?.threadId === "number"
2163
- ? { message_thread_id: options.target.threadId }
2164
- : {}),
2165
- ...(options.replyToSource
2166
- ? {
2167
- reply_parameters: {
2168
- message_id: message.message_id,
2169
- allow_sending_without_reply: true,
2170
- },
2171
- }
2172
- : {}),
2173
- });
2174
- rememberRerouteChooser(rerouteId, chooser?.message_id);
2175
- return true;
2267
+ let chooserId: number | undefined;
2268
+ try {
2269
+ if (deps.sendInteractiveMessage) {
2270
+ chooserId = await deps.sendInteractiveMessage(
2271
+ message.chat.id,
2272
+ text,
2273
+ "html",
2274
+ replyMarkup,
2275
+ options.target || options.replyToSource
2276
+ ? {
2277
+ ...(options.target ? { target: options.target } : {}),
2278
+ ...(options.replyToSource
2279
+ ? { replyToMessageId: message.message_id }
2280
+ : {}),
2281
+ }
2282
+ : undefined,
2283
+ );
2284
+ } else if (deps.callApi) {
2285
+ const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
2286
+ chat_id: message.chat.id,
2287
+ text,
2288
+ parse_mode: "HTML",
2289
+ reply_markup: replyMarkup,
2290
+ ...(typeof options.target?.threadId === "number"
2291
+ ? { message_thread_id: options.target.threadId }
2292
+ : {}),
2293
+ ...(options.replyToSource
2294
+ ? {
2295
+ reply_parameters: {
2296
+ message_id: message.message_id,
2297
+ allow_sending_without_reply: true,
2298
+ },
2299
+ }
2300
+ : {}),
2301
+ });
2302
+ chooserId = chooser?.message_id;
2303
+ } else {
2304
+ chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
2305
+ parseMode: "HTML",
2306
+ target: options.target,
2307
+ });
2308
+ }
2309
+ } catch (error) {
2310
+ removePendingReroute(rerouteId);
2311
+ throw error;
2176
2312
  }
2177
- const chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
2178
- parseMode: "HTML",
2179
- target: options.target,
2180
- });
2181
2313
  rememberRerouteChooser(rerouteId, chooserId);
2182
2314
  return true;
2183
2315
  };
@@ -2497,6 +2629,8 @@ export function createTelegramInboundRouteRuntime<
2497
2629
  deps.getLiveThreadTargets?.(),
2498
2630
  );
2499
2631
  const command = getKnownTelegramAllTabCommand(text);
2632
+ // Returning before deferral lets the admission worker terminally settle expired replay.
2633
+ if (command && command.name !== "thread" && isTelegramAllTabCommandExpired(message)) return;
2500
2634
  if (bindings.length > 0 && command && command.name !== "thread") {
2501
2635
  if (
2502
2636
  await sendAllTabCommandChooser(command, text, message as TMessage, {
@@ -1783,11 +1783,44 @@ export function createTelegramBridgeApiRuntime(
1783
1783
  */
1784
1784
  export function createTelegramApiClient(
1785
1785
  getBotToken: () => string | undefined,
1786
- options: TelegramAnswerCallbackQueryOptions = {},
1786
+ options: TelegramAnswerCallbackQueryOptions & { now?: () => number } = {},
1787
1787
  ): TelegramApiClient {
1788
+ const now = options.now ?? Date.now;
1789
+ const draftRetryNotBeforeByTarget = new Map<string, number>();
1788
1790
  return {
1789
- call: async (method, body, options) => {
1790
- return callTelegram(getBotToken(), method, body, options);
1791
+ call: async <TResponse>(
1792
+ method: string,
1793
+ body: Record<string, unknown>,
1794
+ options?: TelegramApiCallOptions,
1795
+ ): Promise<TResponse> => {
1796
+ const token = getBotToken();
1797
+ // Cooldown keys retain only the public bot-id prefix, not the credential.
1798
+ const botId = token?.match(/^(\d+):/)?.[1];
1799
+ const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
1800
+ const draftKey = isDraft && botId
1801
+ ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
1802
+ : undefined;
1803
+ if (draftKey) {
1804
+ const nowMs = now();
1805
+ for (const [key, deadline] of draftRetryNotBeforeByTarget) {
1806
+ if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
1807
+ }
1808
+ if (draftRetryNotBeforeByTarget.has(draftKey)) return false as TResponse;
1809
+ }
1810
+ try {
1811
+ // A draft is a replaceable snapshot, not a body to replay after backoff.
1812
+ return await callTelegram<TResponse>(
1813
+ token, method, body, isDraft ? { ...options, maxAttempts: 1 } : options,
1814
+ );
1815
+ } catch (error) {
1816
+ if (draftKey && isRetryableTelegramApiError(error)) {
1817
+ draftRetryNotBeforeByTarget.set(draftKey, Math.max(
1818
+ draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
1819
+ now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500),
1820
+ ));
1821
+ }
1822
+ throw error;
1823
+ }
1791
1824
  },
1792
1825
  callMultipart: async (
1793
1826
  method,
package/lib/updates.ts CHANGED
@@ -481,12 +481,7 @@ const TELEGRAM_UPDATE_ADMISSION_BINDING = Symbol(
481
481
 
482
482
  interface TelegramUpdateAdmissionBinding {
483
483
  sourceUpdateId: number;
484
- report: (
485
- outcome: Extract<
486
- TelegramUpdateAdmissionOutcome,
487
- { kind: "deferred" | "queued" }
488
- >,
489
- ) => void;
484
+ report: (outcome: TelegramUpdateAdmissionOutcome) => void;
490
485
  }
491
486
 
492
487
  export type TelegramQueueAdmissionReceiptLike = TelegramQueueAdmissionReceipt;
@@ -589,6 +584,16 @@ export function collectTelegramAdmissionSourceUpdateIds(
589
584
  return [...sourceUpdateIds].sort((left, right) => left - right);
590
585
  }
591
586
 
587
+ /** Report source completion; true means reported, not a durable settlement acknowledgement. */
588
+ export function reportTelegramUpdateCompleted(value: unknown): boolean {
589
+ const binding = getTelegramUpdateAdmissionBinding(value);
590
+ if (!binding) return false;
591
+ const execution = getTelegramUpdateExecutionFence(value);
592
+ if (execution && !execution.isCurrent()) return false;
593
+ binding.report({ kind: "complete" });
594
+ return true;
595
+ }
596
+
592
597
  export function reportTelegramUpdateDeferred(value: unknown): boolean {
593
598
  const binding = getTelegramUpdateAdmissionBinding(value);
594
599
  if (!binding) return false;
@@ -1810,10 +1815,7 @@ export interface TelegramUpdateWorkerRuntime<TContext> {
1810
1815
  signal: () => void;
1811
1816
  settleDeferred: (input: {
1812
1817
  updateId: number;
1813
- outcome: Extract<
1814
- TelegramUpdateAdmissionOutcome,
1815
- { kind: "deferred" | "queued" }
1816
- >;
1818
+ outcome: TelegramUpdateAdmissionOutcome;
1817
1819
  signal: AbortSignal;
1818
1820
  }) => void;
1819
1821
  isQueueReceiptCommitted: (
@@ -3018,6 +3020,13 @@ export function createTelegramUpdateWorkerRuntime<TContext>(
3018
3020
  return;
3019
3021
  }
3020
3022
  const claim = claims.get(input.updateId);
3023
+ if (input.outcome.kind === "complete") {
3024
+ // Expiry may retire only a still-deferred source, never accepted queue work.
3025
+ if (claim !== "deferred") return;
3026
+ const result = commitCompletedBatch(expectedOwner, [input.updateId]);
3027
+ if (!result) transition("idle", input.updateId);
3028
+ return;
3029
+ }
3021
3030
  if (input.outcome.kind === "deferred") {
3022
3031
  if (claim === "queued") return;
3023
3032
  if (claim !== "deferred") {
@@ -3453,10 +3462,7 @@ export interface TelegramUpdateAdmissionHandleDeps<
3453
3462
  ) => Promise<void>;
3454
3463
  registry?: TelegramUpdateHandlerRegistry;
3455
3464
  onLateOutcome?: (
3456
- outcome: Extract<
3457
- TelegramUpdateAdmissionOutcome,
3458
- { kind: "deferred" | "queued" }
3459
- >,
3465
+ outcome: TelegramUpdateAdmissionOutcome,
3460
3466
  details: {
3461
3467
  updateId: number;
3462
3468
  ctx: TContext;
@@ -3467,24 +3473,15 @@ export interface TelegramUpdateAdmissionHandleDeps<
3467
3473
  }
3468
3474
 
3469
3475
  function mergeTelegramReportedAdmissionOutcome(
3470
- current:
3471
- | Extract<
3472
- TelegramUpdateAdmissionOutcome,
3473
- { kind: "deferred" | "queued" }
3474
- >
3475
- | undefined,
3476
- next: Extract<
3477
- TelegramUpdateAdmissionOutcome,
3478
- { kind: "deferred" | "queued" }
3479
- >,
3476
+ current: TelegramUpdateAdmissionOutcome | undefined,
3477
+ next: TelegramUpdateAdmissionOutcome,
3480
3478
  updateId: number,
3481
- ): Extract<
3482
- TelegramUpdateAdmissionOutcome,
3483
- { kind: "deferred" | "queued" }
3484
- > {
3479
+ ): TelegramUpdateAdmissionOutcome {
3485
3480
  if (!current || current.kind === "deferred") return next;
3486
3481
  if (next.kind === "deferred") return current;
3487
- if (areTelegramQueueAdmissionReceiptsEqual(current, next)) return current;
3482
+ if (current.kind === "complete" && next.kind === "complete") return current;
3483
+ if (current.kind === "queued" && next.kind === "queued" &&
3484
+ areTelegramQueueAdmissionReceiptsEqual(current, next)) return current;
3488
3485
  throw new TelegramUpdateAdmissionOutcomeError(
3489
3486
  `Telegram update ${updateId} reported conflicting queue outcomes.`,
3490
3487
  );
@@ -3529,12 +3526,7 @@ export function createTelegramUpdateAdmissionHandle<
3529
3526
  execution.assertCurrent();
3530
3527
  if (verdict === "consume") return { kind: "complete" };
3531
3528
  let immediate = true;
3532
- let outcome:
3533
- | Extract<
3534
- TelegramUpdateAdmissionOutcome,
3535
- { kind: "deferred" | "queued" }
3536
- >
3537
- | undefined;
3529
+ let outcome: TelegramUpdateAdmissionOutcome | undefined;
3538
3530
  const boundUpdate = bindTelegramUpdateExecutionFence(
3539
3531
  bindTelegramUpdateAdmissionSource(update, (next) => {
3540
3532
  if (immediate) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.43.1",
3
+ "version": "0.44.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"