@minesa-org/mini-interaction 0.2.22 → 0.2.24

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.
@@ -37,6 +37,25 @@ export type InteractionHandlerResult = {
37
37
  body: APIInteractionResponse | {
38
38
  error: string;
39
39
  };
40
+ /**
41
+ * Promise that resolves when all background work (like editReply) completes.
42
+ * Pass this to Vercel's waitUntil() to prevent premature termination.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * // In Next.js App Router
47
+ * import { waitUntil } from '@vercel/functions';
48
+ *
49
+ * export async function POST(request: Request) {
50
+ * const result = await client.handleRequest({ ... });
51
+ * if (result.backgroundWork) {
52
+ * waitUntil(result.backgroundWork);
53
+ * }
54
+ * return Response.json(result.body, { status: result.status });
55
+ * }
56
+ * ```
57
+ */
58
+ backgroundWork?: Promise<void>;
40
59
  };
41
60
  /** Configuration for interaction timeout handling. */
42
61
  export type InteractionTimeoutConfig = {
@@ -1142,10 +1142,11 @@ export class MiniInteraction {
1142
1142
  }
1143
1143
  return resolvedResponse;
1144
1144
  }, this.timeoutConfig.initialResponseTimeout, `Component "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1145
- const resolvedResponse = await timeoutWrapper();
1145
+ const { response: resolvedResponse, backgroundWork } = await timeoutWrapper();
1146
1146
  return {
1147
1147
  status: 200,
1148
1148
  body: resolvedResponse,
1149
+ backgroundWork,
1149
1150
  };
1150
1151
  }
1151
1152
  catch (error) {
@@ -1208,10 +1209,11 @@ export class MiniInteraction {
1208
1209
  }
1209
1210
  return resolvedResponse;
1210
1211
  }, this.timeoutConfig.initialResponseTimeout, `Modal "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1211
- const resolvedResponse = await timeoutWrapper();
1212
+ const { response: resolvedResponse, backgroundWork } = await timeoutWrapper();
1212
1213
  return {
1213
1214
  status: 200,
1214
1215
  body: resolvedResponse,
1216
+ backgroundWork,
1215
1217
  };
1216
1218
  }
