@xbibzlibrary/telebibz 0.4.3 → 0.4.5

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.id.md +20 -3
  3. package/README.md +20 -3
  4. package/README.zh-CN.md +20 -3
  5. package/dist/src/api/client.d.ts +28 -0
  6. package/dist/src/api/client.d.ts.map +1 -1
  7. package/dist/src/api/client.js +28 -0
  8. package/dist/src/api/client.js.map +1 -1
  9. package/dist/src/api/transport.d.ts +14 -0
  10. package/dist/src/api/transport.d.ts.map +1 -1
  11. package/dist/src/api/transport.js +67 -2
  12. package/dist/src/api/transport.js.map +1 -1
  13. package/dist/src/api/types.d.ts +25 -0
  14. package/dist/src/api/types.d.ts.map +1 -1
  15. package/dist/src/context/context.d.ts +13 -3
  16. package/dist/src/context/context.d.ts.map +1 -1
  17. package/dist/src/context/context.js +15 -0
  18. package/dist/src/context/context.js.map +1 -1
  19. package/dist/src/core/bot.d.ts +12 -0
  20. package/dist/src/core/bot.d.ts.map +1 -1
  21. package/dist/src/core/bot.js +16 -0
  22. package/dist/src/core/bot.js.map +1 -1
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +1 -0
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/testing.d.ts +6 -0
  28. package/dist/src/testing.d.ts.map +1 -1
  29. package/dist/src/testing.js +6 -0
  30. package/dist/src/testing.js.map +1 -1
  31. package/dist/src/utils/files.d.ts +45 -0
  32. package/dist/src/utils/files.d.ts.map +1 -0
  33. package/dist/src/utils/files.js +53 -0
  34. package/dist/src/utils/files.js.map +1 -0
  35. package/dist-cjs/src/api/client.js +28 -0
  36. package/dist-cjs/src/api/transport.js +67 -2
  37. package/dist-cjs/src/context/context.js +15 -0
  38. package/dist-cjs/src/core/bot.js +16 -0
  39. package/dist-cjs/src/index.js +1 -0
  40. package/dist-cjs/src/testing.js +6 -0
  41. package/dist-cjs/src/utils/files.js +58 -0
  42. package/docs/API.id.md +70 -0
  43. package/docs/API.md +70 -0
  44. package/docs/API.zh-CN.md +70 -0
  45. package/docs/COOKBOOK.id.md +321 -0
  46. package/docs/COOKBOOK.md +321 -0
  47. package/docs/COOKBOOK.zh-CN.md +321 -0
  48. package/docs/ERRORS.id.md +194 -0
  49. package/docs/ERRORS.md +194 -0
  50. package/docs/ERRORS.zh-CN.md +194 -0
  51. package/docs/FILES.id.md +243 -0
  52. package/docs/FILES.md +243 -0
  53. package/docs/FILES.zh-CN.md +243 -0
  54. package/docs/GETTING_STARTED.id.md +6 -2
  55. package/docs/GETTING_STARTED.md +6 -2
  56. package/docs/GETTING_STARTED.zh-CN.md +6 -2
  57. package/docs/MIGRATION_TELEGRAF.id.md +147 -0
  58. package/docs/MIGRATION_TELEGRAF.md +154 -0
  59. package/docs/MIGRATION_TELEGRAF.zh-CN.md +147 -0
  60. package/docs/README.md +38 -19
  61. package/docs/STORAGE.id.md +105 -0
  62. package/docs/STORAGE.md +105 -0
  63. package/docs/STORAGE.zh-CN.md +105 -0
  64. package/docs/TESTING.id.md +203 -0
  65. package/docs/TESTING.md +203 -0
  66. package/docs/TESTING.zh-CN.md +203 -0
  67. package/docs/WEBHOOK.id.md +212 -0
  68. package/docs/WEBHOOK.md +215 -0
  69. package/docs/WEBHOOK.zh-CN.md +212 -0
  70. package/examples/files.ts +35 -0
  71. package/package.json +1 -1
