@minesa-org/mini-interaction 0.2.3 → 0.2.5

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.
@@ -49,15 +49,6 @@ export type InteractionTimeoutConfig = {
49
49
  /** Whether to enable debug logging for interaction responses (default: false) */
50
50
  enableResponseDebugLogging?: boolean;
51
51
  };
52
- /** Enhanced timeout configuration with response acknowledgment settings. */
53
- export type InteractionTimeoutConfigV2 = InteractionTimeoutConfig & {
54
- /** Time to wait for Discord acknowledgment after sending response (default: 500ms) */
55
- responseAcknowledgmentTimeout?: number;
56
- /** Maximum retries for response acknowledgment (default: 2) */
57
- responseAcknowledgmentRetries?: number;
58
- /** Delay between acknowledgment retries (default: 100ms) */
59
- responseAcknowledgmentRetryDelay?: number;
60
- };
61
52
  /** Handler signature invoked for Discord button interactions. */
62
53
  export type ButtonComponentHandler = (interaction: ButtonInteraction) => Promise<APIInteractionResponse | void> | APIInteractionResponse | void;
63
54
  /** Handler signature invoked for Discord string select menu interactions. */
@@ -184,46 +175,15 @@ export declare class MiniInteraction {
184
175
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
185
176
  */
186
177
  constructor({ applicationId, publicKey, commandsDirectory, componentsDirectory, utilsDirectory, fetchImplementation, verifyKeyImplementation, timeoutConfig, }: InteractionClientOptions);
187
- /**
188
- * Tracks the state of an interaction to prevent race conditions and double responses.
189
- */
190
178
  private trackInteractionState;
191
179
  /**
192
180
  * Checks if an interaction can still respond (not expired and not already responded).
193
181
  */
194
182
  private canRespond;
195
- /**
196
- * Logs response timing and acknowledgment for debugging.
197
- */
198
- private logResponseTiming;
199
- /**
200
- * Simulates waiting for Discord acknowledgment to help debug timing issues.
201
- * Note: This is a best-effort simulation since actual acknowledgment happens at the HTTP level.
202
- */
203
- private simulateAcknowledgmentWait;
204
- /**
205
- * Enables or disables debug logging for interaction responses and timing.
206
- * Useful for troubleshooting "didn't respond in time" errors.
207
- *
208
- * @param enabled - Whether to enable debug logging
209
- */
210
- setResponseDebugLogging(enabled: boolean): void;
211
183
  /**
212
184
  * Gets the current state of an interaction.
213
185
  */
214
186
  private getInteractionState;
215
- /**
216
- * Gets the current state of an interaction for debugging purposes.
217
- *
218
- * @param interactionId - The interaction ID to check
219
- * @returns The current interaction state or null if not found
220
- */
221
- getInteractionStateInfo(interactionId: string): {
222
- state: "pending" | "deferred" | "responded" | "expired";
223
- timestamp: number;
224
- token: string;
225
- responseCount: number;
226
- } | null;
227
187
  /**
228
188
  * Clears expired interaction states to prevent memory leaks.
229
189
  * Call this periodically to clean up old interaction data.
@@ -434,5 +394,10 @@ export declare class MiniInteraction {
434
394
  * Handles execution of an application command interaction.
435
395
  */
436
396
  private handleApplicationCommand;
397
+ /**
398
+ * Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
399
+ * This is used for interactions that have already been acknowledged (e.g., via deferReply).
400
+ */
401
+ private sendFollowUp;
437
402
  }
438
403
  export {};
@@ -78,27 +78,16 @@ export class MiniInteraction {
78
78
  enableTimeoutWarnings: true,
79
79
  autoDeferSlowOperations: true,
80
80
  enableResponseDebugLogging: false,
81
- responseAcknowledgmentTimeout: 500, // 500ms to wait for acknowledgment
82
- responseAcknowledgmentRetries: 2,
83
- responseAcknowledgmentRetryDelay: 100,
84
81
  ...timeoutConfig,
85
82
  };
86
83
  }
87
- /**
88
- * Tracks the state of an interaction to prevent race conditions and double responses.
89
- */
90
84
  trackInteractionState(interactionId, token, state) {
91
- const existing = this.interactionStates.get(interactionId);
92
85
  const now = Date.now();
93
86
  this.interactionStates.set(interactionId, {
94
87
  state,
95
88
  timestamp: now,
96
89
  token,
97
- responseCount: existing ? existing.responseCount + 1 : 1,
98
90
  });
99
- if (this.timeoutConfig.enableResponseDebugLogging) {
100
- console.log(`[MiniInteraction:DEBUG] Interaction ${interactionId} state: ${state} (${existing ? existing.responseCount + 1 : 1} responses)`);
101
- }
102
91
  }
103
92
  /**
104
93
  * Checks if an interaction can still respond (not expired and not already responded).
@@ -118,68 +107,12 @@ export class MiniInteraction {
118
107
  }
119
108
  return true;
120
109
  }
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
110
  /**
168
111
  * Gets the current state of an interaction.
169
112
  */
