fix: harden typing lifecycle and cross-channel suppression

This commit is contained in:
Peter Steinberger
2026-02-26 17:01:03 +01:00
parent 4894d907fa
commit 37a138c554
14 changed files with 359 additions and 85 deletions

View File

@@ -13,6 +13,8 @@ export type CreateTypingCallbacksParams = {
onStartError: (err: unknown) => void;
onStopError?: (err: unknown) => void;
keepaliveIntervalMs?: number;
/** Stop keepalive after this many consecutive start() failures. Default: 2 */
maxConsecutiveFailures?: number;
/** Maximum duration for typing indicator before auto-cleanup (safety TTL). Default: 60s */
maxDurationMs?: number;
};
@@ -20,19 +22,31 @@ export type CreateTypingCallbacksParams = {
export function createTypingCallbacks(params: CreateTypingCallbacksParams): TypingCallbacks {
const stop = params.stop;
const keepaliveIntervalMs = params.keepaliveIntervalMs ?? 3_000;
const maxConsecutiveFailures = Math.max(1, params.maxConsecutiveFailures ?? 2);
const maxDurationMs = params.maxDurationMs ?? 60_000; // Default 60s TTL
let stopSent = false;
let closed = false;
let consecutiveFailures = 0;
let breakerTripped = false;
let ttlTimer: ReturnType<typeof setTimeout> | undefined;
const fireStart = async () => {
const fireStart = async (): Promise<void> => {
if (closed) {
return;
}
if (breakerTripped) {
return;
}
try {
await params.start();
consecutiveFailures = 0;
} catch (err) {
consecutiveFailures += 1;
params.onStartError(err);
if (consecutiveFailures >= maxConsecutiveFailures) {
breakerTripped = true;
keepaliveLoop.stop();
}
}
};
@@ -67,9 +81,14 @@ export function createTypingCallbacks(params: CreateTypingCallbacksParams): Typi
return;
}
stopSent = false;
breakerTripped = false;
consecutiveFailures = 0;
keepaliveLoop.stop();
clearTtlTimer();
await fireStart();
if (breakerTripped) {
return;
}
keepaliveLoop.start();
startTtlTimer(); // Start TTL safety timer
};