@stonyx/discord 0.1.1-alpha.16 → 0.1.1-alpha.18
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 +13 -4
- package/dist/bot.js +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,15 @@ All methods are on the `DiscordBot` instance (accessible via `this._bot` in comm
|
|
|
180
180
|
|
|
181
181
|
Send a text message to a channel. Optionally attach an image file.
|
|
182
182
|
|
|
183
|
+
Messages are delivered reliably across connection state transitions:
|
|
184
|
+
- If the bot is **ready**, the message is sent immediately.
|
|
185
|
+
- If the bot is **connecting or disconnected**, the message is queued and auto-delivered when the connection resumes.
|
|
186
|
+
- **Transient failures** (5xx, network errors) are retried up to 3 times with exponential backoff.
|
|
187
|
+
- Queued messages expire after **5 minutes** (TTL) — stale messages are rejected rather than sent late.
|
|
188
|
+
- The queue holds up to **1000** messages; excess messages are rejected with a "queue full" error.
|
|
189
|
+
|
|
190
|
+
The returned promise resolves when the message is actually delivered or rejects on permanent failure.
|
|
191
|
+
|
|
183
192
|
```javascript
|
|
184
193
|
await bot.sendMessage('Hello world', '123456789');
|
|
185
194
|
await bot.sendMessage('Check this out', '123456789', './screenshot.png');
|
|
@@ -289,8 +298,8 @@ Configuration is read from `stonyx/config` under `discord`:
|
|
|
289
298
|
| `commands` | `Object` — registered command instances keyed by name |
|
|
290
299
|
| `eventHandlers` | `Array` — registered event handler instances |
|
|
291
300
|
| `client` | Discord.js `Client` instance (after init) |
|
|
292
|
-
| `ready` | `Promise` — resolves when the bot is connected and ready |
|
|
293
|
-
| `sendMessage(content, channelId, imagePath?)` | Send a message to a channel |
|
|
301
|
+
| `ready` | `Promise` — resolves when the bot is connected and ready; re-pends on gateway disconnects |
|
|
302
|
+
| `sendMessage(content, channelId, imagePath?)` | Send a message to a channel (queues during disconnections, retries transient failures) |
|
|
294
303
|
| `sendFile(file, messageObject)` | Replace a message with a file attachment |
|
|
295
304
|
| `reply(interaction, content)` | Reply with auto-chunking |
|
|
296
305
|
| `updateStatus(name, type?)` | Set bot presence |
|
|
@@ -299,8 +308,8 @@ Configuration is read from `stonyx/config` under `discord`:
|
|
|
299
308
|
| `getGuild(guildId?)` | Fetch a guild |
|
|
300
309
|
| `clearChannelMessages(channelId)` | Delete messages in a channel; returns `{ deleted, failed, errors }` and never throws on partial failure |
|
|
301
310
|
| `giveRole(memberId, roleId)` | Add a role to a member |
|
|
302
|
-
| `close()` | Destroy the Discord client
|
|
303
|
-
| `reset()` | Close + clear all commands and
|
|
311
|
+
| `close()` | Destroy the Discord client, reject pending queue, clear singleton |
|
|
312
|
+
| `reset()` | Close + clear all commands, handlers, and connection state |
|
|
304
313
|
|
|
305
314
|
### Command
|
|
306
315
|
|
package/dist/bot.js
CHANGED
|
@@ -5,6 +5,7 @@ import { forEachFileImport } from '@stonyx/utils/file';
|
|
|
5
5
|
import { deriveIntents, derivePartials } from './intents.js';
|
|
6
6
|
import { chunkMessage } from './message.js';
|
|
7
7
|
const MAX_RETRIES = 3;
|
|
8
|
+
const MAX_QUEUE_SIZE = 1000;
|
|
8
9
|
const QUEUE_TTL = 300_000; // 5 minutes
|
|
9
10
|
const READY_TIMEOUT = 30_000; // 30 seconds
|
|
10
11
|
const RETRYABLE_NETWORK_CODES = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED']);
|
|
@@ -85,6 +86,8 @@ export default class DiscordBot {
|
|
|
85
86
|
log.error('Discord session invalidated — rejecting all queued messages');
|
|
86
87
|
const entries = this._queue.splice(0);
|
|
87
88
|
for (const entry of entries) {
|
|
89
|
+
if (entry.timerId !== null)
|
|
90
|
+
clearTimeout(entry.timerId);
|
|
88
91
|
entry.reject(new Error('Discord session invalidated'));
|
|
89
92
|
}
|
|
90
93
|
});
|
|
@@ -177,6 +180,9 @@ export default class DiscordBot {
|
|
|
177
180
|
return this._enqueue(content, channelId, imagePath);
|
|
178
181
|
}
|
|
179
182
|
_enqueue(content, channelId, imagePath) {
|
|
183
|
+
if (this._queue.length >= MAX_QUEUE_SIZE) {
|
|
184
|
+
return Promise.reject(new Error('Message queue full'));
|
|
185
|
+
}
|
|
180
186
|
return new Promise((resolve, reject) => {
|
|
181
187
|
this._queue.push({
|
|
182
188
|
content,
|
|
@@ -186,10 +192,14 @@ export default class DiscordBot {
|
|
|
186
192
|
reject,
|
|
187
193
|
retries: 0,
|
|
188
194
|
enqueuedAt: Date.now(),
|
|
195
|
+
timerId: null,
|
|
189
196
|
});
|
|
190
197
|
});
|
|
191
198
|
}
|
|
192
199
|
_enqueueForRetry(content, channelId, imagePath, retries) {
|
|
200
|
+
if (this._queue.length >= MAX_QUEUE_SIZE) {
|
|
201
|
+
return Promise.reject(new Error('Message queue full'));
|
|
202
|
+
}
|
|
193
203
|
return new Promise((resolve, reject) => {
|
|
194
204
|
const entry = {
|
|
195
205
|
content,
|
|
@@ -199,6 +209,7 @@ export default class DiscordBot {
|
|
|
199
209
|
reject,
|
|
200
210
|
retries,
|
|
201
211
|
enqueuedAt: Date.now(),
|
|
212
|
+
timerId: null,
|
|
202
213
|
};
|
|
203
214
|
this._queue.push(entry);
|
|
204
215
|
this._scheduleRetry(entry);
|
|
@@ -206,7 +217,8 @@ export default class DiscordBot {
|
|
|
206
217
|
}
|
|
207
218
|
_scheduleRetry(entry) {
|
|
208
219
|
const backoff = Math.min(1000 * Math.pow(2, entry.retries) + Math.floor(Math.random() * 500), 30000);
|
|
209
|
-
setTimeout(() => {
|
|
220
|
+
entry.timerId = setTimeout(() => {
|
|
221
|
+
entry.timerId = null;
|
|
210
222
|
// Only process if the entry is still in the queue (wasn't rejected by close/invalidated)
|
|
211
223
|
const idx = this._queue.indexOf(entry);
|
|
212
224
|
if (idx === -1)
|
|
@@ -373,9 +385,11 @@ export default class DiscordBot {
|
|
|
373
385
|
await member.roles.add(role);
|
|
374
386
|
}
|
|
375
387
|
close() {
|
|
376
|
-
//
|
|
388
|
+
// Cancel all pending retry timers and reject all queue entries
|
|
377
389
|
const entries = this._queue.splice(0);
|
|
378
390
|
for (const entry of entries) {
|
|
391
|
+
if (entry.timerId !== null)
|
|
392
|
+
clearTimeout(entry.timerId);
|
|
379
393
|
entry.reject(new Error('Discord bot closed'));
|
|
380
394
|
}
|
|
381
395
|
if (this.client) {
|