170
113
  getInteractionState(interactionId) {
171
114
  return this.interactionStates.get(interactionId);
172
115
  }
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
116
  /**
184
117
  * Clears expired interaction states to prevent memory leaks.
185
118
  * Call this periodically to clean up old interaction data.
@@ -194,9 +127,6 @@ export class MiniInteraction {
194
127
  cleaned++;
195
128
  }
196
129
  }
197
- if (cleaned > 0) {
198
- console.log(`[MiniInteraction] Cleaned up ${cleaned} expired interactions`);
199
- }
200
130
  return cleaned;
201
131
  }
202
132
  normalizeCommandData(data) {
@@ -530,10 +460,6 @@ export class MiniInteraction {
530
460
  `This may cause "didn't respond in time" errors. ` +
531
461
  `Consider optimizing or using deferReply() for slow operations.`);
532
462
  }
533
- // Log successful response timing for debugging
534
- if (this.timeoutConfig.enableResponseDebugLogging) {
535
- console.log(`[MiniInteraction:DEBUG] Request completed in ${totalProcessingTime}ms`);
536
- }
537
463
  return {
538
464
  status: 400,
539
465
  body: {
@@ -1194,8 +1120,18 @@ export class MiniInteraction {
1194
1120
  };
1195
1121
  }
1196
1122
  try {
1197
- const interactionWithHelpers = createMessageComponentInteraction(interaction);
1198
- // Wrap component handler with timeout
1123
+ // Create an acknowledgment promise that resolves when the handler calls reply() or deferReply()
1124
+ let ackResolver = null;
1125
+ const ackPromise = new Promise((resolve) => {
1126
+ ackResolver = resolve;
1127
+ });
1128
+ // Helper to send follow-up responses via webhooks
1129
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1130
+ const interactionWithHelpers = createMessageComponentInteraction(interaction, {
1131
+ onAck: (response) => ackResolver?.(response),
1132
+ sendFollowUp,
1133
+ });
1134
+ // Wrap component handler with timeout and acknowledgment
1199
1135
  const timeoutWrapper = createTimeoutWrapper(async () => {
1200
1136
  const response = await handler(interactionWithHelpers);
1201
1137
  const resolvedResponse = response ?? interactionWithHelpers.getResponse();
@@ -1204,7 +1140,7 @@ export class MiniInteraction {
1204
1140
  "Return an APIInteractionResponse to acknowledge the interaction.");
1205
1141
  }
1206
1142
  return resolvedResponse;
1207
- }, this.timeoutConfig.initialResponseTimeout, `Component "${customId}"`, this.timeoutConfig.enableTimeoutWarnings);
1143
+ }, this.timeoutConfig.initialResponseTimeout, `Component "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1208
1144
  const resolvedResponse = await timeoutWrapper();
1209
1145
  return {
1210
1146
  status: 200,
@@ -1249,8 +1185,18 @@ export class MiniInteraction {
1249
1185
  };
1250
1186
  }
1251
1187
  try {
1252
- const interactionWithHelpers = createModalSubmitInteraction(interaction);
1253
- // Wrap modal handler with timeout
1188
+ // Create an acknowledgment promise for modals
1189
+ let ackResolver = null;
1190
+ const ackPromise = new Promise((resolve) => {
1191
+ ackResolver = resolve;
1192
+ });
1193
+ // Helper to send follow-up responses via webhooks
1194
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1195
+ const interactionWithHelpers = createModalSubmitInteraction(interaction, {
1196
+ onAck: (response) => ackResolver?.(response),
1197
+ sendFollowUp,
1198
+ });
1199
+ // Wrap modal handler with timeout and acknowledgment
1254
1200
  const timeoutWrapper = createTimeoutWrapper(async () => {
1255
1201
  const response = await handler(interactionWithHelpers);
1256
1202
  const resolvedResponse = response ?? interactionWithHelpers.getResponse();
@@ -1259,7 +1205,7 @@ export class MiniInteraction {
1259
1205
  "Return an APIInteractionResponse to acknowledge the interaction.");
1260
1206
  }
1261
1207
  return resolvedResponse;
1262
- }, this.timeoutConfig.initialResponseTimeout, `Modal "${customId}"`, this.timeoutConfig.enableTimeoutWarnings);
1208
+ }, this.timeoutConfig.initialResponseTimeout, `Modal "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1263
1209
  const resolvedResponse = await timeoutWrapper();
1264
1210
  return {
1265
1211
  status: 200,
@@ -1307,6 +1253,13 @@ export class MiniInteraction {
1307
1253
  try {
1308
1254
  let response;
1309
1255
  let resolvedResponse = null;
1256
+ // Create an acknowledgment promise for application commands
1257
+ let ackResolver = null;
1258
+ const ackPromise = new Promise((resolve) => {
1259
+ ackResolver = resolve;
1260
+ });
1261
+ // Helper to send follow-up responses via webhooks
1262
+ const sendFollowUp = (token, data) => this.sendFollowUp(token, data);
1310
1263
  // Create a timeout wrapper for the command handler
1311
1264
  const timeoutWrapper = createTimeoutWrapper(async () => {
1312
1265
  // Check if it's a chat input (slash) command
@@ -1315,7 +1268,8 @@ export class MiniInteraction {
1315
1268
  const interactionWithHelpers = createCommandInteraction(commandInteraction, {
1316
1269
  canRespond: (id) => this.canRespond(id),
1317
1270
  trackResponse: (id, token, state) => this.trackInteractionState(id, token, state),
1318
- logTiming: (id, op, start, success) => this.logResponseTiming(id, op, start, success),
1271
+ onAck: (response) => ackResolver?.(response),
1272
+ sendFollowUp,
1319
1273
  });
1320
1274
  response = await command.handler(interactionWithHelpers);
1321
1275
  resolvedResponse =
@@ -1324,14 +1278,18 @@ export class MiniInteraction {
1324
1278
  else if (commandInteraction.data.type ===
1325
1279
  ApplicationCommandType.User) {
1326
1280
  // User context menu command
1327
- const interactionWithHelpers = createUserContextMenuInteraction(commandInteraction);
1281
+ const interactionWithHelpers = createUserContextMenuInteraction(commandInteraction, {
1282
+ onAck: (response) => ackResolver?.(response),
1283
+ });
1328
1284
  response = await command.handler(interactionWithHelpers);
1329
1285
  resolvedResponse =
1330
1286
  response ?? interactionWithHelpers.getResponse();
1331
1287
  }
1332
1288
  else if (commandInteraction.data.type ===
1333
1289
  ApplicationCommandType.PrimaryEntryPoint) {
1334
- const interactionWithHelpers = createAppCommandInteraction(commandInteraction);
1290
+ const interactionWithHelpers = createAppCommandInteraction(commandInteraction, {
1291
+ onAck: (response) => ackResolver?.(response),
1292
+ });
1335
1293
  response = await command.handler(interactionWithHelpers);
1336
1294
  resolvedResponse =
1337
1295
  response ?? interactionWithHelpers.getResponse();
@@ -1339,7 +1297,9 @@ export class MiniInteraction {
1339
1297
  else if (commandInteraction.data.type ===
1340
1298
  ApplicationCommandType.Message) {
1341
1299
  // Message context menu command
1342
- const interactionWithHelpers = createMessageContextMenuInteraction(commandInteraction);
1300
+ const interactionWithHelpers = createMessageContextMenuInteraction(commandInteraction, {
1301
+ onAck: (response) => ackResolver?.(response),
1302
+ });
1343
1303
  response = await command.handler(interactionWithHelpers);
1344
1304
  resolvedResponse =
1345
1305
  response ?? interactionWithHelpers.getResponse();
@@ -1349,9 +1309,10 @@ export class MiniInteraction {
1349
1309
  response = await command.handler(commandInteraction);
1350
1310
  resolvedResponse = response ?? null;
1351
1311
  }
1352
- }, this.timeoutConfig.initialResponseTimeout, `Command "${commandName}"`, this.timeoutConfig.enableTimeoutWarnings);
1353
- await timeoutWrapper();
1354
- if (!resolvedResponse) {
1312
+ return resolvedResponse;
1313
+ }, this.timeoutConfig.initialResponseTimeout, `Command "${commandName}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1314
+ const finalResponse = await timeoutWrapper();
1315
+ if (!finalResponse) {
1355
1316
  console.error(`[MiniInteraction] Command "${commandName}" did not return a response. ` +
1356
1317
  "This indicates the handler completed but no response was generated. " +
1357
1318
  "Check that deferReply(), reply(), showModal(), or a direct response is returned.");
@@ -1366,7 +1327,7 @@ export class MiniInteraction {
1366
1327
  }
1367
1328
  return {
1368
1329
  status: 200,
1369
- body: resolvedResponse,
1330
+ body: finalResponse,
1370
1331
  };
1371
1332
  }
1372
1333
  catch (error) {
@@ -1386,6 +1347,33 @@ export class MiniInteraction {
1386
1347
  };
1387
1348
  }
1388
1349
  }
1350
+ /**
1351
+ * Sends a follow-up response or edits an existing response via Discord's interaction webhooks.
1352
+ * This is used for interactions that have already been acknowledged (e.g., via deferReply).
1353
+ */
1354
+ async sendFollowUp(token, response) {
1355
+ const url = `${DISCORD_BASE_URL}/webhooks/${this.applicationId}/${token}/messages/@original`;
1356
+ // Only send follow-up if there is data to send
1357
+ if (!('data' in response) || !response.data) {
1358
+ return;
1359
+ }
1360
+ try {
1361
+ const fetchResponse = await this.fetchImpl(url, {
1362
+ method: "PATCH",
1363
+ headers: {
1364
+ "Content-Type": "application/json",
1365
+ },
1366
+ body: JSON.stringify(response.data),
1367
+ });
1368
+ if (!fetchResponse.ok) {
1369
+ const errorBody = await fetchResponse.text();
1370
+ console.error(`[MiniInteraction] Failed to send follow-up response: [${fetchResponse.status}] ${errorBody}`);
1371
+ }
1372
+ }
1373
+ catch (error) {
1374
+ console.error(`[MiniInteraction] Error sending follow-up response: ${error instanceof Error ? error.message : String(error)}`);
1375
+ }
1376
+ }
1389
1377
  }
