{"version":3,"file":"typing-indicator.mjs","names":[],"sources":["../../../src/services/typing-indicator.ts"],"sourcesContent":["/**\n * Typing Indicator — sends Telegram \"typing...\" action for the full\n * duration the agent is thinking/processing.\n *\n * Telegram's `sendChatAction` typing indicator expires after ~5 seconds,\n * so we re-send it every 4.5s on a loop until explicitly stopped.\n *\n * Follows the same raw Bot API call pattern as telegram-draft-stream.ts.\n * No new dependencies — uses guardedFetch.\n *\n * Lifecycle:\n *   message_received → start(chatId)\n *   message_sending  → stop(chatId)\n *\n * Safety:\n *   - Max duration cap (5 minutes) prevents orphaned indicators\n *   - Errors are swallowed (typing indicator is non-critical UX)\n *   - Only activates for Telegram channel\n */\n\nimport { guardedFetch } from './endpoint-allowlist.js';\nimport { getCredentialVault } from './credential-vault.js';\n\n// ─── Constants ───────────────────────────────────────────────────────────\n\n/** Re-send typing action every 4.5s (Telegram expires at ~5s). */\nconst REPEAT_MS = 4_500;\n\n/** Hard cap: stop indicator after 5 minutes even if not explicitly stopped. */\nconst MAX_DURATION_MS = 5 * 60_000;\n\nconst API_BASE = 'https://api.telegram.org';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\ninterface ActiveIndicator {\n  timer: ReturnType<typeof setInterval>;\n  startedAt: number;\n  /** Safety timeout that force-stops after MAX_DURATION_MS. */\n  safetyTimeout: ReturnType<typeof setTimeout>;\n}\n\n// ─── Service ─────────────────────────────────────────────────────────────\n\nclass TypingIndicatorService {\n  private active = new Map<string, ActiveIndicator>();\n  private botToken: string | null = null;\n  private tokenResolved = false;\n\n  /** Resolve the bot token lazily (only when first needed). */\n  private getToken(): string | null {\n    if (this.tokenResolved) return this.botToken;\n    this.tokenResolved = true;\n\n    try {\n      const vault = getCredentialVault();\n      this.botToken = vault.getSecret('bot.telegram.botToken', 'typing-indicator') ?? null;\n    } catch {\n      this.botToken = process.env.TELEGRAM_BOT_TOKEN ?? null;\n    }\n    return this.botToken;\n  }\n\n  /**\n   * Start the typing indicator for a chat.\n   * Sends `sendChatAction` immediately, then repeats every 4.5s.\n   */\n  start(chatId: string): void {\n    const token = this.getToken();\n    if (!token) return; // No bot token — can't send typing\n\n    const key = String(chatId);\n    if (this.active.has(key)) return; // Already typing for this chat\n\n    // Fire immediately\n    this.sendTyping(token, key);\n\n    // Repeat on interval\n    const timer = setInterval(() => this.sendTyping(token, key), REPEAT_MS);\n    if (typeof timer.unref === 'function') timer.unref();\n\n    // Safety cap\n    const safetyTimeout = setTimeout(() => this.stop(key), MAX_DURATION_MS);\n    if (typeof safetyTimeout.unref === 'function') safetyTimeout.unref();\n\n    this.active.set(key, { timer, startedAt: Date.now(), safetyTimeout });\n  }\n\n  /** Stop the typing indicator for a chat. */\n  stop(chatId: string): void {\n    const key = String(chatId);\n    const entry = this.active.get(key);\n    if (!entry) return;\n\n    clearInterval(entry.timer);\n    clearTimeout(entry.safetyTimeout);\n    this.active.delete(key);\n  }\n\n  /** Stop all active indicators (for shutdown). */\n  stopAll(): void {\n    for (const [key] of this.active) {\n      this.stop(key);\n    }\n  }\n\n  /** Number of chats with active typing indicator (for testing). */\n  get size(): number {\n    return this.active.size;\n  }\n\n  /** Whether a chat has an active indicator (for testing). */\n  isActive(chatId: string): boolean {\n    return this.active.has(String(chatId));\n  }\n\n  /** Send a single sendChatAction typing call. Fire-and-forget. */\n  private sendTyping(token: string, chatId: string): void {\n    const url = `${API_BASE}/bot${token}/sendChatAction`;\n    guardedFetch(url, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ chat_id: chatId, action: 'typing' }),\n      signal: AbortSignal.timeout(5_000),\n    }).catch(() => {\n      // Non-critical — swallow errors silently.\n      // Common failures: bot token invalid, chat not found, rate limited.\n    });\n  }\n}\n\n// ─── Singleton ───────────────────────────────────────────────────────────\n\nlet instance: TypingIndicatorService | null = null;\n\nexport function getTypingIndicator(): TypingIndicatorService {\n  if (!instance) {\n    instance = new TypingIndicatorService();\n  }\n  return instance;\n}\n\nexport function resetTypingIndicator(): void {\n  instance?.stopAll();\n  instance = null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,YAAY;;AAGlB,MAAM,kBAAkB,IAAI;AAE5B,MAAM,WAAW;AAajB,IAAM,yBAAN,MAA6B;CAC3B,yBAAiB,IAAI,KAA8B;CACnD,WAAkC;CAClC,gBAAwB;;CAGxB,WAAkC;AAChC,MAAI,KAAK,cAAe,QAAO,KAAK;AACpC,OAAK,gBAAgB;AAErB,MAAI;AAEF,QAAK,WADS,oBAAoB,CACZ,UAAU,yBAAyB,mBAAmB,IAAI;UAC1E;AACN,QAAK,WAAW,QAAQ,IAAI,sBAAsB;;AAEpD,SAAO,KAAK;;;;;;CAOd,MAAM,QAAsB;EAC1B,MAAM,QAAQ,KAAK,UAAU;AAC7B,MAAI,CAAC,MAAO;EAEZ,MAAM,MAAM,OAAO,OAAO;AAC1B,MAAI,KAAK,OAAO,IAAI,IAAI,CAAE;AAG1B,OAAK,WAAW,OAAO,IAAI;EAG3B,MAAM,QAAQ,kBAAkB,KAAK,WAAW,OAAO,IAAI,EAAE,UAAU;AACvE,MAAI,OAAO,MAAM,UAAU,WAAY,OAAM,OAAO;EAGpD,MAAM,gBAAgB,iBAAiB,KAAK,KAAK,IAAI,EAAE,gBAAgB;AACvE,MAAI,OAAO,cAAc,UAAU,WAAY,eAAc,OAAO;AAEpE,OAAK,OAAO,IAAI,KAAK;GAAE;GAAO,WAAW,KAAK,KAAK;GAAE;GAAe,CAAC;;;CAIvE,KAAK,QAAsB;EACzB,MAAM,MAAM,OAAO,OAAO;EAC1B,MAAM,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClC,MAAI,CAAC,MAAO;AAEZ,gBAAc,MAAM,MAAM;AAC1B,eAAa,MAAM,cAAc;AACjC,OAAK,OAAO,OAAO,IAAI;;;CAIzB,UAAgB;AACd,OAAK,MAAM,CAAC,QAAQ,KAAK,OACvB,MAAK,KAAK,IAAI;;;CAKlB,IAAI,OAAe;AACjB,SAAO,KAAK,OAAO;;;CAIrB,SAAS,QAAyB;AAChC,SAAO,KAAK,OAAO,IAAI,OAAO,OAAO,CAAC;;;CAIxC,WAAmB,OAAe,QAAsB;AAEtD,eADY,GAAG,SAAS,MAAM,MAAM,kBAClB;GAChB,QAAQ;GACR,SAAS,EAAE,gBAAgB,oBAAoB;GAC/C,MAAM,KAAK,UAAU;IAAE,SAAS;IAAQ,QAAQ;IAAU,CAAC;GAC3D,QAAQ,YAAY,QAAQ,IAAM;GACnC,CAAC,CAAC,YAAY,GAGb;;;AAMN,IAAI,WAA0C;AAE9C,SAAgB,qBAA6C;AAC3D,KAAI,CAAC,SACH,YAAW,IAAI,wBAAwB;AAEzC,QAAO;;AAGT,SAAgB,uBAA6B;AAC3C,WAAU,SAAS;AACnB,YAAW"}