@adriangalilea/utils 2.22.0 β†’ 2.24.0

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/README.md CHANGED
@@ -517,6 +517,8 @@ Telegram's [Threaded Mode](https://telegram.org/blog/threaded-conversations) for
517
517
  | `@adriangalilea/utils/bot/language` (picker surface) | The language-picker vocabulary and surfaces, so bots don't fork label lists: `flagFor` / `autonym` / `languageLabel` ("πŸ‡ͺπŸ‡Έ EspaΓ±ol"), `languagePickerItem({ label, codes, isActive, pick })` (the 2-up primary-highlighted MenuItem factory β€” storage and policy live in your closures), and `addLanguageRows(kb, { codes, pack, active?, activeStyle? })` (the raw-InlineKeyboard twin for onboarding / group-welcome keyboards; `activeStyle: "success"` when primary already marks something else on the keyboard). |
518
518
  | `@adriangalilea/utils/bot/user` | `userLabel(u)` β€” the conditional "[name] [@username] [id]" line every bot re-rolls for admin DMs and logs: `Ada Lovelace (@ada Β· 42)` / `Ada (42)` / `@ada (42)` / `id 42`, missing pieces drop instead of padding. Reads both gramio spellings (`firstName` / `first_name`); plain text by design. |
519
519
  | `@adriangalilea/utils/bot/urls` | Telegram-message URL + entity parsing, on what the platform ALREADY parsed (entities carry exact UTF-16 spans β€” never `startsWith("/")` or a mention regex). `urlsInMessage(msg)` β†’ the url module's `Url[]`: the visible text scanned by `urlsIn` PLUS `text_link` entities β€” hyperlinked words whose URL never appears in the text, invisible to any text scanner (what forwarded newsletters carry); a text_link's `raw` is its anchor words and its span covers them, so span-cutting treats linked words as the link. `isCommandMessage(msg)` (a `bot_command` entity at offset 0), `commandToken(msg)` (the leading command without any `@botname` suffix, `/summary@my_bot` β†’ `/summary`; null for ordinary text), `commandAddressee(msg)` (the bot a command is explicitly addressed to, lowercased, null when bare β€” the group addressing gate: a bare command may belong to any bot in the room, one addressed elsewhere never concerns yours), `cutEntities(text, entities, shouldCut)` (exact span surgery; the predicate sees each visible slice), `messageTextAndEntities(msg)` (text or caption with ITS entity set). Framework-agnostic by shape: gramio's `MessageEntity` class and the raw Bot API object both fit. |
520
+ | `@adriangalilea/utils/bot/announce` | The house announcement (news) template for bot mass messages, first-class: an `Announcement` = banner (`# πŸ“° <bot> News`), ordered sections (canonical `section.info/features/fixes` with πŸ’¬/✨/πŸ›, free-form allowed), italic closer, `Β· signature` (middle dot by design, never an em dash). `renderAnnouncement(a)` β†’ the Telegram-markdown subset; `renderAnnouncementBodies({en, es, …})` β†’ per-language bodies for a polyglot broadcast engine ("en" is the required fallback anchor). Pure, zero deps. |
521
+ | `@adriangalilea/utils/bot/updates` | The pending-update queue of a webhook-less bot token, treated as the destroy-on-read resource it is (`getUpdates` with an offset CONFIRMS β€” destroys β€” everything below it). `updateQueue({ token, fetch?, apiRoot? })` β†’ `peek({ limit? })` (webhook state + pending count + the queue head, verbatim; sends NO offset so nothing is ever confirmed β€” repeatable, zero risk), `count()`, `webhookInfo()`, and `drain({ onBatch, limit?, timeoutS?, quietPolls? })` β€” the one-chance-safe sweep: each raw batch is handed to `onBatch` (persist it somewhere durable) and AWAITED before the offset advances, because the next call is the confirmation; a crash re-delivers the unconfirmed tail on re-run, and the drain ends only after several consecutive silent long-polls, never on the first empty response. A 409 (webhook registered / competing poller) is a typed `SourcedError`, never a silent retry. Plain `fetch`, injectable for tests; no framework. |
520
522
  | `@adriangalilea/utils/bot/inline-feedback` | Behavioral tripwire for BotFather's inline feedback probability (gates `chosen_inline_result`; defaults 0%, silently resets on bot transfer and inline-mode toggling, invisible to `getMe`): served-but-never-chosen crosses a threshold β†’ throttled admin DM naming the exact BotFather switch. Storage injected as three ops; never throws into the answer path. |
