@llblab/pi-telegram 0.21.1 → 0.22.1
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 +7 -4
- package/BACKLOG.md +29 -1
- package/CHANGELOG.md +479 -452
- package/docs/architecture.md +12 -10
- package/docs/delivery.md +2 -1
- package/docs/locks.md +13 -5
- package/docs/multi-instance-bus.md +5 -5
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +854 -57
- package/lib/logs.ts +185 -22
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/bus-leader.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import * as Sync from "./sync.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
isTelegramApiCommitUnknownError,
|
|
12
|
+
type TelegramApiCallOptions,
|
|
13
|
+
} from "./telegram-api.ts";
|
|
11
14
|
import type { TelegramTarget } from "./target.ts";
|
|
12
15
|
import * as Threads from "./threads.ts";
|
|
13
16
|
import {
|
|
@@ -65,8 +68,7 @@ export interface TelegramBusLeaderTargetProvisionerDeps<TContext> {
|
|
|
65
68
|
) => Promise<TResponse>;
|
|
66
69
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
67
70
|
getThreadReconciliationMachineState?: () =>
|
|
68
|
-
|
|
69
|
-
| undefined;
|
|
71
|
+
ThreadReconciler.ThreadReconciliationMachineState | undefined;
|
|
70
72
|
recordThreadReconciliationPlan?: (
|
|
71
73
|
plan: ThreadReconciler.ThreadReconciliationPlan,
|
|
72
74
|
) => void;
|
|
@@ -221,6 +223,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
221
223
|
provisionFollowerTarget: createTelegramBusFollowerTargetProvisioner({
|
|
222
224
|
...provisionerPorts,
|
|
223
225
|
}),
|
|
226
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
224
227
|
reconcileFollowerBindings:
|
|
225
228
|
createTelegramBusFollowerBindingRealityReconciler({
|
|
226
229
|
topicTargetStore: deps.topicTargetStore,
|
|
@@ -273,6 +276,7 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
|
|
|
273
276
|
|
|
274
277
|
export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
275
278
|
socketPath: TelegramBusSocketPathSource;
|
|
279
|
+
commitEndpointPublication?: (commit: () => void) => boolean;
|
|
276
280
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
277
281
|
authSecret?: string;
|
|
278
282
|
startPolling: (ctx: TContext) => void | Promise<void>;
|
|
@@ -287,6 +291,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
287
291
|
provisionFollowerTarget?: (
|
|
288
292
|
registration: TelegramBusInstanceRegistration,
|
|
289
293
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
294
|
+
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
290
295
|
reconcileFollowerBindings?: () => Promise<unknown> | unknown;
|
|
291
296
|
provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
|
|
292
297
|
getNowMs?: () => number;
|
|
@@ -308,8 +313,7 @@ export function createTelegramBusFollowerBindingRealityReconciler(
|
|
|
308
313
|
): () => Promise<number> {
|
|
309
314
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
310
315
|
const recentBindingGraceMs =
|
|
311
|
-
deps.recentBindingGraceMs ??
|
|
312
|
-
TELEGRAM_BUS_RECENT_FOLLOWER_BINDING_GRACE_MS;
|
|
316
|
+
deps.recentBindingGraceMs ?? TELEGRAM_BUS_RECENT_FOLLOWER_BINDING_GRACE_MS;
|
|
313
317
|
return async () => {
|
|
314
318
|
await deps.topicTargetStore.load();
|
|
315
319
|
const nowMs = getNowMs();
|
|
@@ -339,11 +343,13 @@ export function createTelegramBusFollowerBindingRealityReconciler(
|
|
|
339
343
|
(record.owner
|
|
340
344
|
? Threads.getTelegramThreadOwnerKey(record.owner)
|
|
341
345
|
: undefined);
|
|
342
|
-
if (profileKey)
|
|
346
|
+
if (profileKey)
|
|
347
|
+
deps.topicTargetStore.forgetIdentityByProfileKey(profileKey);
|
|
343
348
|
}
|
|
344
349
|
if (removed === 0) return 0;
|
|
345
|
-
const cursorRealigned =
|
|
346
|
-
|
|
350
|
+
const cursorRealigned = Threads.reconcileTelegramFreshAllocationCursor(
|
|
351
|
+
deps.topicTargetStore,
|
|
352
|
+
);
|
|
347
353
|
await deps.topicTargetStore.persist();
|
|
348
354
|
deps.recordRuntimeEvent(
|
|
349
355
|
"bus",
|
|
@@ -443,7 +449,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
443
449
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
444
450
|
const pendingRegistrations = new Map<
|
|
445
451
|
string,
|
|
446
|
-
Promise<
|
|
452
|
+
Promise<
|
|
453
|
+
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
454
|
+
>
|
|
447
455
|
>();
|
|
448
456
|
return async (registration) => {
|
|
449
457
|
const registrationStartedAtMs = Date.now();
|
|
@@ -574,125 +582,133 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
574
582
|
const runRegistration = async (): Promise<
|
|
575
583
|
(TelegramTarget & { slot?: string; threadName?: string }) | undefined
|
|
576
584
|
> => {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
585
|
+
let result = reconnectRecord
|
|
586
|
+
? {
|
|
587
|
+
target: reconnectRecord.target,
|
|
588
|
+
reused: true,
|
|
589
|
+
record: reconnectRecord,
|
|
590
|
+
}
|
|
591
|
+
: recoverableTarget
|
|
592
|
+
? await recoverRequestedTarget()
|
|
593
|
+
: await provisionTarget();
|
|
594
|
+
if (reconnectRecord) {
|
|
595
|
+
const nowMs = getNowMs();
|
|
596
|
+
const transferredRecord = deps.topicTargetStore.upsert({
|
|
597
|
+
...reconnectRecord,
|
|
598
|
+
instanceId: registration.instanceId,
|
|
599
|
+
updatedAtMs: nowMs,
|
|
600
|
+
lastSyncObservedAtMs: nowMs,
|
|
601
|
+
lastReconcileAction:
|
|
602
|
+
reconnectRecord.instanceId === registration.instanceId
|
|
603
|
+
? "follower-register-reuse"
|
|
604
|
+
: "follower-session-handoff",
|
|
605
|
+
});
|
|
606
|
+
await deps.topicTargetStore.persist();
|
|
607
|
+
result = {
|
|
608
|
+
target: transferredRecord.target,
|
|
609
|
+
reused: true,
|
|
610
|
+
record: transferredRecord,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
deps.setSyncState(
|
|
614
|
+
Sync.markTelegramSyncSliceFresh(
|
|
615
|
+
deps.getSyncState(),
|
|
616
|
+
"target-bindings",
|
|
617
|
+
{
|
|
618
|
+
nowMs: getNowMs(),
|
|
619
|
+
action: "follower-register",
|
|
620
|
+
},
|
|
621
|
+
),
|
|
622
|
+
);
|
|
623
|
+
const connectedAnnouncement = result.reused
|
|
624
|
+
? undefined
|
|
625
|
+
: createTelegramBusInstanceLifecycleAnnouncement({
|
|
626
|
+
target: result.target,
|
|
627
|
+
threadName: result.record.threadName,
|
|
628
|
+
slot: result.record.slot,
|
|
629
|
+
state: "connected",
|
|
630
|
+
});
|
|
631
|
+
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
632
|
+
phase: "follower-register-critical",
|
|
633
|
+
elapsedMs: Date.now() - registrationStartedAtMs,
|
|
586
634
|
instanceId: registration.instanceId,
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
lastReconcileAction:
|
|
590
|
-
reconnectRecord.instanceId === registration.instanceId
|
|
591
|
-
? "follower-register-reuse"
|
|
592
|
-
: "follower-session-handoff",
|
|
635
|
+
target: result.target,
|
|
636
|
+
reused: result.reused,
|
|
593
637
|
});
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
state: "connected",
|
|
614
|
-
});
|
|
615
|
-
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
616
|
-
phase: "follower-register-critical",
|
|
617
|
-
elapsedMs: Date.now() - registrationStartedAtMs,
|
|
618
|
-
instanceId: registration.instanceId,
|
|
619
|
-
target: result.target,
|
|
620
|
-
reused: result.reused,
|
|
621
|
-
});
|
|
622
|
-
scheduleTelegramBusLeaderBackgroundTask(async () => {
|
|
623
|
-
const backgroundStartedAtMs = Date.now();
|
|
624
|
-
if (connectedAnnouncement) {
|
|
638
|
+
scheduleTelegramBusLeaderBackgroundTask(async () => {
|
|
639
|
+
const backgroundStartedAtMs = Date.now();
|
|
640
|
+
if (connectedAnnouncement) {
|
|
641
|
+
try {
|
|
642
|
+
await deps.callApi("sendMessage", {
|
|
643
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
644
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
645
|
+
text: connectedAnnouncement.text,
|
|
646
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
647
|
+
});
|
|
648
|
+
} catch (error) {
|
|
649
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
650
|
+
phase: "follower-topic-announce",
|
|
651
|
+
instanceId: registration.instanceId,
|
|
652
|
+
chatId: result.target.chatId,
|
|
653
|
+
threadId: result.target.threadId,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
625
657
|
try {
|
|
626
|
-
await
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
658
|
+
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
659
|
+
ThreadReconciler.planThreadReconciliation({
|
|
660
|
+
nowMs: getNowMs(),
|
|
661
|
+
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
662
|
+
records: recordsBeforeProvision,
|
|
663
|
+
pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
|
|
664
|
+
replacedBindings: [
|
|
665
|
+
{
|
|
666
|
+
instanceId: registration.instanceId,
|
|
667
|
+
replacementTarget: result.target,
|
|
668
|
+
},
|
|
669
|
+
],
|
|
670
|
+
}),
|
|
671
|
+
{
|
|
672
|
+
callApi: deps.callApi,
|
|
673
|
+
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
674
|
+
return deps.topicTargetStore.markStaleByTarget(
|
|
675
|
+
target,
|
|
676
|
+
syncStatus,
|
|
677
|
+
lastSyncError,
|
|
678
|
+
);
|
|
679
|
+
},
|
|
680
|
+
persist() {
|
|
681
|
+
return deps.topicTargetStore.persist();
|
|
682
|
+
},
|
|
683
|
+
removePendingProvisionById(id) {
|
|
684
|
+
return deps.topicTargetStore.removePendingProvision(id);
|
|
685
|
+
},
|
|
686
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
687
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
688
|
+
},
|
|
689
|
+
);
|
|
690
|
+
await deps.topicTargetStore.persist();
|
|
632
691
|
} catch (error) {
|
|
633
692
|
deps.recordRuntimeEvent("telegram", error, {
|
|
634
|
-
phase: "follower-
|
|
693
|
+
phase: "follower-register-background-reconcile",
|
|
635
694
|
instanceId: registration.instanceId,
|
|
636
695
|
chatId: result.target.chatId,
|
|
637
696
|
threadId: result.target.threadId,
|
|
638
697
|
});
|
|
639
698
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
ThreadReconciler.planThreadReconciliation({
|
|
644
|
-
nowMs: getNowMs(),
|
|
645
|
-
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
646
|
-
records: recordsBeforeProvision,
|
|
647
|
-
pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
|
|
648
|
-
replacedBindings: [
|
|
649
|
-
{
|
|
650
|
-
instanceId: registration.instanceId,
|
|
651
|
-
replacementTarget: result.target,
|
|
652
|
-
},
|
|
653
|
-
],
|
|
654
|
-
}),
|
|
655
|
-
{
|
|
656
|
-
callApi: deps.callApi,
|
|
657
|
-
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
658
|
-
return deps.topicTargetStore.markStaleByTarget(
|
|
659
|
-
target,
|
|
660
|
-
syncStatus,
|
|
661
|
-
lastSyncError,
|
|
662
|
-
);
|
|
663
|
-
},
|
|
664
|
-
persist() {
|
|
665
|
-
return deps.topicTargetStore.persist();
|
|
666
|
-
},
|
|
667
|
-
removePendingProvisionById(id) {
|
|
668
|
-
return deps.topicTargetStore.removePendingProvision(id);
|
|
669
|
-
},
|
|
670
|
-
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
671
|
-
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
672
|
-
},
|
|
673
|
-
);
|
|
674
|
-
await deps.topicTargetStore.persist();
|
|
675
|
-
} catch (error) {
|
|
676
|
-
deps.recordRuntimeEvent("telegram", error, {
|
|
677
|
-
phase: "follower-register-background-reconcile",
|
|
699
|
+
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
700
|
+
phase: "follower-register-background",
|
|
701
|
+
elapsedMs: Date.now() - backgroundStartedAtMs,
|
|
678
702
|
instanceId: registration.instanceId,
|
|
679
|
-
|
|
680
|
-
|
|
703
|
+
target: result.target,
|
|
704
|
+
reused: result.reused,
|
|
681
705
|
});
|
|
682
|
-
}
|
|
683
|
-
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
684
|
-
phase: "follower-register-background",
|
|
685
|
-
elapsedMs: Date.now() - backgroundStartedAtMs,
|
|
686
|
-
instanceId: registration.instanceId,
|
|
687
|
-
target: result.target,
|
|
688
|
-
reused: result.reused,
|
|
689
706
|
});
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
};
|
|
707
|
+
return {
|
|
708
|
+
...result.target,
|
|
709
|
+
slot: result.record.slot,
|
|
710
|
+
threadName: result.record.threadName,
|
|
711
|
+
};
|
|
696
712
|
};
|
|
697
713
|
const registrationPromise = runRegistration().finally(() => {
|
|
698
714
|
pendingRegistrations.delete(registrationKey);
|
|
@@ -722,6 +738,12 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
722
738
|
): (ctx: TContext) => Promise<void> {
|
|
723
739
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
724
740
|
return async (ctx) => {
|
|
741
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
742
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
"Telegram leader target provisioning requires ownership.",
|
|
745
|
+
);
|
|
746
|
+
}
|
|
725
747
|
deps.onProvisioningStart?.();
|
|
726
748
|
let ownTarget: Threads.TelegramOwnTopicProvisionResult | undefined;
|
|
727
749
|
try {
|
|
@@ -743,6 +765,12 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
743
765
|
} finally {
|
|
744
766
|
deps.onProvisioningEnd?.();
|
|
745
767
|
}
|
|
768
|
+
if (
|
|
769
|
+
deps.getCurrentLeaderEpoch &&
|
|
770
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
771
|
+
) {
|
|
772
|
+
throw new Error("Telegram leader target provisioning lost ownership.");
|
|
773
|
+
}
|
|
746
774
|
if (!ownTarget) return;
|
|
747
775
|
deps.setLeaderTarget({
|
|
748
776
|
target: ownTarget.target,
|
|
@@ -848,6 +876,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
848
876
|
provisionFollowerTarget?: (
|
|
849
877
|
registration: TelegramBusInstanceRegistration,
|
|
850
878
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
879
|
+
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
851
880
|
}): (
|
|
852
881
|
envelope: TelegramBusEnvelope,
|
|
853
882
|
) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
|
|
@@ -868,7 +897,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
868
897
|
const followerSocketPath =
|
|
869
898
|
follower?.busSocketPath ??
|
|
870
899
|
getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
|
|
871
|
-
if (follower)
|
|
900
|
+
if (follower)
|
|
901
|
+
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
872
902
|
try {
|
|
873
903
|
const response = await sendTelegramBusLocalEnvelope({
|
|
874
904
|
socketPath: followerSocketPath,
|
|
@@ -880,7 +910,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
880
910
|
}),
|
|
881
911
|
});
|
|
882
912
|
if (response?.kind === "bus.ack" && response.ok) {
|
|
883
|
-
if (follower)
|
|
913
|
+
if (follower)
|
|
914
|
+
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
884
915
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
885
916
|
}
|
|
886
917
|
const message =
|
|
@@ -913,9 +944,23 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
913
944
|
switch (envelope.kind) {
|
|
914
945
|
case "follower.register": {
|
|
915
946
|
try {
|
|
947
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
948
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
949
|
+
throw new Error(
|
|
950
|
+
"Telegram follower registration requires leader ownership.",
|
|
951
|
+
);
|
|
952
|
+
}
|
|
916
953
|
const target = await deps.provisionFollowerTarget?.(
|
|
917
954
|
envelope.registration,
|
|
918
955
|
);
|
|
956
|
+
if (
|
|
957
|
+
deps.getCurrentLeaderEpoch &&
|
|
958
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch
|
|
959
|
+
) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
"Telegram follower registration lost leader ownership.",
|
|
962
|
+
);
|
|
963
|
+
}
|
|
919
964
|
const registeredTarget = target ?? envelope.registration.target;
|
|
920
965
|
deps.followerRegistry.register({
|
|
921
966
|
...envelope.registration,
|
|
@@ -941,6 +986,18 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
941
986
|
}
|
|
942
987
|
}
|
|
943
988
|
case "follower.heartbeat": {
|
|
989
|
+
const current = deps.followerRegistry.get(envelope.instanceId);
|
|
990
|
+
if (
|
|
991
|
+
current?.registrationGeneration &&
|
|
992
|
+
envelope.registrationGeneration !== current.registrationGeneration
|
|
993
|
+
) {
|
|
994
|
+
return {
|
|
995
|
+
kind: "bus.ack",
|
|
996
|
+
requestId: envelope.requestId,
|
|
997
|
+
ok: false,
|
|
998
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
944
1001
|
const follower = deps.followerRegistry.heartbeat(
|
|
945
1002
|
envelope.instanceId,
|
|
946
1003
|
getNowMs(),
|
|
@@ -985,14 +1042,15 @@ function asInteger(value: unknown): number | undefined {
|
|
|
985
1042
|
return Number.isInteger(parsed) ? parsed : undefined;
|
|
986
1043
|
}
|
|
987
1044
|
|
|
988
|
-
function getFollowerApiMethodAndBody(
|
|
1045
|
+
function getFollowerApiMethodAndBody(
|
|
1046
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>,
|
|
1047
|
+
): {
|
|
989
1048
|
apiMethod: string;
|
|
990
1049
|
body?: Record<string, unknown>;
|
|
991
1050
|
} {
|
|
992
1051
|
if (envelope.method === "call" || envelope.method === "callMultipart") {
|
|
993
1052
|
return {
|
|
994
|
-
apiMethod:
|
|
995
|
-
typeof envelope.args[0] === "string" ? envelope.args[0] : "",
|
|
1053
|
+
apiMethod: typeof envelope.args[0] === "string" ? envelope.args[0] : "",
|
|
996
1054
|
body: asRecord(envelope.args[1]),
|
|
997
1055
|
};
|
|
998
1056
|
}
|
|
@@ -1026,7 +1084,8 @@ function recordFollowerApiMessageOwnership(input: {
|
|
|
1026
1084
|
}
|
|
1027
1085
|
const chatId = asInteger(body?.chat_id) ?? input.follower.target?.chatId;
|
|
1028
1086
|
if (chatId === undefined) return;
|
|
1029
|
-
const threadId =
|
|
1087
|
+
const threadId =
|
|
1088
|
+
asInteger(body?.message_thread_id) ?? input.follower.target?.threadId;
|
|
1030
1089
|
const target = threadId !== undefined ? { chatId, threadId } : { chatId };
|
|
1031
1090
|
for (const messageId of getSentMessageIds(input.result)) {
|
|
1032
1091
|
input.record({
|
|
@@ -1061,6 +1120,17 @@ async function handleFollowerApiCall(
|
|
|
1061
1120
|
message: "Unknown Telegram bus follower instance.",
|
|
1062
1121
|
};
|
|
1063
1122
|
}
|
|
1123
|
+
if (
|
|
1124
|
+
follower.registrationGeneration &&
|
|
1125
|
+
envelope.registrationGeneration !== follower.registrationGeneration
|
|
1126
|
+
) {
|
|
1127
|
+
return {
|
|
1128
|
+
kind: "bus.ack",
|
|
1129
|
+
requestId: envelope.requestId,
|
|
1130
|
+
ok: false,
|
|
1131
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1064
1134
|
deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
|
|
1065
1135
|
if (
|
|
1066
1136
|
deps.authorizeFollowerApiCall &&
|
|
@@ -1108,6 +1178,14 @@ async function handleFollowerApiCall(
|
|
|
1108
1178
|
error instanceof Error
|
|
1109
1179
|
? error.message
|
|
1110
1180
|
: "Telegram bus API call failed.",
|
|
1181
|
+
...(isTelegramApiCommitUnknownError(error)
|
|
1182
|
+
? {
|
|
1183
|
+
error: {
|
|
1184
|
+
code: "commit-unknown" as const,
|
|
1185
|
+
method: error.method,
|
|
1186
|
+
},
|
|
1187
|
+
}
|
|
1188
|
+
: {}),
|
|
1111
1189
|
};
|
|
1112
1190
|
}
|
|
1113
1191
|
}
|
|
@@ -1237,6 +1315,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1237
1315
|
};
|
|
1238
1316
|
const localServer = createTelegramBusLocalServer({
|
|
1239
1317
|
socketPath: deps.socketPath,
|
|
1318
|
+
commitEndpointPublication: deps.commitEndpointPublication,
|
|
1240
1319
|
recordTransportEvent(phase, details) {
|
|
1241
1320
|
deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
|
|
1242
1321
|
phase: `leader-${phase}`,
|
|
@@ -1251,6 +1330,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
1251
1330
|
authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
|
|
1252
1331
|
recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
|
|
1253
1332
|
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
1333
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1254
1334
|
}),
|
|
1255
1335
|
});
|
|
1256
1336
|
return {
|