1390
1378
  const DEFAULT_DISCORD_OAUTH_TEMPLATES = {
1391
1379
  success: ({ user }) => {
@@ -1554,7 +1542,7 @@ function resolveOAuthConfig(provided) {
1554
1542
  /**
1555
1543
  * Wraps a handler function with timeout detection and error handling.
1556
1544
  */
1557
- function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings = true) {
1545
+ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings = true, ackPromise) {
1558
1546
  return async (...args) => {
1559
1547
  const startTime = Date.now();
1560
1548
  let timeoutId;
@@ -1566,10 +1554,14 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1566
1554
  }, timeoutMs);
1567
1555
  });
1568
1556
  try {
1569
- const result = await Promise.race([
1557
+ const promises = [
1570
1558
  Promise.resolve(handler(...args)),
1571
1559
  timeoutPromise,
1572
- ]);
1560
+ ];
1561
+ if (ackPromise) {
1562
+ promises.push(ackPromise);
1563
+ }
1564
+ const result = await Promise.race(promises);
1573
1565
  if (timeoutId) {
1574
1566
  clearTimeout(timeoutId);
1575
1567
  }
@@ -77,7 +77,6 @@ export class MiniDatabase {
77
77
  await this.mongoClient.connect();
78
78
  this.mongoDb = this.mongoClient.db(this.config.dbName || "minidb");
79
79
  this.mongoCollection = this.mongoDb.collection(this.config.collectionName || "data");
80
- console.log("✅ [MiniDatabase] Connected to MongoDB");
81
80
  }
82
81
  catch (err) {
83
82
  console.error("❌ [MiniDatabase] Failed to connect to MongoDB:", err);
@@ -136,7 +135,6 @@ export class MiniDatabase {
136
135
  createdAt: new Date(),
137
136
  },
138
137
  }, { upsert: true });