521
523
  | `@adriangalilea/utils/bot/coalesce` | Joins client-split inbound messages back into one. When a user pastes >4096 chars, Telegram clients fragment it into separate `message` updates with no marker. Middleware detects the burst and emits one combined event. |
522
524
  | `@adriangalilea/utils/llm` | Multi-provider LLM caller on the Vercel AI SDK β€” the policy layer the SDK doesn't ship. `createLlm({ providers, health? })` β†’ `.stream(req)` (typed events: `delta` / `reasoning` / `reset` / `tool-call` / `end`) and `.complete(req)`. **Failover:** priority-ordered across OpenAI-compatible, Anthropic-compatible, and OpenRouter endpoints; multiple keys per provider (health tracked per key fingerprint); per-key circuit breaker with exponential cool-down persisted in any KV-shaped `HealthStore`; an attempt that dies after emitting yields `reset` (discard, next candidate regenerates); empty completions count as failures. **Accounting:** tokens summed across billed attempts; `costUsd` is the ACTUAL charge (OpenRouter usage accounting via `providerMetadata`, or a `cost` field on the raw usage frame) β€” never a price-table estimate. **Knobs:** per-model temperature, maxTokens caps, `disableThinking` mapped to each dialect. **Tools:** AI SDK `tool()` + `toolChoice` pass through (re-exported). Worker-safe; peers: `ai`, `@ai-sdk/openai-compatible`, `@ai-sdk/anthropic`, `@openrouter/ai-sdk-provider`. |
