@minesa-org/mini-interaction 0.1.12 → 0.2.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/dist/builders/ActionRowBuilder.d.ts +13 -15
- package/dist/builders/ActionRowBuilder.js +14 -16
- package/dist/clients/MiniInteraction.d.ts +102 -34
- package/dist/clients/MiniInteraction.js +293 -70
- package/dist/index.d.ts +9 -9
- package/dist/index.js +5 -1
- package/dist/types/Commands.d.ts +7 -6
- package/dist/types/ComponentTypes.d.ts +4 -4
- package/dist/types/InteractionFlags.d.ts +6 -7
- package/dist/types/InteractionFlags.js +10 -12
- package/dist/utils/CommandInteractionOptions.d.ts +13 -1
- package/dist/utils/CommandInteractionOptions.js +76 -4
- package/dist/utils/ContextMenuInteraction.d.ts +3 -0
- package/dist/utils/ContextMenuInteraction.js +3 -0
- package/dist/utils/MessageComponentInteraction.d.ts +9 -0
- package/dist/utils/MessageComponentInteraction.js +9 -0
- package/dist/utils/ModalSubmitInteraction.d.ts +1 -0
- package/dist/utils/ModalSubmitInteraction.js +1 -0
- package/dist/utils/interactionMessageHelpers.d.ts +11 -4
- package/dist/utils/interactionMessageHelpers.js +13 -30
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { ApplicationCommandType, InteractionResponseType, InteractionType, } from "discord-api-types/v10";
|
|
6
6
|
import { verifyKey } from "discord-interactions";
|
|
7
|
+
import { resolveJSONEncodable } from "../builders/shared.js";
|
|
7
8
|
import { DISCORD_BASE_URL } from "../utils/constants.js";
|
|
8
9
|
import { createCommandInteraction } from "../utils/CommandInteractionOptions.js";
|
|
9
10
|
import { createMessageComponentInteraction, } from "../utils/MessageComponentInteraction.js";
|
|
@@ -30,10 +31,12 @@ export class MiniInteraction {
|
|
|
30
31
|
commandsDirectory;
|
|
31
32
|
componentsDirectory;
|
|
32
33
|
utilsDirectory;
|
|
34
|
+
timeoutConfig;
|
|
33
35
|
commands = new Map();
|
|
34
36
|
componentHandlers = new Map();
|
|
35
37
|
modalHandlers = new Map();
|
|
36
38
|
htmlTemplateCache = new Map();
|
|
39
|
+
interactionStates = new Map();
|
|
37
40
|
commandsLoaded = false;
|
|
38
41
|
loadCommandsPromise = null;
|
|
39
42
|
componentsLoaded = false;
|
|
@@ -43,7 +46,7 @@ export class MiniInteraction {
|
|
|
43
46
|
/**
|
|
44
47
|
* Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
|
|
45
48
|
*/
|
|
46
|
-
constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, utilsDirectory, fetchImplementation, verifyKeyImplementation, }) {
|
|
49
|
+
constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, utilsDirectory, fetchImplementation, verifyKeyImplementation, timeoutConfig, }) {
|
|
47
50
|
if (!applicationId) {
|
|
48
51
|
throw new Error("[MiniInteraction] applicationId is required");
|
|
49
52
|
}
|
|
@@ -70,13 +73,135 @@ export class MiniInteraction {
|
|
|
70
73
|
utilsDirectory === false
|
|
71
74
|
? null
|
|
72
75
|
: this.resolveUtilsDirectory(utilsDirectory);
|
|
76
|
+
this.timeoutConfig = {
|
|
77
|
+
initialResponseTimeout: 2800, // Leave 200ms buffer before Discord's 3s limit
|
|
78
|
+
enableTimeoutWarnings: true,
|
|
79
|
+
autoDeferSlowOperations: true,
|
|
80
|
+
enableResponseDebugLogging: false,
|
|
81
|
+
responseAcknowledgmentTimeout: 500, // 500ms to wait for acknowledgment
|
|
82
|
+
responseAcknowledgmentRetries: 2,
|
|
83
|
+
responseAcknowledgmentRetryDelay: 100,
|
|
84
|
+
...timeoutConfig,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Tracks the state of an interaction to prevent race conditions and double responses.
|
|
89
|
+
*/
|
|
90
|
+
trackInteractionState(interactionId, token, state) {
|
|
91
|
+
const existing = this.interactionStates.get(interactionId);
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
this.interactionStates.set(interactionId, {
|
|
94
|
+
state,
|
|
95
|
+
timestamp: now,
|
|
96
|
+
token,
|
|
97
|
+
responseCount: existing ? existing.responseCount + 1 : 1,
|
|
98
|
+
});
|
|
99
|
+
if (this.timeoutConfig.enableResponseDebugLogging) {
|
|
100
|
+
console.log(`[MiniInteraction:DEBUG] Interaction ${interactionId} state: ${state} (${existing ? existing.responseCount + 1 : 1} responses)`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks if an interaction can still respond (not expired and not already responded).
|
|
105
|
+
*/
|
|
106
|
+
canRespond(interactionId) {
|
|
107
|
+
const state = this.getInteractionState(interactionId);
|
|
108
|
+
if (!state)
|
|
109
|
+
return true; // New interaction
|
|
110
|
+
// Check if expired (15 minutes)
|
|
111
|
+
if (Date.now() - state.timestamp > 900000) {
|
|
112
|
+
this.trackInteractionState(interactionId, state.token, 'expired');
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Check if already responded
|
|
116
|
+
if (state.state === 'responded') {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Logs response timing and acknowledgment for debugging.
|
|
123
|
+
*/
|
|
124
|
+
logResponseTiming(interactionId, operation, startTime, success) {
|
|
125
|
+
if (!this.timeoutConfig.enableResponseDebugLogging)
|
|
126
|
+
return;
|
|
127
|
+
const elapsed = Date.now() - startTime;
|
|
128
|
+
const status = success ? 'SUCCESS' : 'FAILED';
|
|
129
|
+
console.log(`[MiniInteraction:DEBUG] ${operation} for interaction ${interactionId}: ${status} (${elapsed}ms)`);
|
|
130
|
+
if (success && elapsed > 2000) {
|
|
131
|
+
console.warn(`[MiniInteraction:WARN] ${operation} took ${elapsed}ms - consider using deferReply() for slow operations`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Simulates waiting for Discord acknowledgment to help debug timing issues.
|
|
136
|
+
* Note: This is a best-effort simulation since actual acknowledgment happens at the HTTP level.
|
|
137
|
+
*/
|
|
138
|
+
async simulateAcknowledgmentWait(interactionId, operation) {
|
|
139
|
+
if (!this.timeoutConfig.enableResponseDebugLogging)
|
|
140
|
+
return;
|
|
141
|
+
const timeout = this.timeoutConfig.responseAcknowledgmentTimeout || 500;
|
|
142
|
+
const retries = this.timeoutConfig.responseAcknowledgmentRetries || 2;
|
|
143
|
+
const retryDelay = this.timeoutConfig.responseAcknowledgmentRetryDelay || 100;
|
|
144
|
+
console.log(`[MiniInteraction:DEBUG] Waiting for acknowledgment of ${operation} for interaction ${interactionId}...`);
|
|
145
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
146
|
+
await new Promise(resolve => setTimeout(resolve, attempt === 0 ? timeout : retryDelay));
|
|
147
|
+
// In a real implementation, this would verify Discord actually received the response
|
|
148
|
+
// For now, we just simulate the wait time
|
|
149
|
+
const state = this.getInteractionState(interactionId);
|
|
150
|
+
if (state?.state === 'responded' || state?.state === 'deferred') {
|
|
151
|
+
console.log(`[MiniInteraction:DEBUG] Acknowledgment confirmed for ${operation} after ${attempt + 1} attempts`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
console.warn(`[MiniInteraction:WARN] Acknowledgment timeout for ${operation} on interaction ${interactionId}`);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Enables or disables debug logging for interaction responses and timing.
|
|
159
|
+
* Useful for troubleshooting "didn't respond in time" errors.
|
|
160
|
+
*
|
|
161
|
+
* @param enabled - Whether to enable debug logging
|
|
162
|
+
*/
|
|
163
|
+
setResponseDebugLogging(enabled) {
|
|
164
|
+
this.timeoutConfig.enableResponseDebugLogging = enabled;
|
|
165
|
+
console.log(`[MiniInteraction] Response debug logging ${enabled ? 'enabled' : 'disabled'}`);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Gets the current state of an interaction.
|
|
169
|
+
*/
|
|
170
|
+
getInteractionState(interactionId) {
|
|
171
|
+
return this.interactionStates.get(interactionId);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Gets the current state of an interaction for debugging purposes.
|
|
175
|
+
*
|
|
176
|
+
* @param interactionId - The interaction ID to check
|
|
177
|
+
* @returns The current interaction state or null if not found
|
|
178
|
+
*/
|
|
179
|
+
getInteractionStateInfo(interactionId) {
|
|
180
|
+
const state = this.interactionStates.get(interactionId);
|
|
181
|
+
return state ? { ...state } : null; // Return a copy to prevent external modification
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Clears expired interaction states to prevent memory leaks.
|
|
185
|
+
* Call this periodically to clean up old interaction data.
|
|
186
|
+
*/
|
|
187
|
+
cleanupExpiredInteractions() {
|
|
188
|
+
const now = Date.now();
|
|
189
|
+
let cleaned = 0;
|
|
190
|
+
for (const [id, state] of this.interactionStates.entries()) {
|
|
191
|
+
// Remove interactions older than 15 minutes
|
|
192
|
+
if (now - state.timestamp > 900000) {
|
|
193
|
+
this.interactionStates.delete(id);
|
|
194
|
+
cleaned++;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (cleaned > 0) {
|
|
198
|
+
console.log(`[MiniInteraction] Cleaned up ${cleaned} expired interactions`);
|
|
199
|
+
}
|
|
200
|
+
return cleaned;
|
|
73
201
|
}
|
|
74
202
|
normalizeCommandData(data) {
|
|
75
203
|
if (typeof data === "object" && data !== null) {
|
|
76
|
-
|
|
77
|
-
if (typeof toJSON === "function") {
|
|
78
|
-
return toJSON.call(data);
|
|
79
|
-
}
|
|
204
|
+
return resolveJSONEncodable(data);
|
|
80
205
|
}
|
|
81
206
|
return data;
|
|
82
207
|
}
|
|
@@ -94,12 +219,14 @@ export class MiniInteraction {
|
|
|
94
219
|
data: normalizedData,
|
|
95
220
|
};
|
|
96
221
|
this.commands.set(commandName, normalizedCommand);
|
|
97
|
-
if (normalizedCommand.components &&
|
|
222
|
+
if (normalizedCommand.components &&
|
|
223
|
+
Array.isArray(normalizedCommand.components)) {
|
|
98
224
|
for (const component of normalizedCommand.components) {
|
|
99
225
|
this.useComponent(component);
|
|
100
226
|
}
|
|
101
227
|
}
|
|
102
|
-
if (normalizedCommand.modals &&
|
|
228
|
+
if (normalizedCommand.modals &&
|
|
229
|
+
Array.isArray(normalizedCommand.modals)) {
|
|
103
230
|
for (const modal of normalizedCommand.modals) {
|
|
104
231
|
this.useModal(modal);
|
|
105
232
|
}
|
|
@@ -354,6 +481,7 @@ export class MiniInteraction {
|
|
|
354
481
|
* @param request - The request payload containing headers and body data.
|
|
355
482
|
*/
|
|
356
483
|
async handleRequest(request) {
|
|
484
|
+
const requestStartTime = Date.now();
|
|
357
485
|
const { body, signature, timestamp } = request;
|
|
358
486
|
if (!signature || !timestamp) {
|
|
359
487
|
return {
|
|
@@ -390,14 +518,34 @@ export class MiniInteraction {
|
|
|
390
518
|
};
|
|
391
519
|
}
|
|
392
520
|
if (interaction.type === InteractionType.ApplicationCommand) {
|
|
521
|
+
// Track interaction start
|
|
522
|
+
this.trackInteractionState(interaction.id, interaction.token, 'pending');
|
|
393
523
|
return this.handleApplicationCommand(interaction);
|
|
394
524
|
}
|
|
395
525
|
if (interaction.type === InteractionType.MessageComponent) {
|
|
526
|
+
// Track interaction start
|
|
527
|
+
this.trackInteractionState(interaction.id, interaction.token, 'pending');
|
|
396
528
|
return this.handleMessageComponent(interaction);
|
|
397
529
|
}
|
|
398
530
|
if (interaction.type === InteractionType.ModalSubmit) {
|
|
531
|
+
// Track interaction start
|
|
532
|
+
this.trackInteractionState(interaction.id, interaction.token, 'pending');
|
|
399
533
|
return this.handleModalSubmit(interaction);
|
|
400
534
|
}
|
|
535
|
+
// Check total processing time and log potential timeout issues
|
|
536
|
+
const totalProcessingTime = Date.now() - requestStartTime;
|
|
537
|
+
if (this.timeoutConfig.enableTimeoutWarnings &&
|
|
538
|
+
totalProcessingTime >
|
|
539
|
+
this.timeoutConfig.initialResponseTimeout * 0.9) {
|
|
540
|
+
console.warn(`[MiniInteraction] CRITICAL: Interaction processing took ${totalProcessingTime}ms ` +
|
|
541
|
+
`(${Math.round((totalProcessingTime / 3000) * 100)}% of Discord's 3-second limit). ` +
|
|
542
|
+
`This may cause "didn't respond in time" errors. ` +
|
|
543
|
+
`Consider optimizing or using deferReply() for slow operations.`);
|
|
544
|
+
}
|
|
545
|
+
// Log successful response timing for debugging
|
|
546
|
+
if (this.timeoutConfig.enableResponseDebugLogging) {
|
|
547
|
+
console.log(`[MiniInteraction:DEBUG] Request completed in ${totalProcessingTime}ms`);
|
|
548
|
+
}
|
|
401
549
|
return {
|
|
402
550
|
status: 400,
|
|
403
551
|
body: {
|
|
@@ -511,8 +659,8 @@ export class MiniInteraction {
|
|
|
511
659
|
* The following placeholders are available in the HTML file:
|
|
512
660
|
* - `{{username}}`, `{{discriminator}}`, `{{user_id}}`, `{{user_tag}}`
|
|
513
661
|
* - `{{access_token}}`, `{{refresh_token}}`, `{{token_type}}`, `{{scope}}`, `{{expires_at}}`
|
|
514
|
-
|
|
515
|
-
|
|
662
|
+
* - `{{state}}`
|
|
663
|
+
*/
|
|
516
664
|
connectedOAuthPage(filePath) {
|
|
517
665
|
const template = this.loadHtmlTemplate(filePath);
|
|
518
666
|
return ({ user, tokens, state }) => {
|
|
@@ -569,7 +717,7 @@ export class MiniInteraction {
|
|
|
569
717
|
}
|
|
570
718
|
/**
|
|
571
719
|
* Replaces placeholder tokens in a template with escaped HTML values.
|
|
572
|
-
|
|
720
|
+
*/
|
|
573
721
|
renderHtmlTemplate(template, values, options) {
|
|
574
722
|
const rawKeys = options?.rawKeys instanceof Set
|
|
575
723
|
? options.rawKeys
|
|
@@ -666,7 +814,9 @@ export class MiniInteraction {
|
|
|
666
814
|
const location = typeof options.successRedirect === "function"
|
|
667
815
|
? options.successRedirect(authorizeContext)
|
|
668
816
|
: options.successRedirect;
|
|
669
|
-
if (location &&
|
|
817
|
+
if (location &&
|
|
818
|
+
!response.headersSent &&
|
|
819
|
+
!response.writableEnded) {
|
|
670
820
|
response.statusCode = 302;
|
|
671
821
|
response.setHeader("location", location);
|
|
672
822
|
response.end();
|
|
@@ -1057,27 +1207,32 @@ export class MiniInteraction {
|
|
|
1057
1207
|
}
|
|
1058
1208
|
try {
|
|
1059
1209
|
const interactionWithHelpers = createMessageComponentInteraction(interaction);
|
|
1060
|
-
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1210
|
+
// Wrap component handler with timeout
|
|
1211
|
+
const timeoutWrapper = createTimeoutWrapper(async () => {
|
|
1212
|
+
const response = await handler(interactionWithHelpers);
|
|
1213
|
+
const resolvedResponse = response ?? interactionWithHelpers.getResponse();
|
|
1214
|
+
if (!resolvedResponse) {
|
|
1215
|
+
throw new Error(`Component "${customId}" did not return a response. ` +
|
|
1216
|
+
"Return an APIInteractionResponse to acknowledge the interaction.");
|
|
1217
|
+
}
|
|
1218
|
+
return resolvedResponse;
|
|
1219
|
+
}, this.timeoutConfig.initialResponseTimeout, `Component "${customId}"`, this.timeoutConfig.enableTimeoutWarnings);
|
|
1220
|
+
const resolvedResponse = await timeoutWrapper();
|
|
1071
1221
|
return {
|
|
1072
1222
|
status: 200,
|
|
1073
1223
|
body: resolvedResponse,
|
|
1074
1224
|
};
|
|
1075
1225
|
}
|
|
1076
1226
|
catch (error) {
|
|
1227
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1228
|
+
if (errorMessage.includes("Handler timeout")) {
|
|
1229
|
+
console.error(`[MiniInteraction] CRITICAL: Component "${customId}" timed out. ` +
|
|
1230
|
+
`This will result in "didn't respond in time" errors for users.`);
|
|
1231
|
+
}
|
|
1077
1232
|
return {
|
|
1078
1233
|
status: 500,
|
|
1079
1234
|
body: {
|
|
1080
|
-
error: `[MiniInteraction] Component "${customId}" failed: ${
|
|
1235
|
+
error: `[MiniInteraction] Component "${customId}" failed: ${errorMessage}`,
|
|
1081
1236
|
},
|
|
1082
1237
|
};
|
|
1083
1238
|
}
|
|
@@ -1107,27 +1262,32 @@ export class MiniInteraction {
|
|
|
1107
1262
|
}
|
|
1108
1263
|
try {
|
|
1109
1264
|
const interactionWithHelpers = createModalSubmitInteraction(interaction);
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1265
|
+
// Wrap modal handler with timeout
|
|
1266
|
+
const timeoutWrapper = createTimeoutWrapper(async () => {
|
|
1267
|
+
const response = await handler(interactionWithHelpers);
|
|
1268
|
+
const resolvedResponse = response ?? interactionWithHelpers.getResponse();
|
|
1269
|
+
if (!resolvedResponse) {
|
|
1270
|
+
throw new Error(`Modal "${customId}" did not return a response. ` +
|
|
1271
|
+
"Return an APIInteractionResponse to acknowledge the interaction.");
|
|
1272
|
+
}
|
|
1273
|
+
return resolvedResponse;
|
|
1274
|
+
}, this.timeoutConfig.initialResponseTimeout, `Modal "${customId}"`, this.timeoutConfig.enableTimeoutWarnings);
|
|
1275
|
+
const resolvedResponse = await timeoutWrapper();
|
|
1121
1276
|
return {
|
|
1122
1277
|
status: 200,
|
|
1123
1278
|
body: resolvedResponse,
|
|
1124
1279
|
};
|
|
1125
1280
|
}
|
|
1126
1281
|
catch (error) {
|
|
1282
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1283
|
+
if (errorMessage.includes("Handler timeout")) {
|
|
1284
|
+
console.error(`[MiniInteraction] CRITICAL: Modal "${customId}" timed out. ` +
|
|
1285
|
+
`This will result in "didn't respond in time" errors for users.`);
|
|
1286
|
+
}
|
|
1127
1287
|
return {
|
|
1128
1288
|
status: 500,
|
|
1129
1289
|
body: {
|
|
1130
|
-
error: `[MiniInteraction] Modal "${customId}" failed: ${
|
|
1290
|
+
error: `[MiniInteraction] Modal "${customId}" failed: ${errorMessage}`,
|
|
1131
1291
|
},
|
|
1132
1292
|
};
|
|
1133
1293
|
}
|
|
@@ -1159,41 +1319,54 @@ export class MiniInteraction {
|
|
|
1159
1319
|
try {
|
|
1160
1320
|
let response;
|
|
1161
1321
|
let resolvedResponse = null;
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1322
|
+
// Create a timeout wrapper for the command handler
|
|
1323
|
+
const timeoutWrapper = createTimeoutWrapper(async () => {
|
|
1324
|
+
// Check if it's a chat input (slash) command
|
|
1325
|
+
if (commandInteraction.data.type ===
|
|
1326
|
+
ApplicationCommandType.ChatInput) {
|
|
1327
|
+
const interactionWithHelpers = createCommandInteraction(commandInteraction, {
|
|
1328
|
+
canRespond: (id) => this.canRespond(id),
|
|
1329
|
+
trackResponse: (id, token, state) => this.trackInteractionState(id, token, state),
|
|
1330
|
+
logTiming: (id, op, start, success) => this.logResponseTiming(id, op, start, success),
|
|
1331
|
+
});
|
|
1332
|
+
response = await command.handler(interactionWithHelpers);
|
|
1333
|
+
resolvedResponse =
|
|
1334
|
+
response ?? interactionWithHelpers.getResponse();
|
|
1335
|
+
}
|
|
1336
|
+
else if (commandInteraction.data.type ===
|
|
1337
|
+
ApplicationCommandType.User) {
|
|
1338
|
+
// User context menu command
|
|
1339
|
+
const interactionWithHelpers = createUserContextMenuInteraction(commandInteraction);
|
|
1340
|
+
response = await command.handler(interactionWithHelpers);
|
|
1341
|
+
resolvedResponse =
|
|
1342
|
+
response ?? interactionWithHelpers.getResponse();
|
|
1343
|
+
}
|
|
1344
|
+
else if (commandInteraction.data.type ===
|
|
1345
|
+
ApplicationCommandType.PrimaryEntryPoint) {
|
|
1346
|
+
const interactionWithHelpers = createAppCommandInteraction(commandInteraction);
|
|
1347
|
+
response = await command.handler(interactionWithHelpers);
|
|
1348
|
+
resolvedResponse =
|
|
1349
|
+
response ?? interactionWithHelpers.getResponse();
|
|
1350
|
+
}
|
|
1351
|
+
else if (commandInteraction.data.type ===
|
|
1352
|
+
ApplicationCommandType.Message) {
|
|
1353
|
+
// Message context menu command
|
|
1354
|
+
const interactionWithHelpers = createMessageContextMenuInteraction(commandInteraction);
|
|
1355
|
+
response = await command.handler(interactionWithHelpers);
|
|
1356
|
+
resolvedResponse =
|
|
1357
|
+
response ?? interactionWithHelpers.getResponse();
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
// Unknown command type
|
|
1361
|
+
response = await command.handler(commandInteraction);
|
|
1362
|
+
resolvedResponse = response ?? null;
|
|
1363
|
+
}
|
|
1364
|
+
}, this.timeoutConfig.initialResponseTimeout, `Command "${commandName}"`, this.timeoutConfig.enableTimeoutWarnings);
|
|
1365
|
+
await timeoutWrapper();
|
|
1196
1366
|
if (!resolvedResponse) {
|
|
1367
|
+
console.error(`[MiniInteraction] Command "${commandName}" did not return a response. ` +
|
|
1368
|
+
"This indicates the handler completed but no response was generated. " +
|
|
1369
|
+
"Check that deferReply(), reply(), showModal(), or a direct response is returned.");
|
|
1197
1370
|
return {
|
|
1198
1371
|
status: 500,
|
|
1199
1372
|
body: {
|
|
@@ -1209,10 +1382,18 @@ export class MiniInteraction {
|
|
|
1209
1382
|
};
|
|
1210
1383
|
}
|
|
1211
1384
|
catch (error) {
|
|
1385
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1386
|
+
// Check if this was a timeout error
|
|
1387
|
+
if (errorMessage.includes("Handler timeout")) {
|
|
1388
|
+
console.error(`[MiniInteraction] CRITICAL: Command "${commandName}" timed out before responding to Discord. ` +
|
|
1389
|
+
`This will result in "didn't respond in time" errors for users. ` +
|
|
1390
|
+
`Handler took longer than ${this.timeoutConfig.initialResponseTimeout}ms to complete. ` +
|
|
1391
|
+
`Consider using deferReply() for operations that take more than 3 seconds.`);
|
|
1392
|
+
}
|
|
1212
1393
|
return {
|
|
1213
1394
|
status: 500,
|
|
1214
1395
|
body: {
|
|
1215
|
-
error: `[MiniInteraction] Command "${commandName}" failed: ${
|
|
1396
|
+
error: `[MiniInteraction] Command "${commandName}" failed: ${errorMessage}`,
|
|
1216
1397
|
},
|
|
1217
1398
|
};
|
|
1218
1399
|
}
|
|
@@ -1382,3 +1563,45 @@ function resolveOAuthConfig(provided) {
|
|
|
1382
1563
|
redirectUri,
|
|
1383
1564
|
};
|
|
1384
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Wraps a handler function with timeout detection and error handling.
|
|
1568
|
+
*/
|
|
1569
|
+
function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings = true) {
|
|
1570
|
+
return async (...args) => {
|
|
1571
|
+
const startTime = Date.now();
|
|
1572
|
+
let timeoutId;
|
|
1573
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1574
|
+
timeoutId = setTimeout(() => {
|
|
1575
|
+
const elapsed = Date.now() - startTime;
|
|
1576
|
+
console.error(`[MiniInteraction] ${handlerName} timed out after ${elapsed}ms (limit: ${timeoutMs}ms)`);
|
|
1577
|
+
reject(new Error(`Handler timeout: ${handlerName} exceeded ${timeoutMs}ms limit`));
|
|
1578
|
+
}, timeoutMs);
|
|
1579
|
+
});
|
|
1580
|
+
try {
|
|
1581
|
+
const result = await Promise.race([
|
|
1582
|
+
Promise.resolve(handler(...args)),
|
|
1583
|
+
timeoutPromise,
|
|
1584
|
+
]);
|
|
1585
|
+
if (timeoutId) {
|
|
1586
|
+
clearTimeout(timeoutId);
|
|
1587
|
+
}
|
|
1588
|
+
const elapsed = Date.now() - startTime;
|
|
1589
|
+
if (enableWarnings && elapsed > timeoutMs * 0.8) {
|
|
1590
|
+
console.warn(`[MiniInteraction] ${handlerName} completed in ${elapsed}ms (${Math.round((elapsed / timeoutMs) * 100)}% of timeout limit)`);
|
|
1591
|
+
}
|
|
1592
|
+
return result;
|
|
1593
|
+
}
|
|
1594
|
+
catch (error) {
|
|
1595
|
+
if (timeoutId) {
|
|
1596
|
+
clearTimeout(timeoutId);
|
|
1597
|
+
}
|
|
1598
|
+
// Re-throw the error with additional context
|
|
1599
|
+
if (error instanceof Error &&
|
|
1600
|
+
error.message.includes("Handler timeout")) {
|
|
1601
|
+
throw error;
|
|
1602
|
+
}
|
|
1603
|
+
console.error(`[MiniInteraction] ${handlerName} failed:`, error);
|
|
1604
|
+
throw error;
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,21 +5,21 @@ export { CommandBuilder, CommandContext, IntegrationType, } from "./commands/Com
|
|
|
5
5
|
export { UserCommandBuilder, MessageCommandBuilder, AppCommandBuilder, } from "./commands/ContextMenuCommandBuilder.js";
|
|
6
6
|
export type { AttachmentOptionBuilder, ChannelOptionBuilder, MentionableOptionBuilder, NumberOptionBuilder, RoleOptionBuilder, StringOptionBuilder, SubcommandBuilder, SubcommandGroupBuilder, UserOptionBuilder, } from "./commands/CommandBuilder.js";
|
|
7
7
|
export { CommandInteractionOptionResolver, createCommandInteraction, } from "./utils/CommandInteractionOptions.js";
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export type {
|
|
11
|
-
export type {
|
|
12
|
-
export type {
|
|
13
|
-
export
|
|
14
|
-
export
|
|
8
|
+
export { CommandInteraction, MentionableOption, ResolvedUserOption, } from "./utils/CommandInteractionOptions.js";
|
|
9
|
+
export { UserContextMenuInteraction, MessageContextMenuInteraction, AppCommandInteraction, } from "./utils/ContextMenuInteraction.js";
|
|
10
|
+
export type { InteractionFetchHandler, InteractionNodeHandler, InteractionHandlerResult, InteractionRequest, InteractionClientOptions, DiscordOAuthAuthorizeContext, DiscordOAuthCallbackOptions, DiscordOAuthCallbackTemplates, DiscordOAuthErrorTemplateContext, DiscordOAuthServerErrorTemplateContext, DiscordOAuthStateTemplateContext, DiscordOAuthSuccessTemplateContext, DiscordOAuthVerificationPageOptions, } from "./clients/MiniInteraction.js";
|
|
11
|
+
export type { InteractionCommand, SlashCommandHandler, UserCommandHandler, MessageCommandHandler, AppCommandHandler, CommandHandler, } from "./types/Commands.js";
|
|
12
|
+
export type { ComponentCommand, ButtonComponentHandler, StringSelectComponentHandler, RoleSelectComponentHandler, UserSelectComponentHandler, ChannelSelectComponentHandler, MentionableSelectComponentHandler, ComponentHandler, ModalCommand, ModalHandler, InteractionHandler, } from "./clients/MiniInteraction.js";
|
|
13
|
+
export { MessageComponentInteraction, ButtonInteraction, StringSelectInteraction, RoleSelectInteraction, UserSelectInteraction, ChannelSelectInteraction, MentionableSelectInteraction, ResolvedUserOption as ComponentResolvedUserOption, ResolvedMentionableOption as ComponentResolvedMentionableOption, } from "./utils/MessageComponentInteraction.js";
|
|
14
|
+
export { ModalSubmitInteraction } from "./utils/ModalSubmitInteraction.js";
|
|
15
15
|
export { RoleConnectionMetadataTypes } from "./types/RoleConnectionMetadataTypes.js";
|
|
16
16
|
export { ChannelType } from "./types/ChannelType.js";
|
|
17
|
-
export { InteractionFollowUpFlags, InteractionReplyFlags, } from "./types/InteractionFlags.js";
|
|
17
|
+
export { InteractionFollowUpFlags, InteractionReplyFlags, InteractionFlags, } from "./types/InteractionFlags.js";
|
|
18
18
|
export { ButtonStyle } from "./types/ButtonStyle.js";
|
|
19
19
|
export { SeparatorSpacingSize } from "./types/SeparatorSpacingSize.js";
|
|
20
20
|
export { TextInputStyle } from "discord-api-types/v10";
|
|
21
21
|
export { MiniPermFlags } from "./types/PermissionFlags.js";
|
|
22
|
-
export type {
|
|
22
|
+
export type { ActionRowComponent, MessageActionRowComponent, } from "./types/ComponentTypes.js";
|
|
23
23
|
export * from "./builders/index.js";
|
|
24
24
|
export { MiniDataBuilder } from "./database/MiniDataBuilder.js";
|
|
25
25
|
export type { DataField } from "./database/MiniDataBuilder.js";
|
package/dist/index.js
CHANGED
|
@@ -3,9 +3,13 @@ export { MiniInteraction } from "./clients/MiniInteraction.js";
|
|
|
3
3
|
export { CommandBuilder, CommandContext, IntegrationType, } from "./commands/CommandBuilder.js";
|
|
4
4
|
export { UserCommandBuilder, MessageCommandBuilder, AppCommandBuilder, } from "./commands/ContextMenuCommandBuilder.js";
|
|
5
5
|
export { CommandInteractionOptionResolver, createCommandInteraction, } from "./utils/CommandInteractionOptions.js";
|
|
6
|
+
export { CommandInteraction, MentionableOption, ResolvedUserOption, } from "./utils/CommandInteractionOptions.js";
|
|
7
|
+
export { UserContextMenuInteraction, MessageContextMenuInteraction, AppCommandInteraction, } from "./utils/ContextMenuInteraction.js";
|
|
8
|
+
export { MessageComponentInteraction, ButtonInteraction, StringSelectInteraction, RoleSelectInteraction, UserSelectInteraction, ChannelSelectInteraction, MentionableSelectInteraction, ResolvedUserOption as ComponentResolvedUserOption, ResolvedMentionableOption as ComponentResolvedMentionableOption, } from "./utils/MessageComponentInteraction.js";
|
|
9
|
+
export { ModalSubmitInteraction } from "./utils/ModalSubmitInteraction.js";
|
|
6
10
|
export { RoleConnectionMetadataTypes } from "./types/RoleConnectionMetadataTypes.js";
|
|
7
11
|
export { ChannelType } from "./types/ChannelType.js";
|
|
8
|
-
export { InteractionFollowUpFlags, InteractionReplyFlags, } from "./types/InteractionFlags.js";
|
|
12
|
+
export { InteractionFollowUpFlags, InteractionReplyFlags, InteractionFlags, } from "./types/InteractionFlags.js";
|
|
9
13
|
export { ButtonStyle } from "./types/ButtonStyle.js";
|
|
10
14
|
export { SeparatorSpacingSize } from "./types/SeparatorSpacingSize.js";
|
|
11
15
|
export { TextInputStyle } from "discord-api-types/v10";
|
package/dist/types/Commands.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { APIInteractionResponse, RESTPostAPIChatInputApplicationCommandsJSONBody, RESTPostAPIContextMenuApplicationCommandsJSONBody, RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody } from "discord-api-types/v10";
|
|
2
2
|
import type { CommandInteraction } from "../utils/CommandInteractionOptions.js";
|
|
3
3
|
import type { UserContextMenuInteraction, MessageContextMenuInteraction, AppCommandInteraction } from "../utils/ContextMenuInteraction.js";
|
|
4
|
-
import type {
|
|
4
|
+
import type { JSONEncodable } from "../builders/shared.js";
|
|
5
|
+
import type { ComponentCommand, ModalCommand } from "../clients/MiniInteraction.js";
|
|
5
6
|
import type { CommandBuilder } from "../commands/CommandBuilder.js";
|
|
6
7
|
import type { MessageCommandBuilder, UserCommandBuilder, AppCommandBuilder } from "../commands/ContextMenuCommandBuilder.js";
|
|
7
8
|
/** Handler signature for slash command executions within MiniInteraction. */
|
|
@@ -15,19 +16,19 @@ export type AppCommandHandler = (interaction: AppCommandInteraction) => Promise<
|
|
|
15
16
|
/** Union of all command handler types. */
|
|
16
17
|
export type CommandHandler = SlashCommandHandler | UserCommandHandler | MessageCommandHandler | AppCommandHandler;
|
|
17
18
|
/** Structure representing a slash command definition and its runtime handler. */
|
|
18
|
-
export type
|
|
19
|
-
data: RESTPostAPIChatInputApplicationCommandsJSONBody | RESTPostAPIContextMenuApplicationCommandsJSONBody | RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody | CommandBuilder | UserCommandBuilder | MessageCommandBuilder | AppCommandBuilder
|
|
19
|
+
export type InteractionCommand = {
|
|
20
|
+
data: RESTPostAPIChatInputApplicationCommandsJSONBody | RESTPostAPIContextMenuApplicationCommandsJSONBody | RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody | CommandBuilder | UserCommandBuilder | MessageCommandBuilder | AppCommandBuilder | JSONEncodable<RESTPostAPIChatInputApplicationCommandsJSONBody | RESTPostAPIContextMenuApplicationCommandsJSONBody | RESTPostAPIPrimaryEntryPointApplicationCommandJSONBody>;
|
|
20
21
|
handler: CommandHandler;
|
|
21
22
|
/**
|
|
22
23
|
* Optional array of component handlers related to this command.
|
|
23
24
|
* These will be automatically registered when the command is loaded.
|
|
24
25
|
*/
|
|
25
|
-
components?:
|
|
26
|
+
components?: ComponentCommand[];
|
|
26
27
|
/**
|
|
27
28
|
* Optional array of modal handlers related to this command.
|
|
28
29
|
* These will be automatically registered when the command is loaded.
|
|
29
30
|
*/
|
|
30
|
-
modals?:
|
|
31
|
+
modals?: ModalCommand[];
|
|
31
32
|
};
|
|
32
33
|
/** Map of command names to their registered MiniInteraction command definitions. */
|
|
33
|
-
export type
|
|
34
|
+
export type InteractionCommandsMap = Map<string, InteractionCommand>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { APIComponentInActionRow, APIComponentInMessageActionRow } from "discord-api-types/v10";
|
|
2
|
-
/**
|
|
3
|
-
export type
|
|
4
|
-
/**
|
|
5
|
-
export type
|
|
2
|
+
/** Defines a component structure for use in ActionRow builders. */
|
|
3
|
+
export type ActionRowComponent = APIComponentInActionRow;
|
|
4
|
+
/** Defines a message component structure for use in message builders. */
|
|
5
|
+
export type MessageActionRowComponent = APIComponentInMessageActionRow;
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
/** Flags available
|
|
2
|
-
export declare enum
|
|
3
|
-
Ephemeral = 64,
|
|
4
|
-
IsComponentsV2 = 32768
|
|
5
|
-
}
|
|
6
|
-
/** Flags available when sending a follow-up message for an interaction. */
|
|
7
|
-
export declare enum InteractionFollowUpFlags {
|
|
1
|
+
/** Flags available for interaction responses. */
|
|
2
|
+
export declare enum InteractionFlags {
|
|
8
3
|
Ephemeral = 64,
|
|
9
4
|
IsComponentsV2 = 32768
|
|
10
5
|
}
|
|
6
|
+
/** @deprecated Use InteractionFlags instead. */
|
|
7
|
+
export { InteractionFlags as InteractionReplyFlags };
|
|
8
|
+
/** @deprecated Use InteractionFlags instead. */
|
|
9
|
+
export { InteractionFlags as InteractionFollowUpFlags };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
/** Flags available
|
|
2
|
-
export var
|
|
3
|
-
(function (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
})(
|
|
7
|
-
/**
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
InteractionFollowUpFlags[InteractionFollowUpFlags["IsComponentsV2"] = 32768] = "IsComponentsV2";
|
|
12
|
-
})(InteractionFollowUpFlags || (InteractionFollowUpFlags = {}));
|
|
1
|
+
/** Flags available for interaction responses. */
|
|
2
|
+
export var InteractionFlags;
|
|
3
|
+
(function (InteractionFlags) {
|
|
4
|
+
InteractionFlags[InteractionFlags["Ephemeral"] = 64] = "Ephemeral";
|
|
5
|
+
InteractionFlags[InteractionFlags["IsComponentsV2"] = 32768] = "IsComponentsV2";
|
|
6
|
+
})(InteractionFlags || (InteractionFlags = {}));
|
|
7
|
+
/** @deprecated Use InteractionFlags instead. */
|
|
8
|
+
export { InteractionFlags as InteractionReplyFlags };
|
|
9
|
+
/** @deprecated Use InteractionFlags instead. */
|
|
10
|
+
export { InteractionFlags as InteractionFollowUpFlags };
|