139
- console.log(`✅ [MiniDatabase] Saved data for key "${key}"`);
140
138
  return true;
141
139
  }
142
140
  catch (err) {
@@ -175,7 +173,6 @@ export class MiniDatabase {
175
173
  createdAt: new Date(),
176
174
  },
177
175
  }, { upsert: true });
178
- console.log(`✅ [MiniDatabase] Updated data for key "${key}"`);
179
176
  return true;
180
177
  }
181
178
  catch (err) {
@@ -194,7 +191,6 @@ export class MiniDatabase {
194
191
  throw new Error("MongoDB collection is not initialized");
195
192
  }
196
193
  await collection.deleteOne({ _id: key });
197
- console.log(`✅ [MiniDatabase] Deleted data for key "${key}"`);
198
194
  return true;
199
195
  }
200
196
  catch (err) {
@@ -208,7 +204,6 @@ export class MiniDatabase {
208
204
  async close() {
209
205
  if (this.mongoClient) {
210
206
  await this.mongoClient.close();
211
- console.log("✅ [MiniDatabase] MongoDB connection closed");
212
207
  }
213
208
  }
214
209
  }
@@ -154,7 +154,8 @@ export interface CommandInteraction extends Omit<APIChatInputApplicationCommandI
154
154
  withTimeoutProtection<T>(operation: () => Promise<T>, deferOptions?: DeferReplyOptions): Promise<T>;
155
155
  canRespond?(interactionId: string): boolean;
156
156
  trackResponse?(interactionId: string, token: string, state: 'responded' | 'deferred'): void;
157
- logTiming?(interactionId: string, operation: string, startTime: number, success: boolean): void;
157
+ onAck?(response: APIInteractionResponse): void;
158
+ sendFollowUp?(token: string, response: APIInteractionResponse): void;
158
159
  }
159
160
  export declare const CommandInteraction: {};