1217
1219
  catch (error) {
@@ -1324,7 +1326,7 @@ export class MiniInteraction {
1324
1326
  }
1325
1327
  return resolvedResponse;
1326
1328
  }, this.timeoutConfig.initialResponseTimeout, `Command "${commandName}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1327
- const finalResponse = await timeoutWrapper();
1329
+ const { response: finalResponse, backgroundWork } = await timeoutWrapper();
1328
1330
  if (this.timeoutConfig.enableResponseDebugLogging) {
1329
1331
  console.log(`[MiniInteraction] handleApplicationCommand: initial response determined (type=${finalResponse?.type})`);
1330
1332
  }
@@ -1344,6 +1346,7 @@ export class MiniInteraction {
1344
1346
  return {
1345
1347
  status: 200,
1346
1348
  body: finalResponse,
1349
+ backgroundWork,
1347
1350
  };
1348
1351
  }
1349
1352
  catch (error) {
@@ -1588,13 +1591,16 @@ function resolveOAuthConfig(provided) {
1588
1591
  }
1589
1592
  /**
1590
1593
  * Wraps a handler function with timeout detection and error handling.
1594
+ *
1595
+ * CRITICAL FOR HTTP INTERACTIONS:
1596
+ * When deferReply() is called, we MUST return the ACK to Discord immediately.
1597
+ * The handler continues executing and sends follow-up via webhook.
1598
+ * The ACK must reach Discord before any webhook PATCH requests can succeed.
1591
1599
  */
1592
1600
  function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings = true, ackPromise) {
1593
1601
  return async (...args) => {
1594
1602
  const startTime = Date.now();
1595
1603
  let timeoutId;
1596
- let ackResult = null;
1597
- let ackReceived = false;
1598
1604
  const timeoutPromise = new Promise((_, reject) => {
1599
1605
  timeoutId = setTimeout(() => {
1600
1606
  const elapsed = Date.now() - startTime;
@@ -1602,55 +1608,54 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1602
1608
  reject(new Error(`Handler timeout: ${handlerName} exceeded ${timeoutMs}ms limit`));
1603
1609
  }, timeoutMs);
1604
1610
  });
1605
- // If we have an ackPromise, listen for it but DON'T return early
1606
- // Instead, capture the ACK result and continue waiting for handler
1611
+ // Start handler execution immediately (don't await yet)
1612
+ const handlerPromise = Promise.resolve(handler(...args));
1613
+ // Attach a default error handler to prevent unhandled rejections
1614
+ const backgroundWork = handlerPromise.catch((error) => {
1615
+ console.error(`[MiniInteraction] ${handlerName} background execution failed:`, error instanceof Error ? error.message : String(error));
1616
+ }).then(() => {
1617
+ // Ensure it always resolves to void
1618
+ });
1619
+ // If we have an ackPromise, race between ACK and timeout
1607
1620
  if (ackPromise) {
1608
- ackPromise.then((result) => {
1609
- ackResult = result;
1610
- ackReceived = true;
1611
- // Clear the timeout once we have an ACK - handler can now take longer
1621
+ try {
1622
+ const response = await Promise.race([
1623
+ ackPromise,
1624
+ timeoutPromise,
1625
+ ]);
1626
+ // ACK received! Clear timeout and return immediately
1612
1627
  if (timeoutId) {
1613
1628
  clearTimeout(timeoutId);
1614
- timeoutId = undefined;
1615
1629
  }
1616
- }).catch(() => {
1617
- // ACK promise rejection is fine, we'll use handler result
1618
- });
1630
+ return { response, backgroundWork };
1631
+ }
1632
+ catch (error) {
1633
+ // Timeout occurred before ACK - fall through to check handler
1634
+ if (timeoutId) {
1635
+ clearTimeout(timeoutId);
1636
+ }
1637
+ throw error;
1638
+ }
1619
1639
  }
1640
+ // No ACK promise - wait for handler with timeout
1620
1641
  try {
1621
- // ALWAYS wait for handler to complete
1622
- const handlerResult = await Promise.race([
1623
- handler(...args),
1642
+ const response = await Promise.race([
1643
+ handlerPromise,
1624
1644
  timeoutPromise,
1625
1645
  ]);
1626
1646
  if (timeoutId) {
1627
1647
  clearTimeout(timeoutId);
1628
1648
  }
1629
1649
  const elapsed = Date.now() - startTime;
1630
- if (enableWarnings && elapsed > timeoutMs * 0.8 && !ackReceived) {
1650
+ if (enableWarnings && elapsed > timeoutMs * 0.8) {
1631
1651
  console.warn(`[MiniInteraction] ${handlerName} completed in ${elapsed}ms (${Math.round((elapsed / timeoutMs) * 100)}% of timeout limit)`);
1632
1652
  }
1633
- // If we got an ACK, return that for the HTTP response
1634
- // The handler has completed at this point so all background work is done
1635
- if (ackReceived && ackResult !== null) {
1636
- return ackResult;
1637
- }
1638
- return handlerResult;
1653
+ return { response, backgroundWork };
1639
1654
  }
1640
1655
  catch (error) {
1641
1656
  if (timeoutId) {
1642
1657
  clearTimeout(timeoutId);
1643
1658
  }
1644
- // If handler timed out but we have an ACK, return the ACK
1645
- // This allows deferReply to work even if later operations are slow
1646
- if (error instanceof Error && error.message.includes("Handler timeout")) {
1647
- if (ackReceived && ackResult !== null) {
1648
- console.warn(`[MiniInteraction] ${handlerName} timed out but ACK was already captured. ` +
1649
- `Background work may not complete in serverless environments.`);
1650
- return ackResult;
1651
- }
1652
- throw error;
1653
- }
1654
1659
  console.error(`[MiniInteraction] ${handlerName} failed:`, error);
1655
1660
  throw error;
1656
1661
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
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",