@pixelbyte-software/pixcode 1.53.3 → 1.53.4

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.
@@ -53,15 +53,18 @@ async function callApi(token, method, params, { signal } = {}) {
53
53
  }
54
54
 
55
55
  export class TelegramHttpBot extends EventEmitter {
56
- constructor(token, { polling = true, pollTimeoutSec = 30 } = {}) {
56
+ constructor(token, { polling = true, pollTimeoutSec = 30, pollLimit = 25, dropPendingUpdates = true } = {}) {
57
57
  super();
58
58
  if (!token) throw new Error('TelegramHttpBot: token is required');
59
59
  this._token = token;
60
60
  this._pollTimeoutSec = pollTimeoutSec;
61
+ this._pollLimit = pollLimit;
62
+ this._dropPendingUpdates = dropPendingUpdates;
61
63
  this._offset = 0;
62
64
  this._polling = false;
63
65
  this._abortController = null;
64
- if (polling) this._startPolling();
66
+ this._pollingLoop = null;
67
+ if (polling) this.startPolling();
65
68
  }
66
69
 
67
70
  // ---------- Public API (mirrors node-telegram-bot-api surface) ----------
@@ -96,56 +99,101 @@ export class TelegramHttpBot extends EventEmitter {
96
99
  this._polling = false;
97
100
  try { this._abortController?.abort(); } catch { /* ignore */ }
98
101
  this._abortController = null;
102
+ if (this._pollingLoop) {
103
+ await this._pollingLoop.catch(() => {});
104
+ this._pollingLoop = null;
105
+ }
99
106
  }
100
107
 
101
108
  // ---------- Polling loop ----------
102
109
 
103
- async _startPolling() {
110
+ async startPolling({ dropPendingUpdates = this._dropPendingUpdates } = {}) {
111
+ if (this._polling) return;
104
112
  this._polling = true;
105
- // Kick off a non-awaited loop. Each iteration long-polls getUpdates for
106
- // up to pollTimeoutSec, then loops immediately. We deliberately serialize
107
- // (no concurrent long-polls) because Telegram rejects that with 409.
108
- (async () => {
109
- while (this._polling) {
110
- this._abortController = new AbortController();
111
- try {
112
- const updates = await callApi(
113
- this._token,
114
- 'getUpdates',
115
- {
116
- offset: this._offset,
117
- timeout: this._pollTimeoutSec,
118
- allowed_updates: ['message', 'callback_query'],
119
- },
120
- { signal: this._abortController.signal },
121
- );
122
- for (const update of updates) {
123
- if (typeof update.update_id === 'number') {
124
- this._offset = Math.max(this._offset, update.update_id + 1);
125
- }
126
- if (update.message) {
127
- try { this.emit('message', update.message); } catch (err) {
128
- // Don't let a listener exception break the poll loop.
129
- this.emit('polling_error', err);
130
- }
131
- }
132
- if (update.callback_query) {
133
- try { this.emit('callback_query', update.callback_query); } catch (err) {
134
- this.emit('polling_error', err);
135
- }
136
- }
113
+ this._pollingLoop = this._runPollingLoop({ dropPendingUpdates });
114
+ await Promise.resolve();
115
+ }
116
+
117
+ async _dropPendingUpdatesBeforePolling() {
118
+ try {
119
+ await callApi(this._token, 'deleteWebhook', { drop_pending_updates: true });
120
+ return;
121
+ } catch (err) {
122
+ this.emit('polling_error', err);
123
+ }
124
+
125
+ // Fallback for environments where deleteWebhook is rejected: a negative
126
+ // offset asks Telegram to forget older queued updates.
127
+ try {
128
+ const updates = await callApi(this._token, 'getUpdates', {
129
+ offset: -1,
130
+ limit: 1,
131
+ timeout: 0,
132
+ allowed_updates: ['message', 'callback_query'],
133
+ });
134
+ const lastUpdate = Array.isArray(updates) ? updates.at(-1) : null;
135
+ if (typeof lastUpdate?.update_id === 'number') {
136
+ this._offset = lastUpdate.update_id + 1;
137
+ }
138
+ } catch (err) {
139
+ this.emit('polling_error', err);
140
+ }
141
+ }
142
+
143
+ async _emitSerial(eventName, payload) {
144
+ const listeners = this.listeners(eventName);
145
+ for (const listener of listeners) {
146
+ try {
147
+ await listener(payload);
148
+ } catch (err) {
149
+ this.emit('polling_error', err);
150
+ }
151
+ }
152
+ }
153
+
154
+ async _runPollingLoop({ dropPendingUpdates }) {
155
+ if (dropPendingUpdates) {
156
+ await this._dropPendingUpdatesBeforePolling();
157
+ }
158
+
159
+ // Each iteration long-polls getUpdates for up to pollTimeoutSec, then
160
+ // loops immediately. We deliberately serialize update handling because a
161
+ // stale Telegram backlog can otherwise fan out into many expensive scans.
162
+ while (this._polling) {
163
+ this._abortController = new AbortController();
164
+ try {
165
+ const updates = await callApi(
166
+ this._token,
167
+ 'getUpdates',
168
+ {
169
+ offset: this._offset,
170
+ limit: this._pollLimit,
171
+ timeout: this._pollTimeoutSec,
172
+ allowed_updates: ['message', 'callback_query'],
173
+ },
174
+ { signal: this._abortController.signal },
175
+ );
176
+ for (const update of updates) {
177
+ if (typeof update.update_id === 'number') {
178
+ this._offset = Math.max(this._offset, update.update_id + 1);
179
+ }
180
+ if (update.message) {
181
+ await this._emitSerial('message', update.message);
182
+ }
183
+ if (update.callback_query) {
184
+ await this._emitSerial('callback_query', update.callback_query);
137
185
  }
138
- } catch (err) {
139
- // AbortError is the expected path when stopPolling() is called.
140
- if (err?.name === 'AbortError' || !this._polling) break;
141
- this.emit('polling_error', err);
142
- // Back off before retrying — rapid retries on 401/409 would
143
- // otherwise spin at 100% CPU. Upstream consumer's polling_error
144
- // handler may also call stopBot() on 401/409 which flips _polling
145
- // off and breaks the loop on the next tick.
146
- await new Promise((r) => setTimeout(r, 2000));
147
186
  }
187
+ } catch (err) {
188
+ // AbortError is the expected path when stopPolling() is called.
189
+ if (err?.name === 'AbortError' || !this._polling) break;
190
+ this.emit('polling_error', err);
191
+ // Back off before retrying — rapid retries on 401/409 would
192
+ // otherwise spin at 100% CPU. Upstream consumer's polling_error
193
+ // handler may also call stopBot() on 401/409 which flips _polling
194
+ // off and breaks the loop on the next tick.
195
+ await new Promise((r) => setTimeout(r, 2000));
148
196
  }
149
- })();
197
+ }
150
198
  }
151
199
  }