160
161
  /**
@@ -168,4 +169,6 @@ export declare function createCommandInteraction(interaction: APIChatInputApplic
168
169
  canRespond?: (interactionId: string) => boolean;
169
170
  trackResponse?: (interactionId: string, token: string, state: 'responded' | 'deferred') => void;
170
171
  logTiming?: (interactionId: string, operation: string, startTime: number, success: boolean) => void;
172
+ onAck?: (response: APIInteractionResponse) => void;
173
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
171
174
  }): CommandInteraction;
@@ -379,16 +379,19 @@ export function createCommandInteraction(interaction, helpers) {
379
379
  if (!this.canRespond?.(this.id)) {
380
380
  throw new Error('Interaction cannot respond: already responded or expired');
381
381
  }
382
- const startTime = Date.now();
383
382
  const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
384
383
  // Track response
385
384
  this.trackResponse?.(this.id, this.token, 'responded');
386
- // Log timing if debug enabled
387
- this.logTiming?.(this.id, 'reply', startTime, true);
385
+ // Notify acknowledgment
386
+ this.onAck?.(response);
388
387
  return response;
389
388
  },
390
389
  followUp(data) {
391
- return createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
390
+ const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
391
+ if (this.sendFollowUp) {
392
+ this.sendFollowUp(this.token, response);
393
+ }
394
+ return response;
392
395
  },
393
396
  edit(data) {
394
397
  return createMessageResponse(InteractionResponseType.UpdateMessage, data);
@@ -398,12 +401,13 @@ export function createCommandInteraction(interaction, helpers) {
398
401
  if (!this.canRespond?.(this.id)) {
399
402
  throw new Error('Interaction cannot edit reply: already responded, expired, or not deferred');
400
403
  }
401
- const startTime = Date.now();
402
404
  const response = createMessageResponse(InteractionResponseType.UpdateMessage, data);
405
+ // If it's already deferred or responded, we MUST use a webhook
406
+ if (this.sendFollowUp) {
407
+ this.sendFollowUp(this.token, response);
408
+ }
403
409
  // Track response
404
410
  this.trackResponse?.(this.id, this.token, 'responded');
405
- // Log timing if debug enabled
406
- this.logTiming?.(this.id, 'editReply', startTime, true);
407
411
  return response;
408
412
  },
409
413
  deferReply(options) {
@@ -411,14 +415,13 @@ export function createCommandInteraction(interaction, helpers) {
411
415
  if (!this.canRespond?.(this.id)) {
412
416
  throw new Error('Interaction cannot defer: already responded or expired');
413
417
  }
414
- const startTime = Date.now();
415
418
  const response = createDeferredResponse(options?.flags !== undefined
416
419
  ? { flags: options.flags }
417
420
  : undefined);
418
421
  // Track deferred state
419
422
  this.trackResponse?.(this.id, this.token, 'deferred');
420
- // Log timing if debug enabled
421
- this.logTiming?.(this.id, 'deferReply', startTime, true);
423
+ // Notify acknowledgment
424
+ this.onAck?.(response);
422
425
  return response;
423
426
  },
424
427
  showModal(data) {
@@ -469,7 +472,7 @@ export function createCommandInteraction(interaction, helpers) {
469
472
  // Helper methods for state management
470
473
  canRespond: helpers?.canRespond,
471
474
  trackResponse: helpers?.trackResponse,
472
- logTiming: helpers?.logTiming,
475
+ onAck: helpers?.onAck,
473
476
  };
474
477
  return commandInteraction;
475
478
  }
@@ -12,6 +12,7 @@ type ContextMenuInteractionHelpers = {
12
12
  showModal: (data: APIModalInteractionResponseCallbackData | {
13
13
  toJSON(): APIModalInteractionResponseCallbackData;
14
14
  }) => APIModalInteractionResponse;
15
+ onAck?: (response: APIInteractionResponse) => void;
15
16
  };
16
17
  /**
17
18
  * User context menu interaction with helper methods.
@@ -40,19 +41,25 @@ export declare const AppCommandInteraction: {};
40
41
  * @param interaction - The raw user context menu interaction payload from Discord.
41
42
  * @returns A helper-augmented interaction object.
42
43
  */
43
- export declare function createUserContextMenuInteraction(interaction: APIUserApplicationCommandInteraction): UserContextMenuInteraction;
44
+ export declare function createUserContextMenuInteraction(interaction: APIUserApplicationCommandInteraction, helpers?: {
45
+ onAck?: (response: APIInteractionResponse) => void;
46
+ }): UserContextMenuInteraction;
44
47
  /**
45
48
  * Wraps a raw message context menu interaction with helper methods.
46
49
  *
47
50
  * @param interaction - The raw message context menu interaction payload from Discord.
48
51
  * @returns A helper-augmented interaction object.
49
52
  */
50
- export declare function createMessageContextMenuInteraction(interaction: APIMessageApplicationCommandInteraction): MessageContextMenuInteraction;
53
+ export declare function createMessageContextMenuInteraction(interaction: APIMessageApplicationCommandInteraction, helpers?: {
54
+ onAck?: (response: APIInteractionResponse) => void;
55
+ }): MessageContextMenuInteraction;
51
56
  /**
52
57
  * Wraps a raw primary entry point interaction with helper methods.
53
58
  *
54
59
  * @param interaction - The raw primary entry point interaction payload from Discord.
55
60
  * @returns A helper-augmented interaction object.
56
61
  */
57
- export declare function createAppCommandInteraction(interaction: APIPrimaryEntryPointCommandInteraction): AppCommandInteraction;
62
+ export declare function createAppCommandInteraction(interaction: APIPrimaryEntryPointCommandInteraction, helpers?: {
63
+ onAck?: (response: APIInteractionResponse) => void;
64
+ }): AppCommandInteraction;
58
65
  export {};
@@ -3,7 +3,7 @@ import { normaliseInteractionMessageData, normaliseMessageFlags, } from "./inter
3
3
  export const UserContextMenuInteraction = {};
4
4
  export const MessageContextMenuInteraction = {};
5
5
  export const AppCommandInteraction = {};
