@minesa-org/mini-interaction 0.2.23 → 0.2.25

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 = {
@@ -171,6 +190,8 @@ export declare class MiniInteraction {
171
190
  private loadComponentsPromise;
172
191
  private registerCommandsPromise;
173
192
  private registerCommandsSignature;
193
+ private vercelWaitUntil;
194
+ private searchedForVercel;
174
195
  /**
175
196
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
176
197
  */
@@ -189,6 +210,10 @@ export declare class MiniInteraction {
189
210
  * Call this periodically to clean up old interaction data.
190
211
  */
191
212
  cleanupExpiredInteractions(): number;
213
+ /**
214
+ * Attempt to find a waitUntil implementation in the environment (e.g. Vercel or Cloudflare).
215
+ */
216
+ private getAutoWaitUntil;
192
217
  private normalizeCommandData;
193
218
  private registerCommand;
194
219
  /**
@@ -267,7 +292,9 @@ export declare class MiniInteraction {
267
292
  * Creates a Node.js style request handler compatible with Express, Next.js API routes,
268
293
  * Vercel serverless functions, and any runtime that expects a `(req, res)` listener.
269
294
  */
270
- createNodeHandler(): InteractionNodeHandler;
295
+ createNodeHandler(options?: {
296
+ waitUntil?: (promise: Promise<void>) => void;
297
+ }): InteractionNodeHandler;
271
298
  /**
272
299
  * Generates a lightweight verification handler that serves an HTML page with an embedded OAuth link.
273
300
  *
@@ -321,7 +348,13 @@ export declare class MiniInteraction {
321
348
  /**
322
349
  * Creates a Fetch API compatible handler for runtimes like Workers or Deno.
323
350
  */
324
- createFetchHandler(): InteractionFetchHandler;
351
+ /**
352
+ * Generates a Fetch-standard request handler compatible with Cloudflare Workers,
353
+ * Bun, Deno, and Next.js Edge Runtime.
354
+ */
355
+ createFetchHandler(options?: {
356
+ waitUntil?: (promise: Promise<void>) => void;
357
+ }): InteractionFetchHandler;
325
358
  /**
326
359
  * Checks if the provided directory path exists on disk.
327
360
  */
@@ -43,6 +43,8 @@ export class MiniInteraction {
43
43
  loadComponentsPromise = null;
44
44
  registerCommandsPromise = null;
45
45
  registerCommandsSignature = null;
46
+ vercelWaitUntil = null;
47
+ searchedForVercel = false;
46
48
  /**
47
49
  * Creates a new MiniInteraction client with optional command auto-loading and custom runtime hooks.
48
50
  */
@@ -129,6 +131,33 @@ export class MiniInteraction {
129
131
  }
130
132
  return cleaned;
131
133
  }
134
+ /**
135
+ * Attempt to find a waitUntil implementation in the environment (e.g. Vercel or Cloudflare).
136
+ */
137
+ async getAutoWaitUntil() {
138
+ if (this.searchedForVercel)
139
+ return this.vercelWaitUntil;
140
+ this.searchedForVercel = true;
141
+ // Try Vercel's @vercel/functions
142
+ try {
143
+ // @ts-ignore - Dynamic import to avoid hard dependency or build errors in non-vercel environments
144
+ const { waitUntil } = await import("@vercel/functions");
145
+ if (typeof waitUntil === "function") {
146
+ this.vercelWaitUntil = waitUntil;
147
+ return waitUntil;
148
+ }
149
+ }
150
+ catch {
151
+ // Ignore if not found
152
+ }
153
+ // Try globalThis.waitUntil (some edge runtimes)
154
+ // @ts-ignore
155
+ if (typeof globalThis.waitUntil === "function") {
156
+ // @ts-ignore
157
+ return globalThis.waitUntil;
158
+ }
159
+ return null;
160
+ }
132
161
  normalizeCommandData(data) {
133
162
  if (typeof data === "object" && data !== null) {
134
163
  return resolveJSONEncodable(data);
@@ -471,7 +500,8 @@ export class MiniInteraction {
471
500
  * Creates a Node.js style request handler compatible with Express, Next.js API routes,
472
501
  * Vercel serverless functions, and any runtime that expects a `(req, res)` listener.
473
502
  */
474
- createNodeHandler() {
503
+ createNodeHandler(options) {
504
+ const { waitUntil } = options ?? {};
475
505
  return (request, response) => {
476
506
  if (request.method !== "POST") {
477
507
  response.statusCode = 405;
@@ -508,6 +538,12 @@ export class MiniInteraction {
508
538
  signature,
509
539
  timestamp,
510
540
  });
541
+ if (result.backgroundWork) {
542
+ const resolvedWaitUntil = waitUntil ?? await this.getAutoWaitUntil();
543
+ if (resolvedWaitUntil) {
544
+ resolvedWaitUntil(result.backgroundWork);
545
+ }
546
+ }
511
547
  response.statusCode = result.status;
512
548
  response.setHeader("content-type", "application/json");
513
549
  response.end(JSON.stringify(result.body));
@@ -756,7 +792,12 @@ export class MiniInteraction {
756
792
  /**
757
793
  * Creates a Fetch API compatible handler for runtimes like Workers or Deno.
758
794
  */
759
- createFetchHandler() {
795
+ /**
796
+ * Generates a Fetch-standard request handler compatible with Cloudflare Workers,
797
+ * Bun, Deno, and Next.js Edge Runtime.
798
+ */
799
+ createFetchHandler(options) {
800
+ const { waitUntil } = options ?? {};
760
801
  return async (request) => {
761
802
  if (request.method !== "POST") {
762
803
  return new Response(JSON.stringify({
@@ -776,6 +817,12 @@ export class MiniInteraction {
776
817
  signature,
777
818
  timestamp,
778
819
  });
820
+ if (result.backgroundWork) {
821
+ const resolvedWaitUntil = waitUntil ?? await this.getAutoWaitUntil();
822
+ if (resolvedWaitUntil) {
823
+ resolvedWaitUntil(result.backgroundWork);
824
+ }
825
+ }
779
826
  return new Response(JSON.stringify(result.body), {
780
827
  status: result.status,
781
828
  headers: { "content-type": "application/json" },
@@ -1142,10 +1189,11 @@ export class MiniInteraction {
1142
1189
  }
1143
1190
  return resolvedResponse;
1144
1191
  }, this.timeoutConfig.initialResponseTimeout, `Component "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1145
- const resolvedResponse = await timeoutWrapper();
1192
+ const { response: resolvedResponse, backgroundWork } = await timeoutWrapper();
1146
1193
  return {
1147
1194
  status: 200,
1148
1195
  body: resolvedResponse,
1196
+ backgroundWork,
1149
1197
  };
1150
1198
  }
1151
1199
  catch (error) {
@@ -1208,10 +1256,11 @@ export class MiniInteraction {
1208
1256
  }
1209
1257
  return resolvedResponse;
1210
1258
  }, this.timeoutConfig.initialResponseTimeout, `Modal "${customId}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1211
- const resolvedResponse = await timeoutWrapper();
1259
+ const { response: resolvedResponse, backgroundWork } = await timeoutWrapper();
1212
1260
  return {
1213
1261
  status: 200,
1214
1262
  body: resolvedResponse,
1263
+ backgroundWork,
1215
1264
  };
1216
1265
  }
1217
1266
  catch (error) {
@@ -1324,7 +1373,7 @@ export class MiniInteraction {
1324
1373
  }
1325
1374
  return resolvedResponse;
1326
1375
  }, this.timeoutConfig.initialResponseTimeout, `Command "${commandName}"`, this.timeoutConfig.enableTimeoutWarnings, ackPromise);
1327
- const finalResponse = await timeoutWrapper();
1376
+ const { response: finalResponse, backgroundWork } = await timeoutWrapper();
1328
1377
  if (this.timeoutConfig.enableResponseDebugLogging) {
1329
1378
  console.log(`[MiniInteraction] handleApplicationCommand: initial response determined (type=${finalResponse?.type})`);
1330
1379
  }
@@ -1344,6 +1393,7 @@ export class MiniInteraction {
1344
1393
  return {
1345
1394
  status: 200,
1346
1395
  body: finalResponse,
1396
+ backgroundWork,
1347
1397
  };
1348
1398
  }
1349
1399
  catch (error) {
@@ -1607,12 +1657,16 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1607
1657
  });
1608
1658
  // Start handler execution immediately (don't await yet)
1609
1659
  const handlerPromise = Promise.resolve(handler(...args));
1660
+ // Attach a default error handler to prevent unhandled rejections
1661
+ const backgroundWork = handlerPromise.catch((error) => {
1662
+ console.error(`[MiniInteraction] ${handlerName} background execution failed:`, error instanceof Error ? error.message : String(error));
1663
+ }).then(() => {
1664
+ // Ensure it always resolves to void
1665
+ });
1610
1666
  // If we have an ackPromise, race between ACK and timeout
1611
- // When ACK is received, return IMMEDIATELY so Discord gets the response
1612
- // Handler continues in background
1613
1667
  if (ackPromise) {
1614
1668
  try {
1615
- const ackResult = await Promise.race([
1669
+ const response = await Promise.race([
1616
1670
  ackPromise,
1617
1671
  timeoutPromise,
1618
1672
  ]);
@@ -1620,11 +1674,7 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1620
1674
  if (timeoutId) {
1621
1675
  clearTimeout(timeoutId);
1622
1676
  }
1623
- // Handler continues in background - attach error handler
1624
- handlerPromise.catch((error) => {
1625
- console.error(`[MiniInteraction] ${handlerName} background execution failed:`, error instanceof Error ? error.message : String(error));
1626
- });
1627
- return ackResult;
1677
+ return { response, backgroundWork };
1628
1678
  }
1629
1679
  catch (error) {
1630
1680
  // Timeout occurred before ACK - fall through to check handler
@@ -1636,7 +1686,7 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1636
1686
  }
1637
1687
  // No ACK promise - wait for handler with timeout
1638
1688
  try {
1639
- const handlerResult = await Promise.race([
1689
+ const response = await Promise.race([
1640
1690
  handlerPromise,
1641
1691
  timeoutPromise,
1642
1692
  ]);
@@ -1647,7 +1697,7 @@ function createTimeoutWrapper(handler, timeoutMs, handlerName, enableWarnings =
1647
1697
  if (enableWarnings && elapsed > timeoutMs * 0.8) {
1648
1698
  console.warn(`[MiniInteraction] ${handlerName} completed in ${elapsed}ms (${Math.round((elapsed / timeoutMs) * 100)}% of timeout limit)`);
1649
1699
  }
1650
- return handlerResult;
1700
+ return { response, backgroundWork };
1651
1701
  }
1652
1702
  catch (error) {
1653
1703
  if (timeoutId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minesa-org/mini-interaction",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
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",