@llblab/pi-telegram 0.18.4 → 0.18.6
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 +8 -5
- package/BACKLOG.md +13 -34
- package/CHANGELOG.md +24 -1
- package/README.md +1 -1
- package/docs/multi-instance-bus.md +26 -5
- package/index.ts +56 -4
- package/lib/bindings.ts +3 -14
- package/lib/bus-follower.ts +66 -46
- package/lib/bus-leader.ts +171 -47
- package/lib/bus-transport.ts +227 -0
- package/lib/bus.ts +214 -46
- package/lib/polling.ts +58 -5
- package/lib/preview.ts +20 -39
- package/lib/routing.ts +5 -11
- package/lib/runtime-log.ts +19 -1
- package/lib/runtime.ts +23 -7
- package/lib/status.ts +52 -2
- package/lib/threads.ts +56 -2
- package/lib/updates.ts +43 -2
- package/package.json +1 -1
package/lib/bus-follower.ts
CHANGED
|
@@ -19,11 +19,17 @@ import {
|
|
|
19
19
|
sendTelegramBusLocalEnvelope,
|
|
20
20
|
type TelegramBusEnvelope,
|
|
21
21
|
} from "./bus.ts";
|
|
22
|
+
import {
|
|
23
|
+
getTelegramBusTransportRetryPolicy,
|
|
24
|
+
TELEGRAM_BUS_REGISTRATION_RETRY,
|
|
25
|
+
} from "./bus-transport.ts";
|
|
22
26
|
|
|
23
27
|
export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
24
28
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
25
|
-
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
|
|
26
|
-
|
|
29
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
|
|
30
|
+
TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
|
|
31
|
+
export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
|
|
32
|
+
TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
|
|
27
33
|
|
|
28
34
|
const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
|
|
29
35
|
"__piTelegramFollowerSessionHandoff";
|
|
@@ -197,16 +203,25 @@ export interface TelegramBusFollowerLeaderLock {
|
|
|
197
203
|
busSecret?: string;
|
|
198
204
|
}
|
|
199
205
|
|
|
206
|
+
export interface TelegramBusFollowerPromotedBinding {
|
|
207
|
+
target?: TelegramTarget;
|
|
208
|
+
slot?: string;
|
|
209
|
+
threadName?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
200
212
|
export interface TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext> {
|
|
201
213
|
registrationState: Pick<
|
|
202
214
|
TelegramBusFollowerRegistrationState,
|
|
203
|
-
"setRegistered"
|
|
215
|
+
"getTarget" | "getSlot" | "getThreadName" | "setRegistered"
|
|
204
216
|
>;
|
|
205
217
|
getRegistrationRuntime: () => TelegramBusFollowerRegistrationRuntime<TContext>;
|
|
206
218
|
getLeaderState: () => TelegramBusFollowerLeaderState;
|
|
207
219
|
setLifecyclePhase: (phase: "electing" | undefined) => void;
|
|
208
220
|
updateStatus: (ctx: TContext) => void;
|
|
209
|
-
promoteToLeader: (
|
|
221
|
+
promoteToLeader: (
|
|
222
|
+
ctx: TContext,
|
|
223
|
+
binding: TelegramBusFollowerPromotedBinding,
|
|
224
|
+
) => Promise<void> | void;
|
|
210
225
|
sleep: (ms: number) => Promise<void>;
|
|
211
226
|
promotionGraceMs: number;
|
|
212
227
|
recordRuntimeEvent: (
|
|
@@ -341,6 +356,10 @@ export function createTelegramBusFollowerApiCaller(
|
|
|
341
356
|
const response = await sendTelegramBusLocalEnvelope({
|
|
342
357
|
socketPath: deps.socketPath,
|
|
343
358
|
timeoutMs,
|
|
359
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
360
|
+
endpoint: deps.socketPath,
|
|
361
|
+
operation: "operation",
|
|
362
|
+
}),
|
|
344
363
|
envelope: {
|
|
345
364
|
kind: "follower.callApi",
|
|
346
365
|
requestId: deps.createRequestId(),
|
|
@@ -368,19 +387,6 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
368
387
|
);
|
|
369
388
|
}
|
|
370
389
|
|
|
371
|
-
function isRetryableTelegramBusRegistrationError(error: unknown): boolean {
|
|
372
|
-
if (!(error instanceof Error)) return false;
|
|
373
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
374
|
-
return code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE";
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function delayTelegramBusFollowerRegistration(ms: number): Promise<void> {
|
|
378
|
-
return new Promise((resolve) => {
|
|
379
|
-
const timer = setTimeout(resolve, ms);
|
|
380
|
-
timer.unref?.();
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
390
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
385
391
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
386
392
|
): () => Promise<void> {
|
|
@@ -514,7 +520,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
514
520
|
return false;
|
|
515
521
|
}
|
|
516
522
|
};
|
|
517
|
-
const
|
|
523
|
+
const snapshotBinding = (): TelegramBusFollowerPromotedBinding => ({
|
|
524
|
+
target: deps.registrationState.getTarget(),
|
|
525
|
+
slot: deps.registrationState.getSlot(),
|
|
526
|
+
threadName: deps.registrationState.getThreadName(),
|
|
527
|
+
});
|
|
528
|
+
const promoteToLeader = async (
|
|
529
|
+
reason: unknown,
|
|
530
|
+
ctx: TContext,
|
|
531
|
+
binding = snapshotBinding(),
|
|
532
|
+
) => {
|
|
518
533
|
deps.setLifecyclePhase("electing");
|
|
519
534
|
safeUpdateStatus(ctx);
|
|
520
535
|
deps.recordRuntimeEvent("bus", reason, {
|
|
@@ -526,7 +541,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
526
541
|
deps.recordRuntimeEvent("bus", "Telegram follower elected for promotion", {
|
|
527
542
|
phase: "follower-promotion-electing",
|
|
528
543
|
});
|
|
529
|
-
await deps.promoteToLeader(ctx);
|
|
544
|
+
await deps.promoteToLeader(ctx, binding);
|
|
530
545
|
deps.setLifecyclePhase(undefined);
|
|
531
546
|
safeUpdateStatus(ctx);
|
|
532
547
|
deps.recordRuntimeEvent("bus", "Telegram follower promotion completed", {
|
|
@@ -537,6 +552,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
537
552
|
if (promotionPending) return;
|
|
538
553
|
promotionPending = true;
|
|
539
554
|
try {
|
|
555
|
+
const initialBinding = snapshotBinding();
|
|
540
556
|
const state = deps.getLeaderState();
|
|
541
557
|
if (state.kind === "active-elsewhere") {
|
|
542
558
|
clearRegisteredState(ctx);
|
|
@@ -568,16 +584,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
|
|
|
568
584
|
) {
|
|
569
585
|
return;
|
|
570
586
|
}
|
|
571
|
-
await promoteToLeader(error, ctx);
|
|
587
|
+
await promoteToLeader(error, ctx, initialBinding);
|
|
572
588
|
return;
|
|
573
589
|
}
|
|
574
590
|
if (graceState.kind === "stale" || graceState.kind === "inactive") {
|
|
575
|
-
await promoteToLeader(error, ctx);
|
|
591
|
+
await promoteToLeader(error, ctx, initialBinding);
|
|
576
592
|
}
|
|
577
593
|
return;
|
|
578
594
|
}
|
|
579
595
|
if (state.kind === "stale" || state.kind === "inactive") {
|
|
580
|
-
await promoteToLeader(error, ctx);
|
|
596
|
+
await promoteToLeader(error, ctx, initialBinding);
|
|
581
597
|
}
|
|
582
598
|
} catch (promotionError) {
|
|
583
599
|
deps.setLifecyclePhase(undefined);
|
|
@@ -634,6 +650,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
634
650
|
const response = await sendTelegramBusLocalEnvelope({
|
|
635
651
|
socketPath: activeLeaderSocketPath,
|
|
636
652
|
timeoutMs: deps.timeoutMs,
|
|
653
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
654
|
+
endpoint: activeLeaderSocketPath,
|
|
655
|
+
operation: "operation",
|
|
656
|
+
}),
|
|
637
657
|
envelope: {
|
|
638
658
|
kind: "follower.heartbeat",
|
|
639
659
|
requestId: deps.createRequestId(),
|
|
@@ -692,31 +712,25 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
692
712
|
});
|
|
693
713
|
let response: TelegramBusEnvelope | undefined;
|
|
694
714
|
try {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
phase: "follower-register-retry",
|
|
712
|
-
attempt,
|
|
713
|
-
socketPath: leaderSocketPath,
|
|
715
|
+
response = await sendTelegramBusLocalEnvelope({
|
|
716
|
+
socketPath: leaderSocketPath,
|
|
717
|
+
timeoutMs: registrationTimeoutMs,
|
|
718
|
+
envelope: createRegistrationEnvelope(),
|
|
719
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
720
|
+
endpoint: leaderSocketPath,
|
|
721
|
+
operation: "registration",
|
|
722
|
+
overrides: {
|
|
723
|
+
attempts: registrationRetryAttempts,
|
|
724
|
+
delayMs: registrationRetryDelayMs,
|
|
725
|
+
},
|
|
726
|
+
}),
|
|
727
|
+
recordTransportEvent(phase, details) {
|
|
728
|
+
deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
|
|
729
|
+
phase: `follower-register-${phase}`,
|
|
730
|
+
...details,
|
|
714
731
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
732
|
+
},
|
|
733
|
+
});
|
|
720
734
|
} catch (error) {
|
|
721
735
|
stopHeartbeat();
|
|
722
736
|
activeLeaderSocketPath = undefined;
|
|
@@ -781,6 +795,12 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
|
|
|
781
795
|
): TelegramBusForwardedUpdateReceiverRuntime {
|
|
782
796
|
const server = createTelegramBusLocalServer({
|
|
783
797
|
socketPath: deps.socketPath,
|
|
798
|
+
recordTransportEvent(phase, details) {
|
|
799
|
+
deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
|
|
800
|
+
phase: `follower-receiver-${phase}`,
|
|
801
|
+
...details,
|
|
802
|
+
});
|
|
803
|
+
},
|
|
784
804
|
async handleEnvelope(envelope) {
|
|
785
805
|
const authSecret = deps.getAuthSecret?.();
|
|
786
806
|
if (deps.getAuthSecret && (!authSecret || envelope.auth !== authSecret)) {
|
package/lib/bus-leader.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type TelegramBusFollowerView,
|
|
22
22
|
type TelegramBusInstanceRegistration,
|
|
23
23
|
} from "./bus.ts";
|
|
24
|
+
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
24
25
|
|
|
25
26
|
export interface TelegramBusLeaderRuntime<TContext> {
|
|
26
27
|
startPolling: (ctx: TContext) => Promise<void>;
|
|
@@ -162,6 +163,17 @@ export interface TelegramBusFollowerRegistryRestoreDeps {
|
|
|
162
163
|
getNowMs?: () => number;
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
export interface TelegramBusFollowerMessageOwnershipRecord {
|
|
167
|
+
follower: TelegramBusFollowerView;
|
|
168
|
+
chatId: number;
|
|
169
|
+
messageId: number;
|
|
170
|
+
target?: TelegramTarget;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export type TelegramBusFollowerMessageOwnershipRecorder = (
|
|
174
|
+
record: TelegramBusFollowerMessageOwnershipRecord,
|
|
175
|
+
) => void;
|
|
176
|
+
|
|
165
177
|
export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
166
178
|
socketPath: string;
|
|
167
179
|
followerRegistry: TelegramBusFollowerRegistry;
|
|
@@ -174,6 +186,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
174
186
|
method: string;
|
|
175
187
|
args: unknown[];
|
|
176
188
|
}) => boolean;
|
|
189
|
+
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
177
190
|
provisionFollowerTarget?: (
|
|
178
191
|
registration: TelegramBusInstanceRegistration,
|
|
179
192
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
@@ -324,30 +337,74 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
324
337
|
registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
325
338
|
const followerOwner =
|
|
326
339
|
Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
340
|
+
const provisionTarget = async () => {
|
|
341
|
+
deps.onProvisioningStart?.();
|
|
342
|
+
try {
|
|
343
|
+
return await provision({
|
|
344
|
+
instanceId: registration.instanceId,
|
|
345
|
+
owner:
|
|
346
|
+
followerOwner.kind === "manual-follower"
|
|
347
|
+
? followerOwner
|
|
348
|
+
: {
|
|
349
|
+
kind: "manual-follower",
|
|
350
|
+
instanceId: registration.instanceId,
|
|
351
|
+
},
|
|
352
|
+
profileKey: followerProfileKey,
|
|
353
|
+
threadName: registration.threadName,
|
|
354
|
+
});
|
|
355
|
+
} finally {
|
|
356
|
+
deps.onProvisioningEnd?.();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
let result = await provisionTarget();
|
|
345
360
|
deps.setSyncState(
|
|
346
361
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
347
362
|
nowMs: getNowMs(),
|
|
348
363
|
action: "follower-register",
|
|
349
364
|
}),
|
|
350
365
|
);
|
|
366
|
+
let connectedAnnouncement =
|
|
367
|
+
createTelegramBusInstanceLifecycleAnnouncement({
|
|
368
|
+
target: result.target,
|
|
369
|
+
threadName: result.record.threadName,
|
|
370
|
+
slot: result.record.slot,
|
|
371
|
+
state: "connected",
|
|
372
|
+
});
|
|
373
|
+
let connectedAnnouncementSent = false;
|
|
374
|
+
if (result.reused && connectedAnnouncement) {
|
|
375
|
+
try {
|
|
376
|
+
await deps.callApi("sendMessage", {
|
|
377
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
378
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
379
|
+
text: connectedAnnouncement.text,
|
|
380
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
381
|
+
});
|
|
382
|
+
connectedAnnouncementSent = true;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
385
|
+
phase: "follower-topic-reuse-probe",
|
|
386
|
+
instanceId: registration.instanceId,
|
|
387
|
+
chatId: result.target.chatId,
|
|
388
|
+
threadId: result.target.threadId,
|
|
389
|
+
});
|
|
390
|
+
if (Threads.isTelegramTopicTargetStaleError(error)) {
|
|
391
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
392
|
+
result.target,
|
|
393
|
+
"deleted",
|
|
394
|
+
"Follower registration found the reusable thread target stale.",
|
|
395
|
+
);
|
|
396
|
+
await deps.topicTargetStore.persist();
|
|
397
|
+
result = await provisionTarget();
|
|
398
|
+
connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
399
|
+
target: result.target,
|
|
400
|
+
threadName: result.record.threadName,
|
|
401
|
+
slot: result.record.slot,
|
|
402
|
+
state: "connected",
|
|
403
|
+
});
|
|
404
|
+
connectedAnnouncementSent = false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
351
408
|
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
352
409
|
phase: "follower-register-critical",
|
|
353
410
|
elapsedMs: Date.now() - registrationStartedAtMs,
|
|
@@ -355,16 +412,9 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
355
412
|
target: result.target,
|
|
356
413
|
reused: result.reused,
|
|
357
414
|
});
|
|
358
|
-
const connectedAnnouncement =
|
|
359
|
-
createTelegramBusInstanceLifecycleAnnouncement({
|
|
360
|
-
target: result.target,
|
|
361
|
-
threadName: result.record.threadName,
|
|
362
|
-
slot: result.record.slot,
|
|
363
|
-
state: "connected",
|
|
364
|
-
});
|
|
365
415
|
scheduleTelegramBusLeaderBackgroundTask(async () => {
|
|
366
416
|
const backgroundStartedAtMs = Date.now();
|
|
367
|
-
if (connectedAnnouncement) {
|
|
417
|
+
if (connectedAnnouncement && !connectedAnnouncementSent) {
|
|
368
418
|
try {
|
|
369
419
|
await deps.callApi("sendMessage", {
|
|
370
420
|
chat_id: connectedAnnouncement.target.chatId,
|
|
@@ -578,6 +628,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
578
628
|
method: string;
|
|
579
629
|
args: unknown[];
|
|
580
630
|
}) => boolean;
|
|
631
|
+
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
581
632
|
provisionFollowerTarget?: (
|
|
582
633
|
registration: TelegramBusInstanceRegistration,
|
|
583
634
|
) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
|
|
@@ -598,31 +649,22 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
598
649
|
>,
|
|
599
650
|
): Promise<TelegramBusEnvelope> => {
|
|
600
651
|
const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
ok: false,
|
|
606
|
-
message: "Unknown Telegram bus follower instance.",
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
if (!follower.busSocketPath) {
|
|
610
|
-
return {
|
|
611
|
-
kind: "bus.ack",
|
|
612
|
-
requestId: envelope.requestId,
|
|
613
|
-
ok: false,
|
|
614
|
-
message: "Telegram bus follower does not expose a receiver socket.",
|
|
615
|
-
};
|
|
616
|
-
}
|
|
617
|
-
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
652
|
+
const followerSocketPath =
|
|
653
|
+
follower?.busSocketPath ??
|
|
654
|
+
getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
|
|
655
|
+
if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
618
656
|
try {
|
|
619
657
|
const response = await sendTelegramBusLocalEnvelope({
|
|
620
|
-
socketPath:
|
|
658
|
+
socketPath: followerSocketPath,
|
|
621
659
|
envelope,
|
|
622
660
|
timeoutMs: deps.timeoutMs,
|
|
661
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
662
|
+
endpoint: followerSocketPath,
|
|
663
|
+
operation: "operation",
|
|
664
|
+
}),
|
|
623
665
|
});
|
|
624
666
|
if (response?.kind === "bus.ack" && response.ok) {
|
|
625
|
-
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
667
|
+
if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
626
668
|
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
627
669
|
}
|
|
628
670
|
const message =
|
|
@@ -714,6 +756,72 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
714
756
|
};
|
|
715
757
|
}
|
|
716
758
|
|
|
759
|
+
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
760
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
761
|
+
? (value as Record<string, unknown>)
|
|
762
|
+
: undefined;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function asInteger(value: unknown): number | undefined {
|
|
766
|
+
if (typeof value === "number" && Number.isInteger(value)) return value;
|
|
767
|
+
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
768
|
+
const parsed = Number(value);
|
|
769
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function getFollowerApiMethodAndBody(envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>): {
|
|
773
|
+
apiMethod: string;
|
|
774
|
+
body?: Record<string, unknown>;
|
|
775
|
+
} {
|
|
776
|
+
if (envelope.method === "call" || envelope.method === "callMultipart") {
|
|
777
|
+
return {
|
|
778
|
+
apiMethod:
|
|
779
|
+
typeof envelope.args[0] === "string" ? envelope.args[0] : "",
|
|
780
|
+
body: asRecord(envelope.args[1]),
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return { apiMethod: envelope.method, body: asRecord(envelope.args[0]) };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function getSentMessageIds(result: unknown): number[] {
|
|
787
|
+
const values = Array.isArray(result) ? result : [result];
|
|
788
|
+
return values
|
|
789
|
+
.map((value) => asInteger(asRecord(value)?.message_id))
|
|
790
|
+
.filter((messageId): messageId is number => messageId !== undefined);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function recordFollowerApiMessageOwnership(input: {
|
|
794
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>;
|
|
795
|
+
follower: TelegramBusFollowerView;
|
|
796
|
+
result: unknown;
|
|
797
|
+
record?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
798
|
+
}): void {
|
|
799
|
+
if (!input.record) return;
|
|
800
|
+
const { apiMethod, body } = getFollowerApiMethodAndBody(input.envelope);
|
|
801
|
+
if (
|
|
802
|
+
apiMethod !== "sendMessage" &&
|
|
803
|
+
apiMethod !== "sendRichMessage" &&
|
|
804
|
+
apiMethod !== "sendPhoto" &&
|
|
805
|
+
apiMethod !== "sendDocument" &&
|
|
806
|
+
apiMethod !== "sendVoice" &&
|
|
807
|
+
apiMethod !== "sendMediaGroup"
|
|
808
|
+
) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const chatId = asInteger(body?.chat_id) ?? input.follower.target?.chatId;
|
|
812
|
+
if (chatId === undefined) return;
|
|
813
|
+
const threadId = asInteger(body?.message_thread_id) ?? input.follower.target?.threadId;
|
|
814
|
+
const target = threadId !== undefined ? { chatId, threadId } : { chatId };
|
|
815
|
+
for (const messageId of getSentMessageIds(input.result)) {
|
|
816
|
+
input.record({
|
|
817
|
+
follower: input.follower,
|
|
818
|
+
chatId,
|
|
819
|
+
messageId,
|
|
820
|
+
target,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
717
825
|
async function handleFollowerApiCall(
|
|
718
826
|
envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>,
|
|
719
827
|
deps: {
|
|
@@ -725,6 +833,7 @@ async function handleFollowerApiCall(
|
|
|
725
833
|
method: string;
|
|
726
834
|
args: unknown[];
|
|
727
835
|
}) => boolean;
|
|
836
|
+
recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
|
|
728
837
|
},
|
|
729
838
|
): Promise<TelegramBusEnvelope> {
|
|
730
839
|
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
@@ -761,11 +870,18 @@ async function handleFollowerApiCall(
|
|
|
761
870
|
};
|
|
762
871
|
}
|
|
763
872
|
try {
|
|
873
|
+
const result = await deps.callApi(envelope.method, envelope.args);
|
|
874
|
+
recordFollowerApiMessageOwnership({
|
|
875
|
+
envelope,
|
|
876
|
+
follower,
|
|
877
|
+
result,
|
|
878
|
+
record: deps.recordFollowerMessageOwnership,
|
|
879
|
+
});
|
|
764
880
|
return {
|
|
765
881
|
kind: "bus.ack",
|
|
766
882
|
requestId: envelope.requestId,
|
|
767
883
|
ok: true,
|
|
768
|
-
result
|
|
884
|
+
result,
|
|
769
885
|
};
|
|
770
886
|
} catch (error) {
|
|
771
887
|
return {
|
|
@@ -842,7 +958,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
842
958
|
): TelegramBusLeaderRuntime<TContext> {
|
|
843
959
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
844
960
|
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
845
|
-
const followerStaleAfterMs = deps.followerStaleAfterMs ??
|
|
961
|
+
const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
|
|
846
962
|
let pruneInterval: ReturnType<typeof setInterval> | undefined;
|
|
847
963
|
const stopPruning = () => {
|
|
848
964
|
if (!pruneInterval) return;
|
|
@@ -878,12 +994,19 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
878
994
|
};
|
|
879
995
|
const localServer = createTelegramBusLocalServer({
|
|
880
996
|
socketPath: deps.socketPath,
|
|
997
|
+
recordTransportEvent(phase, details) {
|
|
998
|
+
deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
|
|
999
|
+
phase: `leader-${phase}`,
|
|
1000
|
+
...details,
|
|
1001
|
+
});
|
|
1002
|
+
},
|
|
881
1003
|
handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
|
|
882
1004
|
followerRegistry: deps.followerRegistry,
|
|
883
1005
|
authSecret: deps.authSecret,
|
|
884
1006
|
getNowMs,
|
|
885
1007
|
callApi: deps.callApi,
|
|
886
1008
|
authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
|
|
1009
|
+
recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
|
|
887
1010
|
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
888
1011
|
}),
|
|
889
1012
|
});
|
|
@@ -917,6 +1040,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
917
1040
|
.catch((error) =>
|
|
918
1041
|
deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }),
|
|
919
1042
|
);
|
|
1043
|
+
deps.followerRegistry.clear();
|
|
920
1044
|
}
|
|
921
1045
|
},
|
|
922
1046
|
};
|