@minesa-org/mini-interaction 0.2.17 → 0.2.19
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.
|
@@ -397,6 +397,8 @@ export declare class MiniInteraction {
|
|
|
397
397
|
/**
|
|
398
398
|
* Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
|
|
399
399
|
* This is used for interactions that have already been acknowledged (e.g., via deferReply).
|
|
400
|
+
*
|
|
401
|
+
* Includes retry logic to handle race conditions where the ACK hasn't reached Discord yet.
|
|
400
402
|
*/
|
|
401
403
|
private sendFollowUp;
|
|
402
404
|
}
|
|
@@ -1366,14 +1366,18 @@ export class MiniInteraction {
|
|
|
1366
1366
|
/**
|
|
1367
1367
|
* Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
|
|
1368
1368
|
* This is used for interactions that have already been acknowledged (e.g., via deferReply).
|
|
1369
|
+
*
|
|
1370
|
+
* Includes retry logic to handle race conditions where the ACK hasn't reached Discord yet.
|
|
1369
1371
|
*/
|
|
1370
|
-
async sendFollowUp(token, response, messageId = "@original") {
|
|
1372
|
+
async sendFollowUp(token, response, messageId = "@original", retryCount = 0) {
|
|
1373
|
+
const MAX_RETRIES = 3;
|
|
1374
|
+
const BASE_DELAY_MS = 250; // Start with 250ms delay
|
|
1371
1375
|
const isEdit = messageId !== "";
|
|
1372
1376
|
const url = isEdit
|
|
1373
1377
|
? `${DISCORD_BASE_URL}/webhooks/${this.applicationId}/${token}/messages/${messageId}`
|
|
1374
1378
|
: `${DISCORD_BASE_URL}/webhooks/${this.applicationId}/${token}`;
|
|
1375
1379
|
if (this.timeoutConfig.enableResponseDebugLogging) {
|
|
1376
|
-
console.log(`[MiniInteraction] sendFollowUp: id=${messageId || 'new'}, edit=${isEdit}, url=${url}`);
|
|
1380
|
+
console.log(`[MiniInteraction] sendFollowUp: id=${messageId || 'new'}, edit=${isEdit}, retry=${retryCount}, url=${url}`);
|
|
1377
1381
|
}
|
|
1378
1382
|
// Only send follow-up if there is data to send
|
|
1379
1383
|
if (!('data' in response) || !response.data) {
|
|
@@ -1395,6 +1399,13 @@ export class MiniInteraction {
|
|
|
1395
1399
|
}
|
|
1396
1400
|
if (!fetchResponse.ok) {
|
|
1397
1401
|
const errorBody = await fetchResponse.text();
|
|
1402
|
+
// Check for "Unknown Webhook" error (10015) - this means ACK hasn't reached Discord yet
|
|
1403
|
+
if (fetchResponse.status === 404 && errorBody.includes("10015") && retryCount < MAX_RETRIES) {
|
|
1404
|
+
const delayMs = BASE_DELAY_MS * Math.pow(2, retryCount); // Exponential backoff: 250, 500, 1000ms
|
|
1405
|
+
console.warn(`[MiniInteraction] Webhook not ready yet, retrying in ${delayMs}ms (attempt ${retryCount + 1}/${MAX_RETRIES})`);
|
|
1406
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
1407
|
+
return this.sendFollowUp(token, response, messageId, retryCount + 1);
|
|
1408
|
+
}
|
|
1398
1409
|
console.error(`[MiniInteraction] Failed to send follow-up response (id=${messageId || 'new'}): [${fetchResponse.status}] ${errorBody}`);
|
|
1399
1410
|
if (fetchResponse.status === 404) {
|
|
1400
1411
|
console.error("[MiniInteraction] Hint: Interaction token might have expired or the message was deleted.");
|
|
@@ -1577,6 +1588,8 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
|
|
|
1577
1588
|
return async (...args) => {
|
|
1578
1589
|
const startTime = Date.now();
|
|
1579
1590
|
let timeoutId;
|
|
1591
|
+
let ackResult = null;
|
|
1592
|
+
let ackReceived = false;
|
|
1580
1593
|
const timeoutPromise = new Promise((_, reject) => {
|
|
1581
1594
|
timeoutId = setTimeout(() => {
|
|
1582
1595
|
const elapsed = Date.now() - startTime;
|
|
@@ -1584,47 +1597,56 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
|
|
|
1584
1597
|
reject(new Error(`Handler timeout: ${handlerName} exceeded ${timeoutMs}ms limit`));
|
|
1585
1598
|
}, timeoutMs);
|
|
1586
1599
|
});
|
|
1587
|
-
//
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1600
|
+
// If we have an ackPromise, listen for it but DON'T return early
|
|
1601
|
+
// Instead, capture the ACK result and continue waiting for handler
|
|
1602
|
+
if (ackPromise) {
|
|
1603
|
+
ackPromise.then((result) => {
|
|
1604
|
+
ackResult = result;
|
|
1605
|
+
ackReceived = true;
|
|
1606
|
+
// Clear the timeout once we have an ACK - handler can now take longer
|
|
1607
|
+
if (timeoutId) {
|
|
1608
|
+
clearTimeout(timeoutId);
|
|
1609
|
+
timeoutId = undefined;
|
|
1610
|
+
}
|
|
1611
|
+
}).catch(() => {
|
|
1612
|
+
// ACK promise rejection is fine, we'll use handler result
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1599
1615
|
try {
|
|
1600
|
-
|
|
1601
|
-
|
|
1616
|
+
// ALWAYS wait for handler to complete
|
|
1617
|
+
const handlerResult = await Promise.race([
|
|
1618
|
+
handler(...args),
|
|
1602
1619
|
timeoutPromise,
|
|
1603
|
-
];
|
|
1604
|
-
if (ackPromise) {
|
|
1605
|
-
promises.push(ackPromise);
|
|
1606
|
-
}
|
|
1607
|
-
const result = await Promise.race(promises);
|
|
1620
|
+
]);
|
|
1608
1621
|
if (timeoutId) {
|
|
1609
1622
|
clearTimeout(timeoutId);
|
|
1610
1623
|
}
|
|
1611
1624
|
const elapsed = Date.now() - startTime;
|
|
1612
|
-
if (enableWarnings && elapsed > timeoutMs * 0.8) {
|
|
1625
|
+
if (enableWarnings && elapsed > timeoutMs * 0.8 && !ackReceived) {
|
|
1613
1626
|
console.warn(`[MiniInteraction] ${handlerName} completed in ${elapsed}ms (${Math.round((elapsed / timeoutMs) * 100)}% of timeout limit)`);
|
|
1614
1627
|
}
|
|
1615
|
-
return
|
|
1628
|
+
// If we got an ACK, return that for the HTTP response
|
|
1629
|
+
// The handler has completed at this point so all background work is done
|
|
1630
|
+
if (ackReceived && ackResult !== null) {
|
|
1631
|
+
return ackResult;
|
|
1632
|
+
}
|
|
1633
|
+
return handlerResult;
|
|
1616
1634
|
}
|
|
1617
1635
|
catch (error) {
|
|
1618
1636
|
if (timeoutId) {
|
|
1619
1637
|
clearTimeout(timeoutId);
|
|
1620
1638
|
}
|
|
1621
|
-
//
|
|
1622
|
-
if
|
|
1623
|
-
|
|
1639
|
+
// If handler timed out but we have an ACK, return the ACK
|
|
1640
|
+
// This allows deferReply to work even if later operations are slow
|
|
1641
|
+
if (error instanceof Error && error.message.includes("Handler timeout")) {
|
|
1642
|
+
if (ackReceived && ackResult !== null) {
|
|
1643
|
+
console.warn(`[MiniInteraction] ${handlerName} timed out but ACK was already captured. ` +
|
|
1644
|
+
`Background work may not complete in serverless environments.`);
|
|
1645
|
+
return ackResult;
|
|
1646
|
+
}
|
|
1624
1647
|
throw error;
|
|
1625
1648
|
}
|
|
1626
|
-
|
|
1627
|
-
// But since we catch all in handlerPromise, this is mostly for the timeout/race itself.
|
|
1649
|
+
console.error(`[MiniInteraction] ${handlerName} failed:`, error);
|
|
1628
1650
|
throw error;
|
|
1629
1651
|
}
|
|
1630
1652
|
};
|
package/package.json
CHANGED