@xbibzlibrary/telebibz 0.1.19 → 0.3.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/CHANGELOG.md +42 -8
- package/CONTRIBUTING.md +2 -2
- package/README.id.md +44 -5
- package/README.md +45 -6
- package/README.zh-CN.md +44 -5
- package/RELEASE_AUTOMATION.md +17 -5
- package/bin/telebibz.mjs +1 -1
- package/dist/src/api/client.d.ts +3 -1
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +3 -1
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/api/transport.d.ts +18 -1
- package/dist/src/api/transport.d.ts.map +1 -1
- package/dist/src/api/transport.js +26 -2
- package/dist/src/api/transport.js.map +1 -1
- package/dist/src/branding/terminal.d.ts +62 -0
- package/dist/src/branding/terminal.d.ts.map +1 -1
- package/dist/src/branding/terminal.js +258 -0
- package/dist/src/branding/terminal.js.map +1 -1
- package/dist/src/broadcast/broadcast.d.ts +50 -0
- package/dist/src/broadcast/broadcast.d.ts.map +1 -0
- package/dist/src/broadcast/broadcast.js +56 -0
- package/dist/src/broadcast/broadcast.js.map +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +7 -3
- package/dist/src/cli.js.map +1 -1
- package/dist/src/context/context.d.ts +24 -1
- package/dist/src/context/context.d.ts.map +1 -1
- package/dist/src/context/context.js +102 -8
- package/dist/src/context/context.js.map +1 -1
- package/dist/src/core/bot.d.ts +61 -2
- package/dist/src/core/bot.d.ts.map +1 -1
- package/dist/src/core/bot.js +180 -31
- package/dist/src/core/bot.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/keyboard/index.d.ts +12 -0
- package/dist/src/keyboard/index.d.ts.map +1 -1
- package/dist/src/keyboard/index.js +13 -1
- package/dist/src/keyboard/index.js.map +1 -1
- package/dist/src/observability/logger.d.ts +28 -1
- package/dist/src/observability/logger.d.ts.map +1 -1
- package/dist/src/observability/logger.js +110 -0
- package/dist/src/observability/logger.js.map +1 -1
- package/dist/src/plugins/plugin.d.ts +2 -0
- package/dist/src/plugins/plugin.d.ts.map +1 -1
- package/dist/src/plugins/plugin.js +11 -4
- package/dist/src/plugins/plugin.js.map +1 -1
- package/dist/src/router/router.d.ts +19 -0
- package/dist/src/router/router.d.ts.map +1 -1
- package/dist/src/router/router.js +125 -23
- package/dist/src/router/router.js.map +1 -1
- package/dist/src/state/forms.d.ts +0 -1
- package/dist/src/state/forms.d.ts.map +1 -1
- package/dist/src/state/forms.js +27 -24
- package/dist/src/state/forms.js.map +1 -1
- package/dist/src/storage/storage.d.ts +10 -0
- package/dist/src/storage/storage.d.ts.map +1 -1
- package/dist/src/storage/storage.js +20 -2
- package/dist/src/storage/storage.js.map +1 -1
- package/dist/src/utils/concurrency.d.ts +25 -0
- package/dist/src/utils/concurrency.d.ts.map +1 -0
- package/dist/src/utils/concurrency.js +52 -0
- package/dist/src/utils/concurrency.js.map +1 -0
- package/dist/src/utils/text.d.ts +22 -0
- package/dist/src/utils/text.d.ts.map +1 -1
- package/dist/src/utils/text.js +0 -0
- package/dist/src/utils/text.js.map +1 -1
- package/dist/src/webhook/handler.d.ts +3 -0
- package/dist/src/webhook/handler.d.ts.map +1 -1
- package/dist/src/webhook/handler.js +85 -0
- package/dist/src/webhook/handler.js.map +1 -1
- package/dist-cjs/src/api/client.js +3 -1
- package/dist-cjs/src/api/transport.js +26 -2
- package/dist-cjs/src/branding/terminal.js +264 -1
- package/dist-cjs/src/broadcast/broadcast.js +58 -0
- package/dist-cjs/src/cli.js +7 -3
- package/dist-cjs/src/context/context.js +102 -8
- package/dist-cjs/src/core/bot.js +178 -29
- package/dist-cjs/src/index.js +2 -0
- package/dist-cjs/src/keyboard/index.js +13 -1
- package/dist-cjs/src/observability/logger.js +113 -1
- package/dist-cjs/src/plugins/plugin.js +11 -4
- package/dist-cjs/src/router/router.js +126 -24
- package/dist-cjs/src/state/forms.js +27 -24
- package/dist-cjs/src/storage/storage.js +20 -2
- package/dist-cjs/src/utils/concurrency.js +57 -0
- package/dist-cjs/src/utils/text.js +0 -0
- package/dist-cjs/src/webhook/handler.js +86 -0
- package/docs/API.id.md +120 -4
- package/docs/API.md +122 -4
- package/docs/API.zh-CN.md +119 -3
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,20 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.3.0 — 2026-08-29
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Concurrent update processing built for 1000+ message bursts: updates run in parallel across chats while staying ordered within a single chat, so a slow handler never blocks other chats, sessions never lose writes, and a concurrent burst triggers exactly one `getMe` initialization. `bot.handleUpdates(updates)` processes a whole batch at once and the polling loop uses it for every `getUpdates` batch. Cap simultaneous work with `updates: { concurrency }` (default `Infinity`).
|
|
8
|
+
- `bot.broadcast(chatIds, send, options)` for sending to 1000+ users at once: every chat is attempted immediately (no proactive cooldown, configurable `concurrency`), 429 answers are retried automatically after exactly the `retry_after` delay Telegram ordered, and the returned `BroadcastReport` lists delivered/failed counts with per-chat failure details plus `onProgress` streaming.
|
|
9
|
+
- `FetchTransport` flood gate (`floodGate`, default on): when Telegram answers 429, new requests wait out the `retry_after` window Telegram ordered — the only delay ever introduced, never a proactive cooldown.
|
|
10
|
+
- `Limiter` and `mapWithConcurrency` concurrency primitives (promise semaphore and ordered concurrent mapping) exported for applications.
|
|
4
11
|
|
|
5
12
|
### Fixed
|
|
6
13
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
- A burst of concurrent updates no longer races `init()`: initialization is memoized, so 1000 simultaneous updates trigger exactly one `getMe` call instead of one per update.
|
|
15
|
+
- Session writes are no longer lost when one chat sends several messages at once: same-chat updates are serialized in arrival order, so each update reads the session state written by the previous one.
|
|
16
|
+
|
|
17
|
+
## 0.2.1 — 2026-08-29
|
|
18
|
+
|
|
19
|
+
### Breaking
|
|
20
|
+
|
|
21
|
+
- Node.js 22 is now the minimum supported runtime (`engines.node: ">=22"`); CI tests Node 22 and 24. The TypeScript target moves to ES2023.
|
|
11
22
|
|
|
12
23
|
### Added
|
|
13
24
|
|
|
14
|
-
-
|
|
25
|
+
- `bot.on(filter, handler)` update-type filters with payload narrowing: `bot.on("message:photo")`, `bot.on("callback_query:data")`, or an array like `["message:text", "callback_query:data"]`. Invalid filters throw at registration time.
|
|
26
|
+
- `bot.hears(trigger, handler)` for exact text or `RegExp` message matching.
|
|
27
|
+
- `bot.catch(handler)` error boundary: handler failures are logged, emitted as `update:error`/`bot:error`, and routed to the handler instead of rejecting `handleUpdate()`; webhooks answer `200` and polling continues.
|
|
28
|
+
- Extended context senders with automatic quote-reply: `replyWithAnimation`, `replyWithVideoNote`, `replyWithSticker`, `replyWithMediaGroup`, `replyWithLocation`, `replyWithVenue`, `replyWithContact`, `replyWithPoll`, and `replyWithDice`.
|
|
29
|
+
- Animated terminal startup experience (on by default, `branding: false` to disable): typing effect for `Installing Dependencies......`, a glass progress bar with a sweeping highlight, and the animated rainbow ASCII banner `Tele Bibz` (figlet `Speed` font) that flows until the bot connects and then freezes with `✓ Connected as @<username>`.
|
|
30
|
+
- Human-readable incoming update logs: `[ => ] Message From {id} {nickname} {dd/mm/yyyy} {hh:mm:ss}` plus an indented content line. Regular message/command text is truncated to 50 characters; callback button data is shown in full. Errors print in red with the full stack. Non-interactive stdout falls back to plain output; `logger.format: "json"` emits structured `update.received` entries instead.
|
|
31
|
+
- Branding helpers exported for applications: `runStartupSequence()`, `startTeleBibzBanner()`, `printTeleBibzBanner()`, `paintRainbow()`, `printStatusLine()`, plus `Logger.incoming()` and `describeIncomingUpdate()`.
|
|
32
|
+
- `TransportRequest.timeoutMs` for per-request timeouts, honored by `FetchTransport` and `ApiClient.request()`.
|
|
15
33
|
- Storage-backed conversations, full five-field cron parsing, scheduler error hooks, permission-aware menus, `MenuController`, Web App init-data validation, PaymentsClient, and vendored Telegram declarations.
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Restored a green build: `npm run typecheck`, `lint`, `test:types`, `test:examples`, and `build` failed under TypeScript 5.9 with `exactOptionalPropertyTypes` (`Context.me` assignment, `RoutableContext.me` index-signature constraint, and an invalid `webhookCallback` cast).
|
|
38
|
+
- The published `telebibz` CLI binary crashed with `MODULE_NOT_FOUND` because `bin/telebibz.mjs` imported `../dist/cli.js`; the build emits `dist/src/cli.js`. `release:check` now verifies every bin import resolves inside the tarball.
|
|
39
|
+
- Long polling no longer races its own transport timeout: `getUpdates` now uses a per-request timeout of the polling timeout plus a 10-second buffer instead of the flat 30s transport default, which aborted healthy connections the moment Telegram responded.
|
|
40
|
+
- `bot.stop()` now aborts the in-flight long-poll request (the polling `AbortSignal` is passed through to the transport), so shutdown no longer blocks for up to the full polling timeout.
|
|
41
|
+
- `JsonFileStorage` now persists `expiresAt` metadata, so values written with a TTL no longer silently become permanent after a restart.
|
|
42
|
+
- `webhookCallback` now actually supports Koa-style contexts (`status`/`body`), reads secret-token headers from fetch `Request` header maps, and parses bodies from web-standard `Request` objects instead of misreading their `ReadableStream` `body` as a pre-parsed update.
|
|
43
|
+
- Passing `reply_parameters` in `extra` no longer discards the automatic quote `message_id`; user options now merge with it across all `reply`/`replyWith*` senders.
|
|
44
|
+
- Plugins install exactly once; `bot.restart()` no longer double-registers plugin middleware and routes.
|
|
45
|
+
- Callback-query contexts resolve `message` and `chat` from `callback_query.message`, so `ctx.reply()`, `ctx.edit()`, and `ctx.delete()` work for button callbacks.
|
|
46
|
+
- Router matching is first-match by default; explicit `matchMode: "all"` preserves deliberate fan-out without accidental double replies.
|
|
47
|
+
- Polling isolates handler failures per update, continues the remainder of a batch, emits `update:error`, and uses abortable reconnect backoff.
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- Development report files (`TELEBIBZ_FINAL_REPORT.md`, `TELEBIBZ_E2E_REPORT.md`, `NPM_RELEASE_REPORT.md`) and the feature-matrix row for an approval gate that was never part of the source tree.
|
|
18
52
|
|
|
19
53
|
## 0.1.2 — 2026-08-19
|
|
20
54
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -10,7 +10,7 @@ Check existing issues and pull requests before opening a new one. For a defect,
|
|
|
10
10
|
|
|
11
11
|
## Local setup
|
|
12
12
|
|
|
13
|
-
The project requires Node.js `>=
|
|
13
|
+
The project requires Node.js `>=22` and uses npm for release-compatible commands. Install dependencies and run the baseline checks:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm ci
|
|
@@ -32,7 +32,7 @@ Create a focused branch from `main`. Keep unrelated refactors out of a feature o
|
|
|
32
32
|
|
|
33
33
|
## Implementation expectations
|
|
34
34
|
|
|
35
|
-
Public behavior must be implemented in TypeScript with strict typing. Do not add fake responses, silent stubs, undocumented breaking behavior, or untested branches. Preserve ESM and CommonJS builds, zero runtime vendor dependencies in the core package, Node.js `>=
|
|
35
|
+
Public behavior must be implemented in TypeScript with strict typing. Do not add fake responses, silent stubs, undocumented breaking behavior, or untested branches. Preserve ESM and CommonJS builds, zero runtime vendor dependencies in the core package, Node.js `>=22` compatibility, and the existing package export map.
|
|
36
36
|
|
|
37
37
|
Changes to routing, context, lifecycle, transport, storage, queue, scheduler, approval, generated API declarations, or release automation require regression tests. Changes to a public function, class, method, option, error, event, or generated method require a corresponding API documentation update. Changes to examples require `npm run test:examples` and must use placeholders instead of credentials. Changes that affect package contents must pass `release:check` and `npm pack --dry-run`.
|
|
38
38
|
|
package/README.id.md
CHANGED
|
@@ -27,7 +27,7 @@ Showcase komunitas: [SHOWCASE.md](SHOWCASE.md)
|
|
|
27
27
|
npm install @xbibzlibrary/telebibz
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Node.js **
|
|
30
|
+
Node.js **22 atau lebih baru** diperlukan.
|
|
31
31
|
|
|
32
32
|
## Bot sederhana
|
|
33
33
|
|
|
@@ -67,9 +67,13 @@ bot.use(async (ctx, next) => {
|
|
|
67
67
|
bot.command("help", async (ctx) => { await ctx.reply("Bantuan tersedia."); });
|
|
68
68
|
bot.onRegex(/^order:(\\d+)$/, async (ctx) => { await ctx.reply("Order diterima."); });
|
|
69
69
|
bot.callback("profile:*", async (ctx) => { await ctx.answerCallbackQuery("Dibuka."); });
|
|
70
|
+
bot.on("message:photo", async (ctx) => { await ctx.reply("Foto yang bagus."); });
|
|
71
|
+
bot.on(["message:text", "callback_query:data"], async (ctx) => { await ctx.reply("Diterima."); });
|
|
72
|
+
bot.hears("ping", async (ctx) => { await ctx.reply("pong"); });
|
|
73
|
+
bot.catch(async (error, ctx) => { await ctx.reply("Terjadi kesalahan."); });
|
|
70
74
|
```
|
|
71
75
|
|
|
72
|
-
Router mendukung command, text, regex, pola callback, predikat kustom, router bersarang, middleware per rute, dan prioritas rute.
|
|
76
|
+
Router mendukung command, text, regex, pola callback, filter tipe update (`on`), predikat kustom, router bersarang, middleware per rute, dan prioritas rute. `bot.catch()` mendaftarkan error boundary: kegagalan handler diarahkan ke sana alih-alih menolak update.
|
|
73
77
|
|
|
74
78
|
## Telegram API
|
|
75
79
|
|
|
@@ -103,7 +107,7 @@ Builder hanya menghasilkan payload keyboard native Telegram. UI HTML/CSS memerlu
|
|
|
103
107
|
|
|
104
108
|
## Startup dan log terminal
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
Logger mengeluarkan baris terminal yang ringkas dan mudah dibaca dengan level berwarna serta konteks terstruktur. Level log: `silent`, `error`, `warn`, `info`, `debug`, dan `trace`; nilai sensitif di-redact; error dicetak merah lengkap dengan stack. Gunakan `format: "json"` untuk log terstruktur, dan `includeUpdateContent: true` hanya bila teks pesan atau data callback memang diperlukan.
|
|
107
111
|
|
|
108
112
|
## Webhook
|
|
109
113
|
|
|
@@ -118,13 +122,47 @@ const handler = createWebhookHandler(bot, {
|
|
|
118
122
|
|
|
119
123
|
`createWebhookHandler` menerima Request Web standar dan menghasilkan Response. Secret token, ukuran body, parsing JSON, dan penanganan update duplikat diverifikasi oleh handler.
|
|
120
124
|
|
|
125
|
+
## Update beban tinggi dan broadcast
|
|
126
|
+
|
|
127
|
+
telebibz dibangun untuk burst 1000+ pesan tanpa cooldown buatan:
|
|
128
|
+
|
|
129
|
+
- **Paralel antar chat, berurutan per chat.** Setiap batch `getUpdates` (dan setiap request webhook) diproses secara konkuren — update dari chat berbeda tidak pernah saling mengantre, sementara update dari chat yang sama menjaga urutan kedatangannya sehingga session, wizard, dan conversation tetap benar dan penulisan session tidak pernah hilang. Burst konkuren hanya memicu satu inisialisasi `getMe`.
|
|
130
|
+
- **Tidak ada throttling proaktif.** Permintaan keluar tidak pernah ditunda oleh library. Ketika Telegram menjawab 429, transport menunggu tepat jendela `retry_after` yang diperintahkan Telegram ("flood gate" global melindungi seluruh trafik) lalu otomatis retry — sehingga burst tetap terkirim lengkap, bukan gagal.
|
|
131
|
+
- **Broadcast ke 1000+ user sekaligus.** `bot.broadcast()` langsung mencoba semua chat, me-retry 429 sesuai `retry_after` dari Telegram sendiri, dan mengembalikan laporan lengkap.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const report = await bot.broadcast(
|
|
135
|
+
subscriberIds,
|
|
136
|
+
(chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "Newsletter #42" }),
|
|
137
|
+
{ onProgress: (p) => console.log(`${p.delivered}/${p.total} terkirim`) },
|
|
138
|
+
);
|
|
139
|
+
console.log(`Terkirim ${report.delivered}/${report.total} dalam ${report.durationMs}ms`);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Batasi pekerjaan simultan dengan `new Bot({ ..., updates: { concurrency: 64 } })` atau `broadcast(..., { concurrency: 64 })` jika downstream Anda (database, API) membutuhkannya — secara default keduanya berjalan sepenuhnya paralel.
|
|
143
|
+
|
|
121
144
|
## State, queue, scheduler, dan cache
|
|
122
145
|
|
|
123
146
|
Paket menyediakan `MemoryStorage` dengan TTL dan pembaruan atomik, `JsonFileStorage`, `RedisStorage`, `SqlStorage`, `MongoStorage`, persistent application state storage, session bot, conversation dan form berbasis Storage, menu berbasis permission, pagination `MenuController`, `MemoryCache`, token-bucket limiter, task queue dengan retry/backoff/concurrency/delay/cancel, serta scheduler interval, one-shot, dan cron lima field lengkap. Adapter Redis, SQL, dan Mongo memakai driver kecil sehingga core package tetap tanpa runtime dependency vendor.
|
|
124
147
|
|
|
148
|
+
## Pengalaman terminal
|
|
149
|
+
|
|
150
|
+
Saat bot dinyalakan di terminal interaktif (`npm start`, `node index.js`, `telebibz start`), telebibz memainkan urutan startup: efek ketik `Installing Dependencies......`, glass progress bar dengan kilau menyapu, dan banner ASCII rainbow animasi **Tele Bibz** (font figlet `Speed`) yang terus mengalir sampai bot terhubung, lalu diam dengan `✓ Connected as @<username>`.
|
|
151
|
+
|
|
152
|
+
Setelah itu, setiap update yang masuk ditampilkan dalam baris log yang mudah dibaca, dan error otomatis berwarna merah lengkap dengan stack-nya:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
[ => ] Message From 123456789 John Doe 29/08/2026 15:04:05
|
|
156
|
+
↳ Text: /start
|
|
157
|
+
[ => ] Callback From 123456789 John Doe 29/08/2026 15:04:07
|
|
158
|
+
↳ Data: menu:open
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Teks pesan/command biasa dibatasi 50 karakter; data tombol callback ditampilkan penuh. Matikan dengan `branding: false` pada `Bot`, atau pakai `logger.format: "json"` untuk log terstruktur. Output non-interaktif (pipe, Docker, CI) otomatis fallback ke teks polos tanpa animasi.
|
|
162
|
+
|
|
125
163
|
## CLI
|
|
126
164
|
|
|
127
|
-
|
|
165
|
+
Command CLI seperti `telebibz doctor`, `init`, dan `webhook` diawali banner rainbow `Tele Bibz`. Animasi startup otomatis fallback ke output statis bersih saat stdout bukan TTY.
|
|
128
166
|
|
|
129
167
|
```bash
|
|
130
168
|
npx telebibz init my-bot
|
|
@@ -136,8 +174,9 @@ npx telebibz test
|
|
|
136
174
|
Branding terminal juga dapat dicetak dari aplikasi:
|
|
137
175
|
|
|
138
176
|
```ts
|
|
139
|
-
import { printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
177
|
+
import { printTeleBibzBanner, printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
140
178
|
|
|
179
|
+
printTeleBibzBanner({ subtitle: "Bot saya" });
|
|
141
180
|
printTerminalBranding();
|
|
142
181
|
```
|
|
143
182
|
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/@xbibzlibrary/telebibz)
|
|
8
8
|
[](https://www.npmjs.com/package/@xbibzlibrary/telebibz)
|
|
9
9
|
|
|
10
|
-
**`@xbibzlibrary/telebibz`** is a
|
|
10
|
+
**`@xbibzlibrary/telebibz`** is a Telegram Bot SDK and framework for Node.js and TypeScript. It provides a typed API client, polling, routing, middleware, context helpers, keyboard builders, state/session primitives, webhooks, queues, scheduling, caching, plugin lifecycle, colorful terminal logging, CLI tooling, and testing utilities.
|
|
11
11
|
|
|
12
12
|
## Documentation languages
|
|
13
13
|
|
|
@@ -29,7 +29,7 @@ Community showcase: [SHOWCASE.md](SHOWCASE.md)
|
|
|
29
29
|
npm install @xbibzlibrary/telebibz
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Node.js **
|
|
32
|
+
Node.js **22 or newer** is required.
|
|
33
33
|
|
|
34
34
|
## Minimal bot
|
|
35
35
|
|
|
@@ -69,9 +69,13 @@ bot.use(async (ctx, next) => {
|
|
|
69
69
|
bot.command("help", async (ctx) => { await ctx.reply("Help is available."); });
|
|
70
70
|
bot.onRegex(/^order:(\\d+)$/, async (ctx) => { await ctx.reply("Order received."); });
|
|
71
71
|
bot.callback("profile:*", async (ctx) => { await ctx.answerCallbackQuery("Opened."); });
|
|
72
|
+
bot.on("message:photo", async (ctx) => { await ctx.reply("Nice photo."); });
|
|
73
|
+
bot.on(["message:text", "callback_query:data"], async (ctx) => { await ctx.reply("Got it."); });
|
|
74
|
+
bot.hears("ping", async (ctx) => { await ctx.reply("pong"); });
|
|
75
|
+
bot.catch(async (error, ctx) => { await ctx.reply("Something went wrong."); });
|
|
72
76
|
```
|
|
73
77
|
|
|
74
|
-
The router supports commands, exact text, regular expressions, callback patterns, custom predicates, nested routers, per-route middleware, and route priority.
|
|
78
|
+
The router supports commands, exact text, regular expressions, callback patterns, update-type filters (`on`), custom predicates, nested routers, per-route middleware, and route priority. `bot.catch()` registers an error boundary: handler failures are routed there instead of rejecting the update.
|
|
75
79
|
|
|
76
80
|
## Telegram API
|
|
77
81
|
|
|
@@ -103,7 +107,7 @@ Builders produce native Telegram keyboard payloads. HTML/CSS interfaces require
|
|
|
103
107
|
|
|
104
108
|
## Colorful runtime logging
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
The logger emits compact, readable terminal lines with colored levels and structured context. Log levels are `silent`, `error`, `warn`, `info`, `debug`, and `trace`; sensitive values are redacted; errors print in red with the full stack. Use `format: "json"` for machine ingestion and `includeUpdateContent: true` only when message text or callback data is explicitly required.
|
|
107
111
|
|
|
108
112
|
```ts
|
|
109
113
|
const bot = new Bot({
|
|
@@ -146,13 +150,47 @@ const handler = createWebhookHandler(bot, {
|
|
|
146
150
|
|
|
147
151
|
`createWebhookHandler` accepts a standard Web `Request` and returns a `Response`. It verifies the optional secret token, body size, JSON payload, and update shape before calling `bot.handleUpdate()`.
|
|
148
152
|
|
|
153
|
+
## High-load updates and broadcast
|
|
154
|
+
|
|
155
|
+
telebibz is built for bursts of 1000+ messages with no artificial cooldown:
|
|
156
|
+
|
|
157
|
+
- **Parallel across chats, ordered per chat.** Every `getUpdates` batch (and every webhook request) is processed concurrently — updates from different chats never queue behind each other, while updates from the same chat keep their arrival order so sessions, wizards, and conversations stay correct and session writes are never lost. A concurrent burst triggers exactly one `getMe` initialization.
|
|
158
|
+
- **No proactive throttling.** Outgoing requests are never delayed by the library. When Telegram answers 429, the transport waits exactly the `retry_after` window Telegram ordered (a global "flood gate" protects all in-flight traffic) and retries automatically — so bursts deliver completely instead of failing.
|
|
159
|
+
- **Broadcast to 1000+ users at once.** `bot.broadcast()` attempts every chat immediately, retries 429s per Telegram's own `retry_after`, and returns a full report.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const report = await bot.broadcast(
|
|
163
|
+
subscriberIds,
|
|
164
|
+
(chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "Newsletter #42" }),
|
|
165
|
+
{ onProgress: (p) => console.log(`${p.delivered}/${p.total} delivered`) },
|
|
166
|
+
);
|
|
167
|
+
console.log(`Delivered ${report.delivered}/${report.total} in ${report.durationMs}ms`);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Cap simultaneous work with `new Bot({ ..., updates: { concurrency: 64 } })` or `broadcast(..., { concurrency: 64 })` when your own downstream (database, API) needs it — by default both run fully parallel.
|
|
171
|
+
|
|
149
172
|
## State, queue, scheduler, and cache
|
|
150
173
|
|
|
151
174
|
The package provides `MemoryStorage` with TTL and serialized per-key updates, `JsonFileStorage`, `RedisStorage`, `SqlStorage`, `MongoStorage`, persistent application state storage, bot sessions, storage-backed conversations and forms, permission-aware menus, `MenuController` pagination, `MemoryCache`, a token-bucket limiter, a task queue with retry/backoff/concurrency/delay/cancel, and schedulers for intervals, one-shot tasks, and full five-field cron expressions. Redis, SQL, and Mongo adapters use small driver interfaces so the core package remains free of vendor runtime dependencies.
|
|
152
175
|
|
|
176
|
+
## Terminal experience
|
|
177
|
+
|
|
178
|
+
When the bot starts on an interactive terminal (`npm start`, `node index.js`, `telebibz start`), telebibz plays a startup sequence: a typing effect for `Installing Dependencies......`, a glass progress bar with a sweeping highlight, and the animated rainbow ASCII banner **Tele Bibz** (figlet `Speed` font) that keeps flowing until the bot connects, then freezes with `✓ Connected as @<username>`.
|
|
179
|
+
|
|
180
|
+
Afterwards, every incoming update is logged on a human-readable line, and errors are printed in red with the full stack:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
[ => ] Message From 123456789 John Doe 29/08/2026 15:04:05
|
|
184
|
+
↳ Text: /start
|
|
185
|
+
[ => ] Callback From 123456789 John Doe 29/08/2026 15:04:07
|
|
186
|
+
↳ Data: menu:open
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Message and command text is truncated to 50 characters; callback button data is shown in full. Pass `branding: false` to `Bot` to disable the sequence, or set `logger.format: "json"` for structured log ingestion. Non-interactive stdout (pipes, Docker, CI) automatically falls back to plain output without animations.
|
|
190
|
+
|
|
153
191
|
## CLI
|
|
154
192
|
|
|
155
|
-
|
|
193
|
+
CLI commands such as `telebibz doctor`, `init`, and `webhook` start with the rainbow `Tele Bibz` banner. Startup animation automatically falls back to clean static output when stdout is not a TTY.
|
|
156
194
|
|
|
157
195
|
```bash
|
|
158
196
|
npm start
|
|
@@ -165,8 +203,9 @@ npx telebibz test
|
|
|
165
203
|
Applications can print the same terminal branding explicitly:
|
|
166
204
|
|
|
167
205
|
```ts
|
|
168
|
-
import { printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
206
|
+
import { printTeleBibzBanner, printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
169
207
|
|
|
208
|
+
printTeleBibzBanner({ subtitle: "My bot" });
|
|
170
209
|
printTerminalBranding();
|
|
171
210
|
```
|
|
172
211
|
|
package/README.zh-CN.md
CHANGED
|
@@ -27,7 +27,7 @@ GitHub Packages 指南:[English](docs/GITHUB_PACKAGES.md) · [Bahasa Indonesia
|
|
|
27
27
|
npm install @xbibzlibrary/telebibz
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
需要 Node.js **
|
|
30
|
+
需要 Node.js **22 或更高版本**。
|
|
31
31
|
|
|
32
32
|
## 简单机器人
|
|
33
33
|
|
|
@@ -67,9 +67,13 @@ bot.use(async (ctx, next) => {
|
|
|
67
67
|
bot.command("help", async (ctx) => { await ctx.reply("Bantuan tersedia."); });
|
|
68
68
|
bot.onRegex(/^order:(\\d+)$/, async (ctx) => { await ctx.reply("Order diterima."); });
|
|
69
69
|
bot.callback("profile:*", async (ctx) => { await ctx.answerCallbackQuery("Dibuka."); });
|
|
70
|
+
bot.on("message:photo", async (ctx) => { await ctx.reply("照片不错。"); });
|
|
71
|
+
bot.on(["message:text", "callback_query:data"], async (ctx) => { await ctx.reply("收到。"); });
|
|
72
|
+
bot.hears("ping", async (ctx) => { await ctx.reply("pong"); });
|
|
73
|
+
bot.catch(async (error, ctx) => { await ctx.reply("出错了。"); });
|
|
70
74
|
```
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
路由器支持命令、文本、正则、回调模式、更新类型过滤器(`on`)、自定义谓词、嵌套路由器、每条路由的中间件,以及路由优先级。`bot.catch()` 注册错误边界:处理器失败会转发到那里,而不是拒绝整个 update。
|
|
73
77
|
|
|
74
78
|
## Telegram API
|
|
75
79
|
|
|
@@ -103,7 +107,7 @@ await ctx.reply("Pilih menu:", { reply_markup: keyboard });
|
|
|
103
107
|
|
|
104
108
|
## Startup and terminal logs
|
|
105
109
|
|
|
106
|
-
|
|
110
|
+
The logger emits compact, readable terminal lines with colored levels and structured context. Log levels are `silent`, `error`, `warn`, `info`, `debug`, and `trace`; sensitive values are redacted; errors print in red with the full stack. Use `format: "json"` for machine ingestion and `includeUpdateContent: true` only when message text or callback data is explicitly required.
|
|
107
111
|
|
|
108
112
|
## Webhook
|
|
109
113
|
|
|
@@ -118,13 +122,47 @@ const handler = createWebhookHandler(bot, {
|
|
|
118
122
|
|
|
119
123
|
`createWebhookHandler` 接受标准 Web `Request` 并返回 `Response`。处理程序会验证 secret token、body 大小、JSON 解析以及重复更新处理。
|
|
120
124
|
|
|
125
|
+
## 高负载更新与广播
|
|
126
|
+
|
|
127
|
+
telebibz 为 1000+ 条消息的突发场景而生,没有任何人为冷却:
|
|
128
|
+
|
|
129
|
+
- **跨 chat 并行,同一 chat 内按序。** 每个 `getUpdates` 批次(以及每个 webhook 请求)都并发处理——不同 chat 的 update 不会互相排队,而同一 chat 的 update 保持到达顺序,因此会话、wizard 和 conversation 始终正确,会话写入永不丢失。并发突发只触发一次 `getMe` 初始化。
|
|
130
|
+
- **没有主动限流。** 库永远不会延迟外发请求。当 Telegram 返回 429 时,transport 会严格按照 Telegram 指定的 `retry_after` 窗口等待(全局 "flood gate" 保护所有进行中的流量)并自动重试——因此突发流量会完整送达而不是失败。
|
|
131
|
+
- **一次性向 1000+ 用户广播。** `bot.broadcast()` 立即尝试所有 chat,按照 Telegram 自己的 `retry_after` 重试 429,并返回完整报告。
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
const report = await bot.broadcast(
|
|
135
|
+
subscriberIds,
|
|
136
|
+
(chatId) => bot.api.methods.sendMessage({ chat_id: chatId, text: "Newsletter #42" }),
|
|
137
|
+
{ onProgress: (p) => console.log(`${p.delivered}/${p.total} delivered`) },
|
|
138
|
+
);
|
|
139
|
+
console.log(`Delivered ${report.delivered}/${report.total} in ${report.durationMs}ms`);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
当你自己的下游(数据库、API)需要时,可以用 `new Bot({ ..., updates: { concurrency: 64 } })` 或 `broadcast(..., { concurrency: 64 })` 限制并发——默认情况下两者都完全并行。
|
|
143
|
+
|
|
121
144
|
## 状态、队列、调度器和缓存
|
|
122
145
|
|
|
123
146
|
该包提供带 TTL 和原子更新的 `MemoryStorage`、`JsonFileStorage`、`RedisStorage`、`SqlStorage`、`MongoStorage`、bot session、基于 Storage 的 conversation/form、基于 permission 的菜单、`MenuController` 分页、`MemoryCache`、令牌桶限流器、支持重试/退避/并发/延迟/取消的任务队列,以及间隔、一次性和完整五字段 cron 的调度器。Redis、SQL 和 Mongo 适配器使用小型 driver interface,因此 core package 不需要 vendor runtime dependency。
|
|
124
147
|
|
|
148
|
+
## 终端体验
|
|
149
|
+
|
|
150
|
+
当 bot 在交互式终端启动时(`npm start`、`node index.js`、`telebibz start`),telebibz 会播放启动序列:`Installing Dependencies......` 打字效果、带扫过高光的 glass 进度条,以及动画彩虹 ASCII 横幅 **Tele Bibz**(figlet `Speed` 字体)——彩虹持续流动直到 bot 连接成功,随后定格并显示 `✓ Connected as @<username>`。
|
|
151
|
+
|
|
152
|
+
之后,每一条进入的 update 都会以易读的格式输出,错误自动以红色打印并附带完整堆栈:
|
|
153
|
+
|
|
154
|
+
```text
|
|
155
|
+
[ => ] Message From 123456789 John Doe 29/08/2026 15:04:05
|
|
156
|
+
↳ Text: /start
|
|
157
|
+
[ => ] Callback From 123456789 John Doe 29/08/2026 15:04:07
|
|
158
|
+
↳ Data: menu:open
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
普通消息与命令文本截断为 50 个字符;回调按钮数据完整显示。向 `Bot` 传入 `branding: false` 可关闭启动序列,或设置 `logger.format: "json"` 获取结构化日志。非交互 stdout(管道、Docker、CI)会自动回退到无动画的纯文本输出。
|
|
162
|
+
|
|
125
163
|
## CLI
|
|
126
164
|
|
|
127
|
-
|
|
165
|
+
`telebibz doctor`、`init`、`webhook` 等 CLI command 以彩虹 `Tele Bibz` 横幅开始。当 stdout 不是 TTY 时,启动动画自动回退为干净的静态输出。
|
|
128
166
|
|
|
129
167
|
```bash
|
|
130
168
|
npx telebibz init my-bot
|
|
@@ -136,8 +174,9 @@ npx telebibz test
|
|
|
136
174
|
也可以在应用中打印相同的 terminal branding:
|
|
137
175
|
|
|
138
176
|
```ts
|
|
139
|
-
import { printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
177
|
+
import { printTeleBibzBanner, printTerminalBranding } from "@xbibzlibrary/telebibz";
|
|
140
178
|
|
|
179
|
+
printTeleBibzBanner({ subtitle: "My bot" });
|
|
141
180
|
printTerminalBranding();
|
|
142
181
|
```
|
|
143
182
|
|
package/RELEASE_AUTOMATION.md
CHANGED
|
@@ -8,17 +8,29 @@ Setiap push ke branch `main` menjalankan workflow `.github/workflows/auto-publis
|
|
|
8
8
|
|
|
9
9
|
| Tahap | Perilaku |
|
|
10
10
|
|---|---|
|
|
11
|
-
| Checkout | Mengambil seluruh history
|
|
11
|
+
| Checkout | Mengambil seluruh history beserta tag (`fetch-depth: 0` + `fetch-tags: true`) agar analisis commit dan pembuatan tag akurat. |
|
|
12
12
|
| Install | Menjalankan `npm ci --ignore-scripts`. |
|
|
13
|
-
| Version | Membaca versi
|
|
13
|
+
| Version | Membaca versi `package.json`, versi latest npm, dan commit sejak tag release terakhir, lalu memilih versi berikutnya (lihat aturan di bawah). |
|
|
14
14
|
| Verification | Menjalankan typecheck, type-level tests, lint, runtime tests, build ESM/CommonJS, security audit, dan release check. |
|
|
15
15
|
| Immutable guard | Menolak publish jika versi target sudah ada di npmjs. |
|
|
16
|
-
| Git sync | Commit otomatis `chore(release): vX.Y.Z [skip release]
|
|
16
|
+
| Git sync | Commit otomatis `chore(release): vX.Y.Z [skip release]` (dilewati bila `package.json` sudah berada di versi target), membuat annotated tag `vX.Y.Z`, lalu push commit dan tag ke GitHub. |
|
|
17
17
|
| npmjs publish | Menerbitkan package public menggunakan `NPM_TOKEN`; provenance dinonaktifkan karena npm menolak provenance dari source repository private. |
|
|
18
18
|
| GitHub Release | Membuat GitHub Release dengan generated notes. |
|
|
19
19
|
|
|
20
20
|
Push commit version otomatis tidak memicu release kedua karena mengandung `[skip release]`. Workflow menggunakan concurrency sehingga release berjalan satu per satu.
|
|
21
21
|
|
|
22
|
+
## Aturan penomoran versi
|
|
23
|
+
|
|
24
|
+
Versi berikutnya dihitung dari `max(package.json, npm latest)` dengan bump berdasarkan Conventional Commits sejak tag release terakhir:
|
|
25
|
+
|
|
26
|
+
| Commit sejak tag terakhir | 0.x | >=1.0.0 |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `BREAKING CHANGE:` atau `feat!:` / `fix!:` | minor (`0.1.19` → `0.2.0`) | major (`1.2.3` → `2.0.0`) |
|
|
29
|
+
| `feat:` / `feat(scope):` | minor | minor |
|
|
30
|
+
| lainnya (`fix:`, `docs:`, `chore:`, …) | patch | patch |
|
|
31
|
+
|
|
32
|
+
Jika `package.json` sudah dideklarasikan lebih tinggi daripada versi npm (misalnya dipersiapkan manual untuk `0.2.0`), workflow memakai versi tersebut apa adanya. Hasil perhitungan tidak pernah boleh lebih rendah daripada versi npm yang sudah terbit; jika demikian, workflow gagal dengan pesan yang jelas.
|
|
33
|
+
|
|
22
34
|
## Secret dan permission yang wajib tersedia
|
|
23
35
|
|
|
24
36
|
Workflow membutuhkan `contents: write` untuk version bump, tag, dan GitHub Release. Buka repository GitHub, kemudian masuk ke **Settings → Secrets and variables → Actions** dan tambahkan repository atau environment secret berikut:
|
|
@@ -33,9 +45,9 @@ Jangan menyimpan token di repository, `.npmrc`, source code, issue, commit, atau
|
|
|
33
45
|
|
|
34
46
|
## Aturan penggunaan
|
|
35
47
|
|
|
36
|
-
Perubahan source biasa dapat dipush ke `main`; workflow
|
|
48
|
+
Perubahan source biasa dapat dipush ke `main`; workflow menghitung versi baru berdasarkan Conventional Commits (`feat:` → minor, `fix:`/lainnya → patch, `feat!:`/`BREAKING CHANGE:` → minor pada 0.x / major pada 1.x) setelah seluruh quality gates lulus. Karena versi npm immutable, workflow tidak pernah menimpa versi yang telah ada.
|
|
37
49
|
|
|
38
|
-
Untuk
|
|
50
|
+
Untuk rilis yang dipersiapkan secara eksplisit (misalnya `0.2.0` atau `1.0.0`), deklarasikan versi tersebut langsung di `package.json` sebelum push; workflow akan memakainya apa adanya selama lebih tinggi daripada versi npm yang sudah terbit.
|
|
39
51
|
|
|
40
52
|
Untuk perubahan dokumentasi atau perubahan internal yang tidak boleh menerbitkan npm, gunakan commit message yang memuat marker berikut:
|
|
41
53
|
|
package/bin/telebibz.mjs
CHANGED
package/dist/src/api/client.d.ts
CHANGED
|
@@ -26,7 +26,9 @@ export declare class ApiClient {
|
|
|
26
26
|
private readonly hooks;
|
|
27
27
|
constructor(options: ApiClientOptions);
|
|
28
28
|
call<M extends TelegramMethodName>(method: M, ...args: ApiCallArgs<M>): Promise<ApiResult<M>>;
|
|
29
|
-
request<M extends TelegramMethodName>(method: M, payload?: ApiParams<M>, signal?: AbortSignal
|
|
29
|
+
request<M extends TelegramMethodName>(method: M, payload?: ApiParams<M>, signal?: AbortSignal, options?: {
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}): Promise<ApiResult<M>>;
|
|
30
32
|
raw(method: string, payload?: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>;
|
|
31
33
|
}
|
|
32
34
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAExF,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,cAAc;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AACnK,MAAM,WAAW,gBAAgB;IAAG,SAAS,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAA;CAAE;AAClQ,MAAM,MAAM,UAAU,GAAG;KAAG,CAAC,IAAI,kBAAkB,GAAG,CAAC,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAE3G,qBAAa,SAAS;IACpB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyC;gBACnD,OAAO,EAAE,gBAAgB;IAa/B,IAAI,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC7F,OAAO,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAExF,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,KAAK,EAAE,SAAS,EAAoB,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,cAAc;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AACnK,MAAM,WAAW,gBAAgB;IAAG,SAAS,EAAE,SAAS,CAAC;IAAC,KAAK,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;KAAE,CAAA;CAAE;AAClQ,MAAM,MAAM,UAAU,GAAG;KAAG,CAAC,IAAI,kBAAkB,GAAG,CAAC,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAE3G,qBAAa,SAAS;IACpB,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyC;gBACnD,OAAO,EAAE,gBAAgB;IAa/B,IAAI,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAC7F,OAAO,CAAC,CAAC,SAAS,kBAAkB,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;IAsBvJ,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;CAQrG"}
|
package/dist/src/api/client.js
CHANGED
|
@@ -21,7 +21,7 @@ export class ApiClient {
|
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
async call(method, ...args) { return this.request(method, args[0]); }
|
|
24
|
-
async request(method, payload, signal) {
|
|
24
|
+
async request(method, payload, signal, options) {
|
|
25
25
|
const context = { method, payload, startedAt: Date.now() };
|
|
26
26
|
await this.hooks.onRequest?.(context);
|
|
27
27
|
try {
|
|
@@ -30,6 +30,8 @@ export class ApiClient {
|
|
|
30
30
|
request.payload = payload;
|
|
31
31
|
if (signal !== undefined)
|
|
32
32
|
request.signal = signal;
|
|
33
|
+
if (options?.timeoutMs !== undefined)
|
|
34
|
+
request.timeoutMs = options.timeoutMs;
|
|
33
35
|
const response = await this.transport.request(request);
|
|
34
36
|
context.durationMs = Date.now() - context.startedAt;
|
|
35
37
|
context.response = response.data;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAA2B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAQvE,MAAM,OAAO,SAAS;IACX,OAAO,CAAa;IACZ,SAAS,CAAY;IACrB,KAAK,CAAyC;IAC/D,YAAY,OAAyB;QACnC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QACjC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAS,qBAAqB,CAAC,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,IAAI,KAAK,CAAC,EAAE,EAAE;YAC3B,GAAG,EAAE,CAAC,OAAO,EAAE,QAAyB,EAAE,EAAE;gBAC1C,IAAI,OAAO,QAAQ,KAAK,QAAQ;oBAAE,OAAO,SAAS,CAAC;gBACnD,IAAI,QAAQ,KAAK,MAAM;oBAAE,OAAO,SAAS,CAAC;gBAC1C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAC;gBACjG,OAAO,CAAC,OAAiB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAA8B,EAAE,OAAgB,CAAC,CAAC;YAC/F,CAAC;SACF,CAAe,CAAC;IACnB,CAAC;IACD,KAAK,CAAC,IAAI,CAA+B,MAAS,EAAE,GAAG,IAAoB,IAA2B,OAAO,IAAI,CAAC,OAAO,CAAI,MAAM,EAAE,IAAI,CAAC,CAAC,CAA6B,CAAC,CAAC,CAAC,CAAC;IAC5K,KAAK,CAAC,OAAO,CAA+B,MAAS,EAAE,OAAsB,EAAE,MAAoB;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/api/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAA2B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAQvE,MAAM,OAAO,SAAS;IACX,OAAO,CAAa;IACZ,SAAS,CAAY;IACrB,KAAK,CAAyC;IAC/D,YAAY,OAAyB;QACnC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;QACnC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;QACjC,MAAM,YAAY,GAAG,IAAI,GAAG,CAAS,qBAAqB,CAAC,CAAC;QAC5D,IAAI,CAAC,OAAO,GAAG,IAAI,KAAK,CAAC,EAAE,EAAE;YAC3B,GAAG,EAAE,CAAC,OAAO,EAAE,QAAyB,EAAE,EAAE;gBAC1C,IAAI,OAAO,QAAQ,KAAK,QAAQ;oBAAE,OAAO,SAAS,CAAC;gBACnD,IAAI,QAAQ,KAAK,MAAM;oBAAE,OAAO,SAAS,CAAC;gBAC1C,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC;oBAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,QAAQ,EAAE,CAAC,CAAC;gBACjG,OAAO,CAAC,OAAiB,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,QAA8B,EAAE,OAAgB,CAAC,CAAC;YAC/F,CAAC;SACF,CAAe,CAAC;IACnB,CAAC;IACD,KAAK,CAAC,IAAI,CAA+B,MAAS,EAAE,GAAG,IAAoB,IAA2B,OAAO,IAAI,CAAC,OAAO,CAAI,MAAM,EAAE,IAAI,CAAC,CAAC,CAA6B,CAAC,CAAC,CAAC,CAAC;IAC5K,KAAK,CAAC,OAAO,CAA+B,MAAS,EAAE,OAAsB,EAAE,MAAoB,EAAE,OAAgC;QACnI,MAAM,OAAO,GAAmB,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;QAC3E,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,CAAC;YAC7C,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO,CAAC,OAAO,GAAG,OAAkC,CAAC;YAChF,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;YAClD,IAAI,OAAO,EAAE,SAAS,KAAK,SAAS;gBAAE,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,CAAC;YAC5E,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAe,OAAO,CAAC,CAAC;YACrE,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;YACpD,OAAO,CAAC,QAAQ,GAAG,QAAQ,CAAC,IAAiC,CAAC;YAC9D,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,CAAC;YACvC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS;gBAAE,MAAM,yBAAyB,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1J,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;QAC9B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC;YACpD,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC;YACtB,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC;YACpC,IAAI,KAAK,YAAY,aAAa;gBAAE,MAAM,KAAK,CAAC;YAChD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,MAAc,EAAE,OAAiC,EAAE,MAAoB;QAC/E,MAAM,OAAO,GAAqB,EAAE,MAAM,EAAE,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS;YAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC;QACrD,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;QAClD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACvD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE;YAAE,MAAM,yBAAyB,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QACpH,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC;IAC9B,CAAC;CACF"}
|
|
@@ -3,6 +3,8 @@ export interface TransportRequest {
|
|
|
3
3
|
method: string;
|
|
4
4
|
payload?: Record<string, unknown>;
|
|
5
5
|
signal?: AbortSignal;
|
|
6
|
+
/** Per-request timeout in milliseconds; overrides the transport default. */
|
|
7
|
+
timeoutMs?: number;
|
|
6
8
|
}
|
|
7
9
|
export interface TransportResponse<T = unknown> {
|
|
8
10
|
status: number;
|
|
@@ -21,6 +23,14 @@ export interface FetchTransportOptions {
|
|
|
21
23
|
maxBackoffMs?: number;
|
|
22
24
|
jitter?: number;
|
|
23
25
|
headers?: HeadersInit;
|
|
26
|
+
/**
|
|
27
|
+
* When Telegram answers 429 (rate limit), pause NEW requests until the
|
|
28
|
+
* `retry_after` window Telegram ordered has elapsed. Default `true`.
|
|
29
|
+
* This is never a proactive cooldown: the only waiting ever done is the
|
|
30
|
+
* delay Telegram itself demands, so a flood on one method protects the rest
|
|
31
|
+
* of the traffic instead of every request hitting the same 429 wall.
|
|
32
|
+
*/
|
|
33
|
+
floodGate?: boolean;
|
|
24
34
|
}
|
|
25
35
|
export declare class FetchTransport implements Transport {
|
|
26
36
|
private readonly baseUrl;
|
|
@@ -31,7 +41,14 @@ export declare class FetchTransport implements Transport {
|
|
|
31
41
|
private readonly maxBackoffMs;
|
|
32
42
|
private readonly jitter;
|
|
33
43
|
private readonly headers;
|
|
44
|
+
private readonly floodGate;
|
|
45
|
+
/** Timestamp (ms) until which Telegram asked us to stop sending. */
|
|
46
|
+
private floodUntil;
|
|
34
47
|
constructor(options?: FetchTransportOptions);
|
|
35
|
-
|
|
48
|
+
/** Waits out the remainder of a Telegram-ordered flood window, if any. */
|
|
49
|
+
private waitForFloodWindow;
|
|
50
|
+
/** Extends the flood window when Telegram answers 429 or sends retry_after. */
|
|
51
|
+
private recordFloodWindow;
|
|
52
|
+
request<T>({ method, payload, signal, timeoutMs }: TransportRequest): Promise<TransportResponse<T>>;
|
|
36
53
|
}
|
|
37
54
|
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/api/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/api/transport.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAa,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9D,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,4EAA4E;IAC5E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,OAAO;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,qBAAa,cAAe,YAAW,SAAS;IAC9C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAU;IACpC,oEAAoE;IACpE,OAAO,CAAC,UAAU,CAAK;gBAEX,OAAO,GAAE,qBAA0B;IAa/C,0EAA0E;YAC5D,kBAAkB;IAMhC,+EAA+E;IAC/E,OAAO,CAAC,iBAAiB;IAOnB,OAAO,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,OAAY,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;CAiF/G"}
|
|
@@ -10,6 +10,9 @@ export class FetchTransport {
|
|
|
10
10
|
maxBackoffMs;
|
|
11
11
|
jitter;
|
|
12
12
|
headers;
|
|
13
|
+
floodGate;
|
|
14
|
+
/** Timestamp (ms) until which Telegram asked us to stop sending. */
|
|
15
|
+
floodUntil = 0;
|
|
13
16
|
constructor(options = {}) {
|
|
14
17
|
this.baseUrl = (options.baseUrl ?? "https://api.telegram.org").replace(/\/$/, "");
|
|
15
18
|
this.fetchImpl = options.fetch ?? globalThis.fetch;
|
|
@@ -21,8 +24,27 @@ export class FetchTransport {
|
|
|
21
24
|
this.maxBackoffMs = options.maxBackoffMs ?? 8_000;
|
|
22
25
|
this.jitter = options.jitter ?? 0.2;
|
|
23
26
|
this.headers = options.headers ?? {};
|
|
27
|
+
this.floodGate = options.floodGate ?? true;
|
|
24
28
|
}
|
|
25
|
-
|
|
29
|
+
/** Waits out the remainder of a Telegram-ordered flood window, if any. */
|
|
30
|
+
async waitForFloodWindow() {
|
|
31
|
+
if (!this.floodGate)
|
|
32
|
+
return;
|
|
33
|
+
const waitMs = this.floodUntil - Date.now();
|
|
34
|
+
if (waitMs > 0)
|
|
35
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
36
|
+
}
|
|
37
|
+
/** Extends the flood window when Telegram answers 429 or sends retry_after. */
|
|
38
|
+
recordFloodWindow(data) {
|
|
39
|
+
if (!this.floodGate)
|
|
40
|
+
return;
|
|
41
|
+
if (data.ok || (data.error_code !== 429 && data.parameters?.retry_after === undefined))
|
|
42
|
+
return;
|
|
43
|
+
const retryAfterMs = Math.max(0, (data.parameters?.retry_after ?? 0) * 1000);
|
|
44
|
+
this.floodUntil = Math.max(this.floodUntil, Date.now() + retryAfterMs);
|
|
45
|
+
}
|
|
46
|
+
async request({ method, payload = {}, signal, timeoutMs }) {
|
|
47
|
+
const effectiveTimeoutMs = timeoutMs ?? this.timeoutMs;
|
|
26
48
|
const hasUpload = await containsUpload(payload);
|
|
27
49
|
let body;
|
|
28
50
|
const headers = new Headers(this.headers);
|
|
@@ -38,9 +60,10 @@ export class FetchTransport {
|
|
|
38
60
|
}
|
|
39
61
|
let attempt = 0;
|
|
40
62
|
while (true) {
|
|
63
|
+
await this.waitForFloodWindow();
|
|
41
64
|
let responseStatus;
|
|
42
65
|
const controller = new AbortController();
|
|
43
|
-
const timeout = setTimeout(() => controller.abort(new Error(`Request timed out after ${
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(new Error(`Request timed out after ${effectiveTimeoutMs}ms`)), effectiveTimeoutMs);
|
|
44
67
|
const onAbort = () => controller.abort(signal?.reason);
|
|
45
68
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
69
|
try {
|
|
@@ -64,6 +87,7 @@ export class FetchTransport {
|
|
|
64
87
|
throw error;
|
|
65
88
|
}
|
|
66
89
|
const data = await response.json();
|
|
90
|
+
this.recordFloodWindow(data);
|
|
67
91
|
if (!data.ok && isRetryableResponse(response.status, data) && attempt < this.retries) {
|
|
68
92
|
const retryAfterMs = data.parameters?.retry_after === undefined ? undefined : Math.max(0, data.parameters.retry_after * 1000);
|
|
69
93
|
await waitBeforeRetry(attempt, this.backoffMs, this.maxBackoffMs, this.jitter, retryAfterMs);
|