@@ -0,0 +1,59 @@
1
+ /**
2
+ * The house announcement ("news") template for bot mass messages, as a first-class structure:
3
+ * compose an {@link Announcement} once, render it per language, and hand the bodies to a
4
+ * polyglot broadcast engine. The render targets the common Telegram-markdown subset
5
+ * (`# heading` β†’ bold, `- ` bullets, `_…_` italics) most bot pipelines already convert to
6
+ * Telegram HTML β€” no parse_mode assumptions live here.
7
+ *
8
+ * Shape (each section optional, order preserved):
9
+ *
10
+ * # πŸ“° <banner>
11
+ *
12
+ * πŸ’¬ Info
13
+ * - …
14
+ *
15
+ * ✨ New Features
16
+ * - …
17
+ *
18
+ * πŸ› Fixes
19
+ * - …
20
+ *
21
+ * _<closer>_
22
+ *
23
+ * Β· <signature>
24
+ *
25
+ * The signature separator is a middle dot by design β€” never an em dash.
26
+ */
27
+ export interface AnnouncementSection {
28
+ /** Leading emoji for the section title, e.g. "πŸ’¬". */
29
+ emoji: string;
30
+ /** Section title, e.g. "Info". */
31
+ title: string;
32
+ /** Bulleted items, one line each. */
33
+ items: string[];
34
+ }
35
+ export interface Announcement {
36
+ /** Banner line, e.g. "theSummaryBot News". Rendered as `# πŸ“° <banner>`. */
37
+ banner: string;
38
+ sections: AnnouncementSection[];
39
+ /** Italic closing line, e.g. "Questions? Feedback? Feel free to reach out!". */
40
+ closer?: string;
41
+ /** Attribution, e.g. "@adriangalilea". Rendered as `Β· <signature>`. */
42
+ signature?: string;
43
+ }
44
+ /** The three canonical sections, so every announcement wears the same iconography. */
45
+ export declare const section: {
46
+ info: (items: string[], title?: string) => AnnouncementSection;
47
+ features: (items: string[], title?: string) => AnnouncementSection;
48
+ fixes: (items: string[], title?: string) => AnnouncementSection;
49
+ };
50
+ /** Render one language's announcement to the Telegram-markdown subset. */
51
+ export declare function renderAnnouncement(a: Announcement): string;
52
+ /**
53
+ * Per-language bodies for a polyglot broadcast: language code β†’ rendered body. "en" is the
54
+ * required fallback anchor β€” an engine resolves each recipient's language and falls back to it.
55
+ */
56
+ export type AnnouncementBodies = Record<string, string>;
57
+ /** Render a per-language map of announcements into broadcast bodies. Throws without "en". */
58
+ export declare function renderAnnouncementBodies(byLanguage: Record<string, Announcement>): AnnouncementBodies;
59
+ //# sourceMappingURL=announce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.d.ts","sourceRoot":"","sources":["../../src/bot/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,WAAW,mBAAmB;IACnC,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC5B,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,sFAAsF;AACtF,eAAO,MAAM,OAAO;kBACL,MAAM,EAAE,qBAAmB,mBAAmB;sBAC1C,MAAM,EAAE,qBAA2B,mBAAmB;mBACzD,MAAM,EAAE,qBAAoB,mBAAmB;CAC9D,CAAC;AAEF,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,YAAY,GAAG,MAAM,CAU1D;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAExD,6FAA6F;AAC7F,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,kBAAkB,CAKrG"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The house announcement ("news") template for bot mass messages, as a first-class structure:
3
+ * compose an {@link Announcement} once, render it per language, and hand the bodies to a
4
+ * polyglot broadcast engine. The render targets the common Telegram-markdown subset
5
+ * (`# heading` β†’ bold, `- ` bullets, `_…_` italics) most bot pipelines already convert to
6
+ * Telegram HTML β€” no parse_mode assumptions live here.
7
+ *
8
+ * Shape (each section optional, order preserved):
9
+ *
10
+ * # πŸ“° <banner>
11
+ *
12
+ * πŸ’¬ Info
13
+ * - …
14
+ *
15
+ * ✨ New Features
16
+ * - …
17
+ *
18
+ * πŸ› Fixes
19
+ * - …
20
+ *
21
+ * _<closer>_
22
+ *
23
+ * Β· <signature>
24
+ *
25
+ * The signature separator is a middle dot by design β€” never an em dash.
26
+ */
27
+ /** The three canonical sections, so every announcement wears the same iconography. */
28
+ export const section = {
29
+ info: (items, title = "Info") => ({ emoji: "πŸ’¬", title, items }),
30
+ features: (items, title = "New Features") => ({ emoji: "✨", title, items }),
31
+ fixes: (items, title = "Fixes") => ({ emoji: "πŸ›", title, items }),
32
+ };
33
+ /** Render one language's announcement to the Telegram-markdown subset. */
34
+ export function renderAnnouncement(a) {
35
+ const blocks = [`# πŸ“° ${a.banner}`];
36
+ for (const s of a.sections) {
37
+ if (s.items.length === 0)
38
+ continue;
39
+ blocks.push(`${s.emoji} **${s.title}**`);
40
+ blocks.push(s.items.map((i) => `- ${i}`).join("\n"));
41
+ }
42
+ if (a.closer)
43
+ blocks.push(`_${a.closer}_`);
44
+ if (a.signature)
45
+ blocks.push(`Β· ${a.signature}`);
46
+ return blocks.join("\n\n");
47
+ }
48
+ /** Render a per-language map of announcements into broadcast bodies. Throws without "en". */
49
+ export function renderAnnouncementBodies(byLanguage) {
50
+ if (!byLanguage.en)
51
+ throw new Error("announcement bodies need an 'en' fallback");
52
+ const bodies = {};
53
+ for (const [lang, a] of Object.entries(byLanguage))
54
+ bodies[lang] = renderAnnouncement(a);
55
+ return bodies;
56
+ }
57
+ //# sourceMappingURL=announce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"announce.js","sourceRoot":"","sources":["../../src/bot/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAqBH,sFAAsF;AACtF,MAAM,CAAC,MAAM,OAAO,GAAG;IACtB,IAAI,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,MAAM,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC/F,QAAQ,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,cAAc,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC1G,KAAK,EAAE,CAAC,KAAe,EAAE,KAAK,GAAG,OAAO,EAAuB,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;CACjG,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,kBAAkB,CAAC,CAAe;IACjD,MAAM,MAAM,GAAa,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,CAAC,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IAC3C,IAAI,CAAC,CAAC,SAAS;QAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;IACjD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAQD,6FAA6F;AAC7F,MAAM,UAAU,wBAAwB,CAAC,UAAwC;IAChF,IAAI,CAAC,UAAU,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IACjF,MAAM,MAAM,GAAuB,EAAE,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACzF,OAAO,MAAM,CAAC;AACf,CAAC"}
@@ -0,0 +1,53 @@
1
+ /** A raw Bot API update. Only `update_id` is load-bearing here; everything else passes through. */
2
+ export interface TelegramUpdate {
3
+ update_id: number;
4
+ [key: string]: unknown;
5
+ }
6
+ export interface QueuePeek {
7
+ /** The registered webhook URL, or null when the token polls (getUpdates works). */
8
+ webhookUrl: string | null;
9
+ /** Updates waiting server-side (`getWebhookInfo.pending_update_count`). */
10
+ pending: number;
11
+ /** The first updates in the queue, verbatim. Empty when a webhook is registered (polling
12
+ * would 409) β€” the count above still tells the story. */
13
+ head: TelegramUpdate[];
14
+ }
15
+ export interface DrainResult {
16
+ drained: number;
17
+ batches: number;
18
+ lastUpdateId?: number;
19
+ }
20
+ export interface UpdateQueueOptions {
21
+ token: string;
22
+ /** Transport override for tests. Defaults to global fetch. */
23
+ fetch?: typeof globalThis.fetch;
24
+ /** Bot API origin override (local Bot API servers). */
25
+ apiRoot?: string;
26
+ }
27
+ /** Peek/drain/count over one bot token's pending-update queue. See the module doc for the
28
+ * destroy-on-read contract each verb upholds. */
29
+ export declare function updateQueue(options: UpdateQueueOptions): {
30
+ /** Raw `getWebhookInfo`. */
31
+ webhookInfo(): Promise<Record<string, unknown>>;
32
+ /** Updates waiting server-side, without touching any of them. */
33
+ count(): Promise<number>;
34
+ /** Non-destructive look: webhook state, pending count, and the first `limit` updates,
35
+ * verbatim. Confirms NOTHING β€” repeat as often as you like. */
36
+ peek({ limit }?: {
37
+ limit?: number;
38
+ }): Promise<QueuePeek>;
39
+ /**
40
+ * Drain the queue, one-chance-safe. `onBatch` receives each raw batch and is AWAITED
41
+ * before the offset advances β€” persist there, because the next `getUpdates` call is the
42
+ * confirmation that destroys the batch server-side. An `onBatch` throw aborts the drain
43
+ * with nothing confirmed beyond what `onBatch` already accepted; re-running resumes at
44
+ * the unconfirmed tail. Ends after `quietPolls` consecutive empty long-polls.
45
+ */
46
+ drain({ onBatch, limit, timeoutS, quietPolls, }: {
47
+ onBatch: (updates: TelegramUpdate[]) => void | Promise<void>;
48
+ limit?: number;
49
+ timeoutS?: number;
50
+ quietPolls?: number;
51
+ }): Promise<DrainResult>;
52
+ };
53
+ //# sourceMappingURL=updates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updates.d.ts","sourceRoot":"","sources":["../../src/bot/updates.ts"],"names":[],"mappings":"AAqBA,mGAAmG;AACnG,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACzB,mFAAmF;IACnF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB;8DAC0D;IAC1D,IAAI,EAAE,cAAc,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;kDACkD;AAClD,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB;IA0BrD,4BAA4B;mBACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAI/C,iEAAiE;aAClD,OAAO,CAAC,MAAM,CAAC;IAK9B;oEACgE;qBACtC;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAQ,OAAO,CAAC,SAAS,CAAC;IAQtE;;;;;;OAMG;qDAMA;QACF,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,UAAU,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,WAAW,CAAC;EAsBzB"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The pending-update queue of a webhook-less bot token, handled with the respect a
3
+ * destroy-on-read resource deserves. Telegram's `getUpdates` CONFIRMS (discards) every update
4
+ * older than the offset it is called with β€” reading is destructive, and a queue drained
5
+ * carelessly is gone forever. Two safety properties are the whole point:
6
+ *
7
+ * PEEK is free. `getUpdates` WITHOUT an offset confirms nothing: webhook state, the pending
8
+ * count, and the head of the queue are all inspectable, repeatably, at zero risk.
9
+ *
10
+ * DRAIN is write-before-confirm. Each raw batch is handed to the caller's `onBatch` (persist
11
+ * it there β€” disk, db, anywhere durable) and AWAITED before the next `getUpdates` call
12
+ * advances the offset, because that next call IS the confirmation. A crash mid-drain loses
13
+ * nothing: re-run, and the unconfirmed tail re-delivers. The drain ends only after the queue
14
+ * stays silent across several consecutive long-polls, never on the first empty response.
15
+ *
16
+ * Framework-free: plain `fetch` against the Bot API, injectable for tests. A 409 means the
17
+ * token has a webhook registered or another poller is competing β€” surfaced as a typed error,
18
+ * never retried silently.
19
+ */
20
+ import { SourcedError } from "../offensive.js";
21
+ /** Peek/drain/count over one bot token's pending-update queue. See the module doc for the
22
+ * destroy-on-read contract each verb upholds. */
23
+ export function updateQueue(options) {
24
+ const doFetch = options.fetch ?? globalThis.fetch;
25
+ const root = options.apiRoot ?? "https://api.telegram.org";
26
+ const call = async (method, params) => {
27
+ const res = await doFetch(`${root}/bot${options.token}/${method}`, {
28
+ method: "POST",
29
+ headers: { "content-type": "application/json" },
30
+ body: JSON.stringify(params ?? {}),
31
+ });
32
+ const body = (await res.json());
33
+ if (!body.ok) {
34
+ throw new SourcedError({
35
+ source: "telegram",
36
+ operation: method,
37
+ message: body.error_code === 409
38
+ ? `${body.description ?? "409"} (a webhook is registered, or another poller is competing)`
39
+ : (body.description ?? "unknown error"),
40
+ status: body.error_code ?? res.status,
41
+ });
42
+ }
43
+ return body.result;
44
+ };
45
+ return {
46
+ /** Raw `getWebhookInfo`. */
47
+ webhookInfo() {
48
+ return call("getWebhookInfo");
49
+ },
50
+ /** Updates waiting server-side, without touching any of them. */
51
+ async count() {
52
+ const info = await call("getWebhookInfo");
53
+ return info.pending_update_count ?? 0;
54
+ },
55
+ /** Non-destructive look: webhook state, pending count, and the first `limit` updates,
56
+ * verbatim. Confirms NOTHING β€” repeat as often as you like. */
57
+ async peek({ limit = 1 } = {}) {
58
+ const info = await call("getWebhookInfo");
59
+ const webhookUrl = info.url ? info.url : null;
60
+ // With a webhook registered getUpdates would 409; the count already tells the story.
61
+ const head = webhookUrl ? [] : await call("getUpdates", { limit, timeout: 0 });
62
+ return { webhookUrl, pending: info.pending_update_count ?? 0, head };
63
+ },
64
+ /**
65
+ * Drain the queue, one-chance-safe. `onBatch` receives each raw batch and is AWAITED
66
+ * before the offset advances β€” persist there, because the next `getUpdates` call is the
67
+ * confirmation that destroys the batch server-side. An `onBatch` throw aborts the drain
68
+ * with nothing confirmed beyond what `onBatch` already accepted; re-running resumes at
69
+ * the unconfirmed tail. Ends after `quietPolls` consecutive empty long-polls.
70
+ */
71
+ async drain({ onBatch, limit = 100, timeoutS = 10, quietPolls = 3, }) {
72
+ let offset;
73
+ let drained = 0;
74
+ let batches = 0;
75
+ let lastUpdateId;
76
+ let quiet = 0;
77
+ while (quiet < quietPolls) {
78
+ const batch = await call("getUpdates", { offset, limit, timeout: timeoutS });
79
+ if (batch.length === 0) {
80
+ quiet += 1;
81
+ continue;
82
+ }
83
+ quiet = 0;
84
+ await onBatch(batch); // persist FIRST…
85
+ lastUpdateId = batch[batch.length - 1].update_id;
86
+ offset = lastUpdateId + 1; // …the NEXT call confirms
87
+ drained += batch.length;
88
+ batches += 1;
89
+ }
90
+ return { drained, batches, lastUpdateId };
91
+ },
92
+ };
93
+ }
94
+ //# sourceMappingURL=updates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updates.js","sourceRoot":"","sources":["../../src/bot/updates.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adriangalilea/utils",
3
- "version": "2.22.0",
3
+ "version": "2.24.0",
4
4
  "description": "TypeScript utilities - logger, currency, formatter, GramIO bot plugins, and more",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -156,6 +156,14 @@
156
156
  "types": "./dist/bot/urls.d.ts",
157
157
  "default": "./dist/bot/urls.js"
158
158
  },
159
+ "./bot/announce": {
160
+ "types": "./dist/bot/announce.d.ts",
161
+ "default": "./dist/bot/announce.js"
162
+ },
163
+ "./bot/updates": {
164
+ "types": "./dist/bot/updates.d.ts",
165
+ "default": "./dist/bot/updates.js"
166
+ },
159
167
  "./bot/reactions": {
160
168
  "types": "./dist/bot/reactions.d.ts",
161
169
  "default": "./dist/bot/reactions.js"
@@ -205,6 +213,8 @@
205
213
  "test:self-echo": "tsx tests/self-echo-test.ts",
206
214
  "test:url": "tsx tests/url-test.ts",
207
215
  "test:bot-urls": "tsx tests/bot-urls-test.ts",
216
+ "test:bot-updates": "tsx tests/bot-updates-test.ts",
217
+ "test:bot-announce": "tsx tests/bot-announce-test.ts",
208
218
  "test:reactions": "tsx tests/reactions-test.ts"
209
219
  },
210
220
  "keywords": [