@minesa-org/mini-interaction 0.4.8 → 0.4.10
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/compat/MiniInteraction.d.ts +2 -0
- package/dist/compat/MiniInteraction.js +63 -19
- package/dist/core/http/DiscordRestClient.d.ts +1 -0
- package/dist/core/http/DiscordRestClient.js +30 -11
- package/dist/core/interactions/__tests__/interaction-context.test.js +21 -0
- package/package.json +1 -1
|
@@ -80,6 +80,8 @@ export declare class MiniInteraction {
|
|
|
80
80
|
private normalizeModuleExports;
|
|
81
81
|
private normalizeExportValue;
|
|
82
82
|
private walkFiles;
|
|
83
|
+
private getDefaultInitialResponse;
|
|
84
|
+
private isDeferredResponse;
|
|
83
85
|
private isImportableModule;
|
|
84
86
|
private isInteractionCommand;
|
|
85
87
|
private getCommandName;
|
|
@@ -31,6 +31,14 @@ export class MiniInteraction {
|
|
|
31
31
|
}
|
|
32
32
|
createNodeHandler() {
|
|
33
33
|
return async (req, res) => {
|
|
34
|
+
let responseSent = false;
|
|
35
|
+
const commitInitialResponse = (response) => {
|
|
36
|
+
if (responseSent)
|
|
37
|
+
return false;
|
|
38
|
+
this.sendJson(res, 200, response);
|
|
39
|
+
responseSent = true;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
34
42
|
try {
|
|
35
43
|
const body = await this.readRawBody(req);
|
|
36
44
|
const signature = this.getHeader(req.headers, "x-signature-ed25519");
|
|
@@ -58,17 +66,20 @@ export class MiniInteraction {
|
|
|
58
66
|
this.sendJson(res, 200, { type: InteractionResponseType.Pong });
|
|
59
67
|
return;
|
|
60
68
|
}
|
|
61
|
-
const response = await this.dispatch(interaction);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
const response = await this.dispatch(interaction, commitInitialResponse);
|
|
70
|
+
if (!responseSent) {
|
|
71
|
+
this.sendJson(res, 200, response ?? this.getDefaultInitialResponse(interaction));
|
|
72
|
+
responseSent = true;
|
|
73
|
+
}
|
|
65
74
|
}
|
|
66
75
|
catch (error) {
|
|
67
76
|
const message = error instanceof Error ? error.message : "[MiniInteraction] Unknown error";
|
|
68
77
|
if (this.options.debug) {
|
|
69
78
|
console.error("[MiniInteraction] createNodeHandler failed", error);
|
|
70
79
|
}
|
|
71
|
-
|
|
80
|
+
if (!responseSent) {
|
|
81
|
+
this.sendJson(res, 500, { error: message });
|
|
82
|
+
}
|
|
72
83
|
}
|
|
73
84
|
};
|
|
74
85
|
}
|
|
@@ -156,29 +167,29 @@ export class MiniInteraction {
|
|
|
156
167
|
}
|
|
157
168
|
};
|
|
158
169
|
}
|
|
159
|
-
async dispatch(interaction) {
|
|
170
|
+
async dispatch(interaction, commitInitialResponse) {
|
|
160
171
|
const modules = await this.loadModules();
|
|
161
172
|
if (interaction.type === InteractionType.ApplicationCommand) {
|
|
162
173
|
const command = modules.commands.find((candidate) => this.getCommandName(candidate) === interaction.data.name);
|
|
163
174
|
if (!command)
|
|
164
175
|
return undefined;
|
|
165
|
-
return this.executeCommandHandler(command.handler, interaction);
|
|
176
|
+
return this.executeCommandHandler(command.handler, interaction, commitInitialResponse);
|
|
166
177
|
}
|
|
167
178
|
if (interaction.type === InteractionType.MessageComponent) {
|
|
168
179
|
const component = modules.components.find((candidate) => candidate.customId === interaction.data.custom_id);
|
|
169
180
|
if (!component)
|
|
170
181
|
return undefined;
|
|
171
|
-
return this.executeComponentHandler(component.handler, interaction);
|
|
182
|
+
return this.executeComponentHandler(component.handler, interaction, commitInitialResponse);
|
|
172
183
|
}
|
|
173
184
|
if (interaction.type === InteractionType.ModalSubmit) {
|
|
174
185
|
const modal = modules.modals.find((candidate) => candidate.customId === interaction.data.custom_id);
|
|
175
186
|
if (!modal)
|
|
176
187
|
return undefined;
|
|
177
|
-
return this.executeModalHandler(modal.handler, interaction);
|
|
188
|
+
return this.executeModalHandler(modal.handler, interaction, commitInitialResponse);
|
|
178
189
|
}
|
|
179
190
|
return undefined;
|
|
180
191
|
}
|
|
181
|
-
async executeCommandHandler(handler, interaction) {
|
|
192
|
+
async executeCommandHandler(handler, interaction, commitInitialResponse) {
|
|
182
193
|
return this.runWithResponseLifecycle(interaction, async (helpers) => {
|
|
183
194
|
switch (interaction.data.type) {
|
|
184
195
|
case ApplicationCommandType.ChatInput:
|
|
@@ -193,17 +204,19 @@ export class MiniInteraction {
|
|
|
193
204
|
}
|
|
194
205
|
throw new Error(`[MiniInteraction] Unsupported application command type: ${String(interaction.data.type)}`);
|
|
195
206
|
}
|
|
196
|
-
});
|
|
207
|
+
}, commitInitialResponse);
|
|
197
208
|
}
|
|
198
|
-
async executeComponentHandler(handler, interaction) {
|
|
199
|
-
return this.runWithResponseLifecycle(interaction, async (helpers) => handler(createMessageComponentInteraction(interaction, helpers)));
|
|
209
|
+
async executeComponentHandler(handler, interaction, commitInitialResponse) {
|
|
210
|
+
return this.runWithResponseLifecycle(interaction, async (helpers) => handler(createMessageComponentInteraction(interaction, helpers)), commitInitialResponse);
|
|
200
211
|
}
|
|
201
|
-
async executeModalHandler(handler, interaction) {
|
|
202
|
-
return this.runWithResponseLifecycle(interaction, async (helpers) => handler(createModalSubmitInteraction(interaction, helpers)));
|
|
212
|
+
async executeModalHandler(handler, interaction, commitInitialResponse) {
|
|
213
|
+
return this.runWithResponseLifecycle(interaction, async (helpers) => handler(createModalSubmitInteraction(interaction, helpers)), commitInitialResponse);
|
|
203
214
|
}
|
|
204
|
-
async runWithResponseLifecycle(interaction, executor) {
|
|
215
|
+
async runWithResponseLifecycle(interaction, executor, commitInitialResponse) {
|
|
205
216
|
let ackResponse;
|
|
206
217
|
let initialResponseCommitted = false;
|
|
218
|
+
let followUpSent = false;
|
|
219
|
+
let committedInitialResponse;
|
|
207
220
|
const helpers = {
|
|
208
221
|
// Legacy helper contracts use canRespond for both initial acknowledgements
|
|
209
222
|
// and later editReply/followUp calls. The compat layer does not currently
|
|
@@ -215,6 +228,10 @@ export class MiniInteraction {
|
|
|
215
228
|
},
|
|
216
229
|
onAck: (response) => {
|
|
217
230
|
ackResponse = response;
|
|
231
|
+
if (!initialResponseCommitted && commitInitialResponse?.(response)) {
|
|
232
|
+
initialResponseCommitted = true;
|
|
233
|
+
committedInitialResponse = response;
|
|
234
|
+
}
|
|
218
235
|
},
|
|
219
236
|
sendFollowUp: async (token, response, messageId) => {
|
|
220
237
|
// If the initial interaction response has not been sent yet, collapse the
|
|
@@ -228,9 +245,11 @@ export class MiniInteraction {
|
|
|
228
245
|
const responseData = "data" in response ? response.data ?? {} : {};
|
|
229
246
|
if (messageId === "@original") {
|
|
230
247
|
await this.rest.editOriginal(token, responseData);
|
|
248
|
+
followUpSent = true;
|
|
231
249
|
return;
|
|
232
250
|
}
|
|
233
251
|
await this.rest.createFollowup(token, responseData);
|
|
252
|
+
followUpSent = true;
|
|
234
253
|
},
|
|
235
254
|
};
|
|
236
255
|
const autoDeferMs = Math.min(2500, this.options.timeoutConfig?.initialResponseTimeout ?? 2500);
|
|
@@ -241,10 +260,13 @@ export class MiniInteraction {
|
|
|
241
260
|
if (this.options.debug || this.options.timeoutConfig?.enableResponseDebugLogging) {
|
|
242
261
|
console.warn(`[MiniInteraction] Auto-deferred interaction ${interaction.id} after ${autoDeferMs}ms.`);
|
|
243
262
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
};
|
|
263
|
+
const deferredResponse = this.getDefaultInitialResponse(interaction);
|
|
264
|
+
ackResponse = deferredResponse;
|
|
247
265
|
helpers.trackResponse(interaction.id, interaction.token, "deferred");
|
|
266
|
+
if (!initialResponseCommitted && commitInitialResponse?.(deferredResponse)) {
|
|
267
|
+
initialResponseCommitted = true;
|
|
268
|
+
committedInitialResponse = deferredResponse;
|
|
269
|
+
}
|
|
248
270
|
}, autoDeferMs)
|
|
249
271
|
: undefined;
|
|
250
272
|
const timeoutWarningMs = this.options.timeoutConfig?.initialResponseTimeout;
|
|
@@ -260,6 +282,18 @@ export class MiniInteraction {
|
|
|
260
282
|
if (this.options.debug || this.options.timeoutConfig?.enableResponseDebugLogging) {
|
|
261
283
|
console.debug(`[MiniInteraction] Interaction ${interaction.id} completed with ${result ? "explicit" : "fallback"} response.`);
|
|
262
284
|
}
|
|
285
|
+
if (initialResponseCommitted &&
|
|
286
|
+
result &&
|
|
287
|
+
!followUpSent &&
|
|
288
|
+
committedInitialResponse &&
|
|
289
|
+
this.isDeferredResponse(committedInitialResponse) &&
|
|
290
|
+
!this.isDeferredResponse(result)) {
|
|
291
|
+
const responseData = "data" in result ? result.data ?? {} : {};
|
|
292
|
+
await this.rest.editOriginal(interaction.token, responseData);
|
|
293
|
+
}
|
|
294
|
+
if (initialResponseCommitted) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
263
297
|
initialResponseCommitted = true;
|
|
264
298
|
return result ?? ackResponse;
|
|
265
299
|
}
|
|
@@ -351,6 +385,16 @@ export class MiniInteraction {
|
|
|
351
385
|
}));
|
|
352
386
|
return results.flat();
|
|
353
387
|
}
|
|
388
|
+
getDefaultInitialResponse(interaction) {
|
|
389
|
+
if (interaction.type === InteractionType.MessageComponent) {
|
|
390
|
+
return { type: InteractionResponseType.DeferredMessageUpdate };
|
|
391
|
+
}
|
|
392
|
+
return { type: InteractionResponseType.DeferredChannelMessageWithSource };
|
|
393
|
+
}
|
|
394
|
+
isDeferredResponse(response) {
|
|
395
|
+
return (response.type === InteractionResponseType.DeferredChannelMessageWithSource ||
|
|
396
|
+
response.type === InteractionResponseType.DeferredMessageUpdate);
|
|
397
|
+
}
|
|
354
398
|
isImportableModule(filePath) {
|
|
355
399
|
if (filePath.endsWith(".d.ts"))
|
|
356
400
|
return false;
|
|
@@ -15,6 +15,7 @@ export declare class DiscordRestClient {
|
|
|
15
15
|
request<T>(path: string, init?: RequestInit & {
|
|
16
16
|
authenticated?: boolean;
|
|
17
17
|
}): Promise<T>;
|
|
18
|
+
private createRequestError;
|
|
18
19
|
createFollowup(interactionToken: string, body: unknown): Promise<unknown>;
|
|
19
20
|
editOriginal(interactionToken: string, body: unknown): Promise<unknown>;
|
|
20
21
|
}
|
|
@@ -14,18 +14,33 @@ export class DiscordRestClient {
|
|
|
14
14
|
let lastError;
|
|
15
15
|
const { authenticated = true, ...requestInit } = init;
|
|
16
16
|
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
...
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
20
|
+
...requestInit,
|
|
21
|
+
headers: {
|
|
22
|
+
...(authenticated ? { Authorization: `Bot ${this.options.token}` } : {}),
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
...(requestInit.headers ?? {}),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
lastError = this.createRequestError(path, requestInit.method, error);
|
|
30
|
+
if (attempt < this.maxRetries) {
|
|
31
|
+
await sleep(150 * (attempt + 1));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
25
36
|
if (response.status === 429) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
if (attempt < this.maxRetries) {
|
|
38
|
+
const retryAfter = Number(response.headers.get('retry-after') ?? '1');
|
|
39
|
+
await sleep(Math.ceil(retryAfter * 1000));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
lastError = new Error(`[DiscordRestClient] ${requestInit.method ?? 'GET'} ${path} failed: 429`);
|
|
43
|
+
break;
|
|
29
44
|
}
|
|
30
45
|
if (response.ok) {
|
|
31
46
|
if (response.status === 204)
|
|
@@ -41,6 +56,10 @@ export class DiscordRestClient {
|
|
|
41
56
|
}
|
|
42
57
|
throw lastError instanceof Error ? lastError : new Error('[DiscordRestClient] unknown request failure');
|
|
43
58
|
}
|
|
59
|
+
createRequestError(path, method, error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
return new Error(`[DiscordRestClient] ${method ?? 'GET'} ${path} failed: ${message}`, { cause: error instanceof Error ? error : undefined });
|
|
62
|
+
}
|
|
44
63
|
createFollowup(interactionToken, body) {
|
|
45
64
|
return this.request(`/webhooks/${this.options.applicationId}/${interactionToken}`, {
|
|
46
65
|
method: 'POST',
|
|
@@ -29,6 +29,27 @@ test('editReply and followUp call webhook endpoints', async () => {
|
|
|
29
29
|
assert.equal(calls.length, 2);
|
|
30
30
|
assert.match(calls[0], /messages\/\@original/);
|
|
31
31
|
});
|
|
32
|
+
test('editReply retries transient transport failures', async () => {
|
|
33
|
+
let attempts = 0;
|
|
34
|
+
const fetchImpl = (async () => {
|
|
35
|
+
attempts += 1;
|
|
36
|
+
if (attempts === 1) {
|
|
37
|
+
throw new TypeError('fetch failed', {
|
|
38
|
+
cause: { code: 'UND_ERR_SOCKET' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
42
|
+
});
|
|
43
|
+
const rest = new DiscordRestClient({
|
|
44
|
+
token: 'x',
|
|
45
|
+
applicationId: 'app',
|
|
46
|
+
fetchImplementation: fetchImpl,
|
|
47
|
+
maxRetries: 1,
|
|
48
|
+
});
|
|
49
|
+
const ctx = new InteractionContext({ interaction, rest });
|
|
50
|
+
await ctx.editReply({ content: 'edit' });
|
|
51
|
+
assert.equal(attempts, 2);
|
|
52
|
+
});
|
|
32
53
|
test('auto-ack diagnostics callback fires for slow handler', async () => {
|
|
33
54
|
let message = '';
|
|
34
55
|
const { rest } = createRest();
|
package/package.json
CHANGED