6
- function createContextMenuInteractionHelpers() {
6
+ function createContextMenuInteractionHelpers(helpers) {
7
7
  let capturedResponse = null;
8
8
  const captureResponse = (response) => {
9
9
  capturedResponse = response;
@@ -25,15 +25,21 @@ function createContextMenuInteractionHelpers() {
25
25
  }
26
26
  return captureResponse({ type });
27
27
  }
28
- const reply = (data) => createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
28
+ const reply = (data) => {
29
+ const response = createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
30
+ helpers?.onAck?.(response);
31
+ return response;
32
+ };
29
33
  const followUp = (data) => createMessageResponse(InteractionResponseType.ChannelMessageWithSource, data);
30
34
  const editReply = (data) => createMessageResponse(InteractionResponseType.UpdateMessage, data);
31
35
  const deferReply = (options = {}) => {
32
36
  const flags = normaliseMessageFlags(options.flags);
33
- return captureResponse({
37
+ const response = captureResponse({
34
38
  type: InteractionResponseType.DeferredChannelMessageWithSource,
35
39
  data: flags ? { flags } : undefined,
36
40
  });
41
+ helpers?.onAck?.(response);
42
+ return response;
37
43
  };
38
44
  const showModal = (data) => {
39
45
  const modalData = typeof data === "object" && "toJSON" in data ? data.toJSON() : data;
@@ -50,6 +56,7 @@ function createContextMenuInteractionHelpers() {
50
56
  editReply,
51
57
  deferReply,
52
58
  showModal,
59
+ onAck: helpers?.onAck,
53
60
  };
54
61
  }
55
62
  /**
@@ -58,8 +65,8 @@ function createContextMenuInteractionHelpers() {
58
65
  * @param interaction - The raw user context menu interaction payload from Discord.
59
66
  * @returns A helper-augmented interaction object.
60
67
  */
61
- export function createUserContextMenuInteraction(interaction) {
62
- return Object.assign(interaction, createContextMenuInteractionHelpers(), {
68
+ export function createUserContextMenuInteraction(interaction, helpers) {
69
+ return Object.assign(interaction, createContextMenuInteractionHelpers(helpers), {
63
70
  targetUser: resolveTargetUser(interaction),
64
71
  });
65
72
  }
@@ -69,8 +76,8 @@ export function createUserContextMenuInteraction(interaction) {
69
76
  * @param interaction - The raw message context menu interaction payload from Discord.
70
77
  * @returns A helper-augmented interaction object.
71
78
  */
72
- export function createMessageContextMenuInteraction(interaction) {
73
- return Object.assign(interaction, createContextMenuInteractionHelpers(), {
79
+ export function createMessageContextMenuInteraction(interaction, helpers) {
80
+ return Object.assign(interaction, createContextMenuInteractionHelpers(helpers), {
74
81
  targetMessage: resolveTargetMessage(interaction),
75
82
  });
76
83
  }
@@ -80,8 +87,8 @@ export function createMessageContextMenuInteraction(interaction) {
80
87
  * @param interaction - The raw primary entry point interaction payload from Discord.
81
88
  * @returns A helper-augmented interaction object.
82
89
  */
83
- export function createAppCommandInteraction(interaction) {
84
- return Object.assign(interaction, createContextMenuInteractionHelpers());
90
+ export function createAppCommandInteraction(interaction, helpers) {
91
+ return Object.assign(interaction, createContextMenuInteractionHelpers(helpers));
85
92
  }
86
93
  function resolveTargetMessage(interaction) {
87
94
  const targetId = interaction.data?.target_id;
@@ -27,6 +27,8 @@ type BaseComponentInteractionHelpers = {
27
27
  showModal: (data: APIModalInteractionResponseCallbackData | {
28
28
  toJSON(): APIModalInteractionResponseCallbackData;
29
29
  }) => APIModalInteractionResponse;
30
+ onAck?: (response: APIInteractionResponse) => void;
31
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
30
32
  };
31
33
  /**
32
34
  * Button interaction with helper methods.
@@ -96,6 +98,11 @@ export type MessageComponentInteraction = APIMessageComponentInteraction & {
96
98
  showModal: (data: APIModalInteractionResponseCallbackData | {
97
99
  toJSON(): APIModalInteractionResponseCallbackData;
98
100
  }) => APIModalInteractionResponse;
101
+ /**
102
+ * Finalise the interaction response via a webhook follow-up.
103
+ * This is automatically called by reply() and update() if the interaction is deferred.
104
+ */
105
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
99
106
  /**
100
107
  * The selected values from a select menu interaction.
101
108
  * This property is only present for select menu interactions.
@@ -135,5 +142,8 @@ export declare const MessageComponentInteraction: {};
135
142
  * @param interaction - The raw interaction payload from Discord.
136
143
  * @returns A helper-augmented interaction object.
137
144
  */
138
- export declare function createMessageComponentInteraction(interaction: APIMessageComponentInteraction): MessageComponentInteraction;
145
+ export declare function createMessageComponentInteraction(interaction: APIMessageComponentInteraction, helpers?: {
146
+ onAck?: (response: APIInteractionResponse) => void;
147
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
148
+ }): MessageComponentInteraction;
139
149
  export {};
@@ -15,8 +15,9 @@ export const MessageComponentInteraction = {};
15
15
  * @param interaction - The raw interaction payload from Discord.
16
16
  * @returns A helper-augmented interaction object.
17
17
  */
18
- export function createMessageComponentInteraction(interaction) {
18
+ export function createMessageComponentInteraction(interaction, helpers) {
19
19
  let capturedResponse = null;
20
+ let isDeferred = false;
20
21
  const captureResponse = (response) => {
21
22
  capturedResponse = response;
22
23
  return response;
@@ -26,10 +27,17 @@ export function createMessageComponentInteraction(interaction) {
26
27
  if (!normalisedData) {
27
28
  throw new Error("[MiniInteraction] Component replies require response data to be provided.");
28
29
  }
29
- return captureResponse({
30
+ const response = captureResponse({
30
31
  type: InteractionResponseType.ChannelMessageWithSource,
31
32
  data: normalisedData,
32
33
  });
34
+ if (isDeferred && helpers?.sendFollowUp) {
35
+ helpers.sendFollowUp(interaction.token, response);
36
+ }
37
+ else {
38
+ helpers?.onAck?.(response);
39
+ }
40
+ return response;
33
41
  };
34
42
  const deferReply = (options) => {
35
43
  const flags = normaliseMessageFlags(options?.flags);
@@ -41,7 +49,10 @@ export function createMessageComponentInteraction(interaction) {
41
49
  : {
42
50
  type: InteractionResponseType.DeferredChannelMessageWithSource,
43
51
  };
44
- return captureResponse(response);
52
+ captureResponse(response);
53
+ isDeferred = true;
54
+ helpers?.onAck?.(response);
55
+ return response;
45
56
  };
46
57
  const update = (data) => {
47
58
  const normalisedData = normaliseInteractionMessageData(data);
@@ -53,11 +64,19 @@ export function createMessageComponentInteraction(interaction) {
53
64
  : {
54
65
  type: InteractionResponseType.UpdateMessage,
55
66
  };
67
+ if (isDeferred && helpers?.sendFollowUp) {
68
+ helpers.sendFollowUp(interaction.token, response);
69
+ }
56
70
  return captureResponse(response);
57
71
  };
58
- const deferUpdate = () => captureResponse({
59
- type: InteractionResponseType.DeferredMessageUpdate,
60
- });
72
+ const deferUpdate = () => {
73
+ const response = captureResponse({
74
+ type: InteractionResponseType.DeferredMessageUpdate,
75
+ });
76
+ isDeferred = true;
77
+ helpers?.onAck?.(response);
78
+ return response;
79
+ };
61
80
  const showModal = (data) => {
62
81
  const resolvedData = typeof data === "object" &&
63
82
  "toJSON" in data &&
@@ -178,5 +197,7 @@ export function createMessageComponentInteraction(interaction) {
178
197
  getChannels,
179
198
  getUsers,
180
199
  getMentionables,
200
+ onAck: helpers?.onAck,
201
+ sendFollowUp: helpers?.sendFollowUp,
181
202
  });
182
203
  }
@@ -8,28 +8,24 @@ export type ModalSubmitInteraction = APIModalSubmitInteraction & {
8
8
  reply: (data: InteractionMessageData) => APIInteractionResponseChannelMessageWithSource;
9
9
  deferReply: (options?: DeferReplyOptions) => APIInteractionResponseDeferredChannelMessageWithSource;
10
10
  /**
11
- * Helper method to get the value of a text input component by custom_id.
12
- * @param customId - The custom_id of the text input component
13
- * @returns The value of the text input, or undefined if not found
11
+ * Helper method to get the value of a text input component by its custom ID.
14
12
  */
15
- getTextInputValue: (customId: string) => string | undefined;
13
+ getTextFieldValue: (customId: string) => string | undefined;
16
14
  /**
17
- * Helper method to get all text input values as a map.
18
- * @returns A map of custom_id to value for all text inputs
15
+ * Finalise the interaction response via a webhook follow-up.
16
+ * This is automatically called by reply() if the interaction is deferred.
19
17
  */
20
- getTextInputValues: () => Map<string, string>;
21
- /**
22
- * Helper method to get the selected values of a select menu component by custom_id.
23
- * @param customId - The custom_id of the select menu component
24
- * @returns The selected values of the select menu, or undefined if not found
25
- */
26
- getSelectMenuValues: (customId: string) => string[] | undefined;
18
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
27
19
  };
28
20
  export declare const ModalSubmitInteraction: {};
29
21
  /**
30
- * Wraps a raw modal submit interaction with helper methods.
22
+ * Wraps a raw modal submit interaction with helper methods mirroring Discord's expected responses.
31
23
  *
32
24
  * @param interaction - The raw interaction payload from Discord.
25
+ * @param helpers - Optional callback to capture the final interaction response.
33
26
  * @returns A helper-augmented interaction object.
34
27
  */
35
- export declare function createModalSubmitInteraction(interaction: APIModalSubmitInteraction): ModalSubmitInteraction;
28
+ export declare function createModalSubmitInteraction(interaction: APIModalSubmitInteraction, helpers?: {
29
+ onAck?: (response: APIInteractionResponse) => void;
30
+ sendFollowUp?: (token: string, response: APIInteractionResponse) => void;
31
+ }): ModalSubmitInteraction;
@@ -1,14 +1,16 @@
1
- import { InteractionResponseType, } from "discord-api-types/v10";
1
+ import { ComponentType, InteractionResponseType, } from "discord-api-types/v10";
2
2
  import { normaliseInteractionMessageData, normaliseMessageFlags, } from "./interactionMessageHelpers.js";
3
3
  export const ModalSubmitInteraction = {};
4
4
  /**
5
- * Wraps a raw modal submit interaction with helper methods.
5
+ * Wraps a raw modal submit interaction with helper methods mirroring Discord's expected responses.
6
6
  *
7
7
  * @param interaction - The raw interaction payload from Discord.
8
+ * @param helpers - Optional callback to capture the final interaction response.
8
9
  * @returns A helper-augmented interaction object.
9
10
  */
10
- export function createModalSubmitInteraction(interaction) {
11
+ export function createModalSubmitInteraction(interaction, helpers) {
11
12
  let capturedResponse = null;
13
+ let isDeferred = false;
12
14
  const captureResponse = (response) => {
13
15
  capturedResponse = response;
14
16
  return response;
@@ -16,12 +18,19 @@ export function createModalSubmitInteraction(interaction) {
16
18
  const reply = (data) => {
17
19
  const normalisedData = normaliseInteractionMessageData(data);
18
20
  if (!normalisedData) {
19
- throw new Error("[MiniInteraction] Modal submit replies require response data to be provided.");
21
+ throw new Error("[MiniInteraction] Modal replies require response data to be provided.");
20
22
  }
21
- return captureResponse({
23
+ const response = captureResponse({
22
24
  type: InteractionResponseType.ChannelMessageWithSource,
23
25
  data: normalisedData,
24
26
  });
27
+ if (isDeferred && helpers?.sendFollowUp) {
28
+ helpers.sendFollowUp(interaction.token, response);
29
+ }
30
+ else {
31
+ helpers?.onAck?.(response);
32
+ }
33
+ return response;
25
34
  };
26
35
  const deferReply = (options) => {
27
36
  const flags = normaliseMessageFlags(options?.flags);
@@ -33,67 +42,30 @@ export function createModalSubmitInteraction(interaction) {
33
42
  : {
34
43
  type: InteractionResponseType.DeferredChannelMessageWithSource,
35
44
  };
36
- return captureResponse(response);
45
+ captureResponse(response);
46
+ isDeferred = true;
47
+ helpers?.onAck?.(response);
48
+ return response;
37
49
  };
38
50
  const getResponse = () => capturedResponse;
39
- // Helper to extract text input values from modal components
40
- const extractTextInputs = () => {
41
- const textInputs = new Map();
42
- for (const component of interaction.data.components) {
43
- // Handle action rows
44
- if ("components" in component && Array.isArray(component.components)) {
45
- for (const child of component.components) {
46
- if ("value" in child && "custom_id" in child) {
47
- textInputs.set(child.custom_id, child.value);
51
+ const getTextFieldValue = (customId) => {
52
+ for (const actionRow of interaction.data.components) {
53
+ if ("components" in actionRow && Array.isArray(actionRow.components)) {
54
+ for (const component of actionRow.components) {
55
+ if (component.type === ComponentType.TextInput &&
56
+ component.custom_id === customId) {
57
+ return component.value;
48
58
  }
49
59
  }
50
60
  }
51
- // Handle labeled components
52
- else if ("component" in component) {
53
- const labeledComponent = component.component;
54
- if ("value" in labeledComponent && "custom_id" in labeledComponent) {
55
- textInputs.set(labeledComponent.custom_id, labeledComponent.value);
56
- }
57
- }
58
61
  }
59
- return textInputs;
60
- };
61
- // Helper to extract select menu values from modal components
62
- const extractSelectMenuValues = () => {
63
- const selectMenuValues = new Map();
64
- for (const component of interaction.data.components) {
65
- // Handle action rows
66
- if ("components" in component && Array.isArray(component.components)) {
67
- for (const child of component.components) {
68
- if ("values" in child && "custom_id" in child && Array.isArray(child.values)) {
69
- selectMenuValues.set(child.custom_id, child.values);
70
- }
71
- }
72
- }
73
- // Handle labeled components (unlikely for select menus but good for completeness if spec allows)
74
- else if ("component" in component) {
75
- const labeledComponent = component.component; // Using any as ModalSubmitComponent might not cover select menus fully in types yet or strictness varies
76
- if ("values" in labeledComponent && "custom_id" in labeledComponent && Array.isArray(labeledComponent.values)) {
77
- selectMenuValues.set(labeledComponent.custom_id, labeledComponent.values);
78
- }
79
- }
80
- }
81
- return selectMenuValues;
82
- };
83
- const textInputValues = extractTextInputs();
84
- const selectMenuValues = extractSelectMenuValues();
85
- const getTextInputValue = (customId) => {
86
- return textInputValues.get(customId);
87
- };
88
- const getTextInputValues = () => {
89
- return new Map(textInputValues);
62
+ return undefined;
90
63
  };
91
64
  return Object.assign(interaction, {
92
65
  reply,
93
66
  deferReply,
94
67
  getResponse,
95
- getTextInputValue,
96
- getTextInputValues,
97
- getSelectMenuValues: (customId) => selectMenuValues.get(customId),
68
+ getTextFieldValue,
69
+ sendFollowUp: helpers?.sendFollowUp,
98
70
  });
99
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Mini interaction, connecting your app with Discord via HTTP-interaction (Vercel support).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",