@@ -0,0 +1,321 @@
1
+ # 生产实战手册(简体中文)
2
+
3
+ 真实 bot 所需功能的完整、已验证配方:按用户限流、鉴权中间件、广播、定时任务、后台队列、分页菜单、表单、缓存、Mini App 校验与支付。每个配方自成一体 —— 复制进你的 bot 后改名即可。
4
+
5
+ ## 目录
6
+
7
+ 1. [按用户限流](#1-按用户限流)
8
+ 2. [鉴权中间件(白名单 / 仅管理员)](#2-鉴权中间件白名单--仅管理员)
9
+ 3. [向数千用户广播](#3-向数千用户广播)
10
+ 4. [定时消息(间隔、单次、cron)](#4-定时消息间隔单次cron)
11
+ 5. [带重试的后台任务](#5-带重试的后台任务)
12
+ 6. [分页菜单](#6-分页菜单)
13
+ 7. [权限感知菜单](#7-权限感知菜单)
14
+ 8. [带校验的多步表单](#8-带校验的多步表单)
15
+ 9. [编辑消息与内联键盘](#9-编辑消息与内联键盘)
16
+ 10. [缓存昂贵结果](#10-缓存昂贵结果)
17
+ 11. [Mini App initData 校验](#11-mini-app-initdata-校验)
18
+ 12. [Telegram Stars / 账单支付](#12-telegram-stars--账单支付)
19
+ 13. [结构化日志与指标钩子](#13-结构化日志与指标钩子)
20
+
21
+ ## 1. 按用户限流
22
+
23
+ `TokenBucketLimiter` 为每个键维护独立桶 —— 以用户或 chat 为键:
24
+
25
+ ```ts
26
+ import { Bot, TokenBucketLimiter } from "@xbibzlibrary/telebibz";
27
+
28
+ const limiter = new TokenBucketLimiter(5, 0.5); // 突发 5 个,每秒回填 0.5 个(持续速率 = 每 2 秒 1 条)
29
+
30
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
31
+ bot.use(async (ctx, next) => {
32
+ const key = `user:${ctx.from?.id ?? "anon"}`;
33
+ const result = limiter.consume(key);
34
+ if (!result.allowed) {
35
+ const seconds = Math.ceil((result.retryAfterMs ?? 1000) / 1000);
36
+ await ctx.reply(`⏳ 请求太频繁,请在 ${seconds} 秒后重试。`);
37
+ return; // 不调用 next():该更新被丢弃
38
+ }
39
+ await next();
40
+ });
41
+ ```
42
+
43
+ `consume(key, cost)` 支持加权动作(如上传花 5、文本花 1)。`limiter.clear(key?)` 重置状态。可与传输层 flood gate 组合 —— 限流器约束*你的用户*;flood gate 服从 *Telegram*。
44
+
45
+ ## 2. 鉴权中间件(白名单 / 仅管理员)
46
+
47
+ ```ts
48
+ const ADMINS = new Set([Number(process.env.ADMIN_ID)]);
49
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
50
+
51
+ bot.use(async (ctx, next) => {
52
+ if (ADMINS.has(ctx.from?.id ?? 0)) return await next(); // 管理员:全放行
53
+ if (ctx.chat?.type === "private") return await next(); // 私聊:放行
54
+ return undefined; // 群组:静默丢弃
55
+ });
56
+
57
+ bot.command("stats", async (ctx) => { // 仅管理员路由
58
+ if (!ADMINS.has(ctx.from?.id ?? 0)) return;
59
+ await ctx.reply("机密统计");
60
+ });
61
+ ```
62
+
63
+ ## 3. 向数千用户广播
64
+
65
+ ```ts
66
+ const report = await bot.broadcast(
67
+ subscriberIds,
68
+ (chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "📰 第 42 期通讯" }),
69
+ {
70
+ concurrency: 64, // 下游需要时才设上限(默认:完全并行)
71
+ onProgress: (p) => console.log(`${p.delivered}/${p.total}`),
72
+ },
73
+ );
74
+
75
+ console.log(`已送达 ${report.delivered}/${report.total},耗时 ${report.durationMs}ms`);
76
+ for (const failure of report.failures) {
77
+ console.error(`chat ${failure.chatId}: ${failure.error}`);
78
+ }
79
+ ```
80
+
81
+ 每个 chat 都会尝试;429 精确按 Telegram 指示的 `retry_after` 重试。失败从不中断整体 —— 全部落入报告。
82
+
83
+ ## 4. 定时消息(间隔、单次、cron)
84
+
85
+ ```ts
86
+ import { Scheduler, parseCronExpression, nextCronOccurrence } from "@xbibzlibrary/telebibz";
87
+
88
+ const scheduler = new Scheduler({ onError: (error, id) => console.error(`任务 ${id} 失败`, error) });
89
+
90
+ // 每 6 小时
91
+ scheduler.every("digest", 6 * 60 * 60 * 1000, async () => {
92
+ await bot.api.methods.sendMessage({ chat_id: ADMIN_CHAT, text: "定时摘要" });
93
+ });
94
+
95
+ // 一次性,5 分钟后(提醒模式)
96
+ scheduler.after("remind-42", 5 * 60 * 1000, async () => {
97
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "⏰ 提醒!" });
98
+ });
99
+
100
+ // cron:工作日 09:00(五字段表达式)
101
+ scheduler.cron("morning", "0 9 * * 1-5", async () => {
102
+ await bot.api.methods.sendMessage({ chat_id: 42, text: "早上好!" });
103
+ });
104
+
105
+ scheduler.cancel("digest"); // 停止单个任务
106
+ scheduler.clear(); // 停止全部
107
+ ```
108
+
109
+ 用于测试与预览的纯函数助手 —— 不涉及定时器:
110
+
111
+ ```ts
112
+ parseCronExpression("*/15 * * * *"); // 已校验的字段
113
+ nextCronOccurrence("0 9 * * 1", new Date()); // 下次运行时间(Date)
114
+ ```
115
+
116
+ ## 5. 带重试的后台任务
117
+
118
+ ```ts
119
+ import { TaskQueue } from "@xbibzlibrary/telebibz";
120
+
121
+ const queue = new TaskQueue(
122
+ async (job) => {
123
+ await fetch(`https://api.example.com/process`, { method: "POST", body: JSON.stringify(job.data) });
124
+ },
125
+ { concurrency: 8, retries: 3, backoffMs: 500, maxBackoffMs: 30_000, onError: (error, job) => log.error("任务失败", { job: job.id, error }) },
126
+ );
127
+
128
+ bot.command("process", async (ctx) => {
129
+ const job = queue.add({ url: ctx.message?.text?.split(" ")[1] }, { priority: 10 }); // 高优先级先执行
130
+ await ctx.reply(`已入队任务 ${job.id}`);
131
+ });
132
+
133
+ bot.command("cancel", async (ctx) => {
134
+ const id = ctx.message?.text?.split(" ")[1];
135
+ if (id && queue.cancel(id)) await ctx.reply("已取消");
136
+ });
137
+ ```
138
+
139
+ ## 6. 分页菜单
140
+
141
+ `MenuController` 一次渲染一页并路由翻页回调:
142
+
143
+ ```ts
144
+ import { Bot, MenuController, InlineKeyboard } from "@xbibzlibrary/telebibz";
145
+
146
+ const products = Array.from({ length: 57 }, (_v, i) => ({ id: i + 1, name: `商品 ${i + 1}` }));
147
+
148
+ const menu = new MenuController({
149
+ id: "products",
150
+ items: () => products, // 或 async () => await db.products()
151
+ pageSize: 10,
152
+ label: (item) => item.name,
153
+ callback: async (item) => { /* 用户选中了一件商品 */ },
154
+ labels: { previous: "◀", next: "▶" },
155
+ });
156
+
157
+ bot.callback("products:*", async (ctx) => {
158
+ // 传入完整的回调数据 —— 控制器需要它自己的 "products:" 前缀。
159
+ const result = await menu.handle(ctx.callbackQuery?.data ?? "");
160
+ if (result === undefined) return void (await ctx.answerCallbackQuery());
161
+ if (result.type === "noop") return void (await ctx.answerCallbackQuery());
162
+ if (result.type === "page") {
163
+ await ctx.reply(`第 ${result.page.page + 1}/${result.page.pageCount} 页`, { reply_markup: result.keyboard });
164
+ } else {
165
+ await ctx.answerCallbackQuery(`已选:${result.item.name}`);
166
+ }
167
+ });
168
+ bot.command("shop", async (ctx) => {
169
+ const result = await menu.handle("products:page:0"); // "<id>:page:<n>"
170
+ if (result?.type === "page") await ctx.reply("商品:", { reply_markup: result.keyboard });
171
+ });
172
+ ```
173
+
174
+ ## 7. 权限感知菜单
175
+
176
+ ```ts
177
+ import { Menu } from "@xbibzlibrary/telebibz";
178
+
179
+ const menu = new Menu("main")
180
+ .breadcrumb("主页")
181
+ .item({ id: "profile", label: "👤 个人资料", callbackData: "open:profile" })
182
+ .item({ id: "stats", label: "📊 统计", permission: (context) => context.permissions?.includes("admin") ?? false })
183
+ .item({ id: "help", label: "❓ 帮助", url: "https://example.com/help" });
184
+
185
+ // build() 是异步的:它为给定 context 计算可见性/权限。
186
+ const keyboard = await menu.build({ permissions: ["admin"] }, { columns: 1, includeBreadcrumbs: true });
187
+ await ctx.reply("主菜单:", { reply_markup: keyboard.build() });
188
+ ```
189
+
190
+ `visible` 彻底隐藏条目;`permission` 接收你的 `MenuContext`(`{ userId, permissions }`)。
191
+
192
+ ## 8. 带校验的多步表单
193
+
194
+ ```ts
195
+ import { Bot, Form, validators } from "@xbibzlibrary/telebibz";
196
+
197
+ const registration = new Form({
198
+ name: { parse: validators.string, required: true },
199
+ age: { parse: validators.integer, validate: (age) => (age >= 13 ? undefined : "需满 13 岁") },
200
+ email: { parse: validators.email },
201
+ });
202
+ ```
203
+
204
+ 通过 `Wizard` 步骤或 `ConversationManager` 接线 —— 每条消息填一个字段;`validators` 覆盖 `string`、`number`、`integer`、`email`、`url`,自定义检查返回错误消息。
205
+
206
+ ## 9. 编辑消息与内联键盘
207
+
208
+ ```ts
209
+ import { InlineKeyboard } from "@xbibzlibrary/telebibz";
210
+
211
+ bot.action("vote:up", async (ctx) => {
212
+ votes += 1;
213
+ const keyboard = new InlineKeyboard()
214
+ .text(`👍 ${votes}`, "vote:up")
215
+ .text("👎 0", "vote:down")
216
+ .build();
217
+ // 原地替换按钮所在消息的键盘
218
+ await ctx.api.methods.editMessageReplyMarkup({
219
+ chat_id: ctx.chat!.id,
220
+ message_id: ctx.callbackQuery!.message!.message_id,
221
+ reply_markup: keyboard,
222
+ });
223
+ await ctx.answerCallbackQuery(); // 停止转圈
224
+ });
225
+ ```
226
+
227
+ `ctx.edit(text, extra)` 改写当前消息的文本(键盘通过 `extra` 中的 `reply_markup` 一并替换);`editMessageLiveLocation`、`stopPoll` 及完整方法面都在 `ctx.api.methods` 上。按钮数据上限 **64 字节** —— builder 在构造时校验,而不是运行时崩溃。
228
+
229
+ ## 10. 缓存昂贵结果
230
+
231
+ ```ts
232
+ import { MemoryCache } from "@xbibzlibrary/telebibz";
233
+
234
+ const weather = new MemoryCache<string>("weather"); // 命名空间;TTL 按每次写入设置
235
+ bot.command("weather", async (ctx) => {
236
+ const city = ctx.message?.text?.split(" ")[1] ?? "北京";
237
+ let text = await weather.get(city);
238
+ if (text === undefined) {
239
+ text = await fetchWeather(city);
240
+ await weather.set(city, text, 5 * 60 * 1000); // 缓存 5 分钟
241
+ }
242
+ await ctx.reply(text);
243
+ });
244
+ ```
245
+
246
+ ## 11. Mini App initData 校验
247
+
248
+ ```ts
249
+ import { validateWebAppInitData } from "@xbibzlibrary/telebibz";
250
+
251
+ bot.command("app", async (ctx) => {
252
+ await ctx.reply("打开应用:", {
253
+ reply_markup: new InlineKeyboard().webApp("🚀 打开", "https://app.example.com").build(),
254
+ });
255
+ });
256
+
257
+ // 在你应用的后端端点里 —— 校验 Mini App 发来的数据:
258
+ app.post("/api/data", express.json(), (req, res) => {
259
+ try {
260
+ const initData = validateWebAppInitData(req.body.initData, process.env.TELEGRAM_BOT_TOKEN!, 3600);
261
+ res.json({ user: initData.user, ok: true }); // 签名 + 新鲜度已验证
262
+ } catch {
263
+ res.status(401).json({ ok: false });
264
+ }
265
+ });
266
+ ```
267
+
268
+ `validateWebAppInitData` 检查 HMAC 签名与 `auth_date` 新鲜度窗口(默认 24 小时;此处 1 小时)。
269
+
270
+ ## 12. Telegram Stars / 账单支付
271
+
272
+ ```ts
273
+ import { Bot, PaymentsClient, InlineKeyboard } from "@xbibzlibrary/telebibz";
274
+
275
+ const bot = new Bot(process.env.TELEGRAM_BOT_TOKEN!);
276
+ const payments = new PaymentsClient(bot.api);
277
+
278
+ // 可在任何地方使用的链接(简介、网站、聊天)
279
+ const link = await payments.createInvoiceLink({
280
+ title: "Premium",
281
+ description: "30 天会员",
282
+ payload: "premium-30d",
283
+ currency: "XTR",
284
+ prices: [{ label: "Premium", amount: 100 }],
285
+ });
286
+ await ctx.reply(`在此支付:${link}`);
287
+
288
+ // 聊天内账单 + 预检 + 支付成功
289
+ bot.on("pre_checkout_query", async (ctx) => {
290
+ const query = ctx.update.pre_checkout_query;
291
+ if (!query) return;
292
+ await ctx.api.methods.answerPreCheckoutQuery({ pre_checkout_query_id: query.id, ok: true });
293
+ });
294
+ bot.on("message:successful_payment", async (ctx) => {
295
+ await ctx.reply("✅ 已收到付款,谢谢!");
296
+ });
297
+
298
+ // Stars 流水与退款
299
+ const history = await payments.getStarTransactions({ limit: 50 });
300
+ await payments.refundStarPayment({ user_id: userId, telegram_payment_charge_id: "charge-id" });
301
+ ```
302
+
303
+ ## 13. 结构化日志与指标钩子
304
+
305
+ ```ts
306
+ const bot = new Bot({
307
+ token: process.env.TELEGRAM_BOT_TOKEN!,
308
+ logger: { level: "info", format: "json" }, // 供采集系统消费的机器可读日志行
309
+ });
310
+
311
+ bot.events.on("api:response", ({ method, durationMs }) => {
312
+ if (durationMs > 3_000) console.warn(JSON.stringify({ event: "slow_api", method, durationMs }));
313
+ });
314
+ bot.events.on("update:error", ({ error }) => {
315
+ console.error(JSON.stringify({ event: "handler_error", error: String(error) }));
316
+ });
317
+ ```
318
+
319
+ 敏感值(token、手机号)自动脱敏;确有需要时用 `includeUpdateContent: true` 开启消息文本记录。
320
+
321
+ English: [COOKBOOK.md](COOKBOOK.md) · Bahasa Indonesia: [COOKBOOK.id.md](COOKBOOK.id.md)
@@ -0,0 +1,194 @@
1
+ # Panduan penanganan error (Bahasa Indonesia)
2
+
3
+ Semua jalur error di telebibz, artinya, dan cara menanganinya — dari error API Telegram sampai kegagalan handler, timeout, dan shutdown.
4
+
5
+ ## Daftar isi
6
+
7
+ 1. [Taksonomi error](#1-taksonomi-error)
8
+ 2. [Anatomi TelegramError](#2-anatomi-telegramerror)
9
+ 3. [Rate limit 429 dan flood gate](#3-rate-limit-429-dan-flood-gate)
10
+ 4. [Error handler dan `bot.catch()`](#4-error-handler-dan-botcatch)
11
+ 5. [handlerTimeout dan `UpdateTimeoutError`](#5-handlertimeout-dan-updatetimeouterror)
12
+ 6. [Observasi error berbasis event](#6-observasi-error-berbasis-event)
13
+ 7. [Retry transport dan error jaringan](#7-retry-transport-dan-error-jaringan)
14
+ 8. [Resep penanganan error](#8-resep-penanganan-error)
15
+
16
+ ## 1. Taksonomi error
17
+
18
+ Setiap kegagalan API Telegram adalah `TelegramError` dengan `kind`:
19
+
20
+ | `kind` | Subclass | Arti | Pemicu umum |
21
+ |---|---|---|---|
22
+ | `rate-limit` | `TelegramRateLimitError` | Telegram menjawab 429 | Kirim terlalu cepat; `retryAfter` terisi |
23
+ | `authentication` | `TelegramAuthError` | Token tidak valid/dicabut (401) | Token salah, bot di-revoke, logout |
24
+ | `validation` | `TelegramValidationError` | Parameter request buruk (400) | `file_id` tidak dikenal, payload rusak |
25
+ | `network` | `TelegramNetworkError` | Kegagalan level transport | DNS, socket, respons non-JSON, unduhan gagal |
26
+ | `server` | — | Error server Telegram (5xx) | Transien; di-retry otomatis |
27
+ | `retryable` | — | Error Telegram lain yang bisa di-retry | Varian flood-wait |
28
+ | `unknown` | — | Lainnya | — |
29
+
30
+ Cek `kind` saat reaksinya perlu berbeda:
31
+
32
+ ```ts
33
+ import { TelegramError, TelegramRateLimitError } from "@xbibzlibrary/telebibz";
34
+
35
+ try {
36
+ await ctx.reply("halo");
37
+ } catch (error) {
38
+ if (error instanceof TelegramError) {
39
+ switch (error.kind) {
40
+ case "rate-limit": console.log(`pelankan ${error.retryAfter}s`); break;
41
+ case "authentication": console.error("masalah token — berhenti"); break;
42
+ case "validation": console.warn(error.message); break;
43
+ default: console.error(error.message);
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ `instanceof TelegramRateLimitError` juga bisa dipakai bila hanya peduli 429.
50
+
51
+ ## 2. Anatomi TelegramError
52
+
53
+ ```ts
54
+ try {
55
+ await bot.api.methods.getChatMember({ chat_id: -100123, user_id: 42 });
56
+ } catch (error) {
57
+ if (error instanceof TelegramError) {
58
+ error.kind; // salah satu dari tujuh kind di atas
59
+ error.errorCode; // error_code Telegram (400, 401, 429, …) bila ada
60
+ error.method; // "getChatMember" — panggilan yang gagal
61
+ error.payload; // payload yang dikirim
62
+ error.retryAfter; // detik, hanya untuk 429 (dari parameters.retry_after)
63
+ error.message; // deskripsi dari Telegram
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## 3. Rate limit 429 dan flood gate
69
+
70
+ Biasanya Anda tidak pernah melihat 429, karena transport sudah menanganinya:
71
+
72
+ 1. Telegram menjawab 429 dengan `parameters.retry_after`.
73
+ 2. **Flood gate** menjeda permintaan keluar *baru* selama jendela itu — melindungi seluruh trafik, bukan hanya request yang ditolak.
74
+ 3. Request yang gagal di-retry otomatis, lalu kegagalan (bila bertahan) muncul sebagai `TelegramRateLimitError`.
75
+
76
+ Flood gate adalah satu-satunya jeda yang pernah diperkenalkan library — tidak pernah ada cooldown proaktif. Atur atau matikan per transport:
77
+
78
+ ```ts
79
+ const bot = new Bot({
80
+ token: process.env.TELEGRAM_BOT_TOKEN!,
81
+ transportOptions: {
82
+ floodGate: false, // tangani 429 sepenuhnya sendiri
83
+ retries: 3, // jumlah retry untuk 429/5xx/error jaringan
84
+ backoffMs: 250, // basis backoff eksponensial
85
+ maxBackoffMs: 8_000,
86
+ timeoutMs: 30_000, // timeout per request
87
+ },
88
+ });
89
+ ```
90
+
91
+ Bila gate dimatikan, tangkap `TelegramRateLimitError` dan patuhi `retryAfter` — kata Telegram final:
92
+
93
+ ```ts
94
+ catch (error) {
95
+ if (error instanceof TelegramRateLimitError) {
96
+ await sleep((error.retryAfter ?? 1) * 1000);
97
+ return retry();
98
+ }
99
+ throw error;
100
+ }
101
+ ```
102
+
103
+ ## 4. Error handler dan `bot.catch()`
104
+
105
+ Tanpa error boundary, handler yang melempar akan menolak `handleUpdate()` (dan webhook menjawab 500). Dengan `bot.catch()`, kegagalan diarahkan ke satu tempat:
106
+
107
+ ```ts
108
+ bot.catch(async (error, ctx) => {
109
+ console.error("handler gagal:", error);
110
+
111
+ if (error instanceof TelegramError && error.kind === "authentication") {
112
+ process.exitCode = 1; // tak tertolak — biarkan supervisor me-restart
113
+ await bot.stop();
114
+ return;
115
+ }
116
+
117
+ await ctx.reply("❌ Terjadi kesalahan. Coba lagi."); // ctx = context update yang gagal
118
+ });
119
+ ```
120
+
121
+ Perilaku kunci:
122
+ - Hanya update yang gagal yang terdampak — chat lain tetap diproses konkuren.
123
+ - Urutan per chat tetap terjaga: update berikutnya dari chat yang sama tetap menunggu yang ini.
124
+ - Kegagalan `broadcast()` terkumpul di report alih-alih melempar.
125
+
126
+ ## 5. handlerTimeout dan `UpdateTimeoutError`
127
+
128
+ `handlerTimeout` (default **90 000 ms**, mengikuti Telegraf) melindungi pipeline dari handler yang menggantung:
129
+
130
+ ```ts
131
+ const bot = new Bot({ token, handlerTimeout: 30_000 }); // 0 atau Infinity menonaktifkan
132
+ ```
133
+
134
+ - Promise `handleUpdate()` untuk update yang menggantung melempar `UpdateTimeoutError` dan mengalir lewat `update:error` → `bot:error` → `bot.catch()`.
135
+ - **Handler tetap berjalan di belakang** — session dan conversation tetap menyelesaikan penulisannya; timeout hanya melepaskan pipeline.
136
+ - Urutan per chat tidak terpengaruh.
137
+
138
+ ## 6. Observasi error berbasis event
139
+
140
+ Untuk metrik/logging yang independen dari boundary, dengarkan event bus:
141
+
142
+ ```ts
143
+ bot.events.on("update:error", ({ update, error }) => {
144
+ metrics.increment("handler_errors", { updateId: (update as { update_id?: number }).update_id });
145
+ });
146
+ bot.events.on("bot:error", ({ error }) => log.error("bot error", { error }));
147
+ bot.events.on("api:response", ({ method, durationMs }) => {
148
+ if (durationMs > 3_000) log.warn("panggilan api lambat", { method, durationMs });
149
+ });
150
+ ```
151
+
152
+ ## 7. Retry transport dan error jaringan
153
+
154
+ `FetchTransport` me-retry otomatis saat: 429 (dengan `retry_after` milik Telegram), 5xx, error jaringan, dan respons non-JSON — dengan backoff eksponensial dan jitter. Setelah `retries` percobaan, error terakhir muncul sebagai `TelegramNetworkError` (lengkap `status` dan cause terpotong). Error autentikasi (401) dan validasi (400) **tidak pernah** di-retry — retry tidak akan memperbaikinya.
155
+
156
+ ## 8. Resep penanganan error
157
+
158
+ **Retry dengan backoff di sekitar satu panggilan** (di luar retry transport):
159
+
160
+ ```ts
161
+ async function withRetry<T>(run: () => Promise<T>, attempts = 3): Promise<T> {
162
+ for (let i = 0; i < attempts; i++) {
163
+ try { return await run(); }
164
+ catch (error) {
165
+ if (error instanceof TelegramError && ["authentication", "validation"].includes(error.kind)) throw error;
166
+ if (i === attempts - 1) throw error;
167
+ await new Promise((r) => setTimeout(r, 500 * 2 ** i));
168
+ }
169
+ }
170
+ throw new Error("unreachable");
171
+ }
172
+ ```
173
+
174
+ **Fail cepat saat token buruk** (polling dengan token tidak valid):
175
+
176
+ ```ts
177
+ try {
178
+ await bot.start();
179
+ } catch (error) {
180
+ if (error instanceof TelegramError && error.kind === "authentication") {
181
+ console.error("TELEGRAM_BOT_TOKEN tidak valid atau dicabut");
182
+ }
183
+ throw error;
184
+ }
185
+ ```
186
+
187
+ **Shutdown anggun** — `stop()` lebih dulu meng-drain handler yang berjalan:
188
+
189
+ ```ts
190
+ process.on("SIGINT", () => { void bot.stop().then(() => process.exit(0)); });
191
+ process.on("SIGTERM", () => { void bot.stop().then(() => process.exit(0)); });
192
+ ```
193
+
194
+ English: [ERRORS.md](ERRORS.md) · 简体中文: [ERRORS.zh-CN.md](ERRORS.zh-CN.md)