Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docs/02-listen-and-respond.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,24 @@ console.log(message.body.mid);
> });
> ```

### Отмена отправки
Методы `sendMessageToChat` и `sendMessageToUser` поддерживают `AbortSignal` для отмены запроса. Это полезно, когда сервер долго обрабатывает вложение (`attachment.not.ready`) и вы хотите прервать ожидание:
```typescript
const controller = new AbortController();

// Отменить через 30 секунд
setTimeout(() => controller.abort(), 30_000);

await bot.api.sendMessageToChat(54321, 'Текст', {
attachments: [image.toJson()],
signal: controller.signal,
});
```

> ℹ️ При получении ошибки `attachment.not.ready` SDK автоматически повторяет
> запрос до 10 раз с экспоненциальной задержкой (1 с, 2 с, 4 с, ...).
> Если передан `signal`, повторы прекращаются при его отмене.

Или воспользоваться методом контекста `reply`:
```typescript
bot.hears('ping', async (ctx) => {
Expand Down
10 changes: 6 additions & 4 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ export class Api {
text: string,
extra?: SendMessageExtra,
) => {
const { signal, ...rest } = extra ?? {};
const { message } = await this.raw.messages.send({
chat_id: chatId,
text,
...extra,
});
...rest,
}, { signal });
return message;
};

Expand All @@ -84,11 +85,12 @@ export class Api {
text: string,
extra?: SendMessageExtra,
) => {
const { signal, ...rest } = extra ?? {};
const { message } = await this.raw.messages.send({
user_id: userId,
text,
...extra,
});
...rest,
}, { signal });
return message;
};

Expand Down
2 changes: 1 addition & 1 deletion src/core/network/api/base-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { ApiMethods } from './modules/types';
type ApiCallFn<HTTPMethod extends keyof ApiMethods> = <Method extends keyof ApiMethods[HTTPMethod]>(
method: Method,
// @ts-ignore
options: ApiMethods[HTTPMethod][Method]['req']
options: ApiMethods[HTTPMethod][Method]['req'] & { signal?: AbortSignal }
// @ts-ignore
) => Promise<ApiMethods[HTTPMethod][Method]['res']>;

Expand Down
6 changes: 5 additions & 1 deletion src/core/network/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type ReqOptions = {
method?: HTTPMethod;
body?: object | null,
query?: Record<string, string | number | boolean | null | undefined>,
path?: Record<string, string | number>
path?: Record<string, string | number>,
signal?: AbortSignal,
};

type CallOptions = {
Expand Down Expand Up @@ -49,6 +50,9 @@ export const createClient = (token: string, options: ClientOptions = {}) => {

const init: RequestInit = { ...getResponseInit(callOptions?.body), method: httpMethod };
init.headers = { ...init.headers, Authorization: token };
if (callOptions.signal) {
init.signal = callOptions.signal;
}

const res = await fetch(url.href, init);

Expand Down
70 changes: 53 additions & 17 deletions src/core/network/api/modules/messages/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setTimeout } from 'node:timers/promises';
import createDebug from 'debug';
import { MaxError } from '../../error';
import { BaseApi } from '../../base-api';
import type {
Expand All @@ -11,6 +12,14 @@ import type {
} from '../types';
import type { SendMessageDTO, DeleteMessageDTO } from './types';

const debug = createDebug('one-me:messages');

/** Максимальное количество повторов при ошибке attachment.not.ready */
const ATTACHMENT_NOT_READY_MAX_RETRIES = 10;

/** Начальная задержка между повторами (мс). Удваивается после каждой попытки. */
const ATTACHMENT_NOT_READY_BASE_DELAY = 1_000;

export class MessagesApi extends BaseApi {
get = async ({ ...query }: FlattenReq<GetMessagesDTO>): Promise<GetMessagesResponse> => {
return this._get('messages', {
Expand All @@ -24,26 +33,53 @@ export class MessagesApi extends BaseApi {
});
};

send = async ({
chat_id, user_id, disable_link_preview, ...body
}: FlattenReq<SendMessageDTO>): Promise<SendMessageResponse> => {
try {
return await this._post('messages', {
body,
query: { chat_id, user_id, disable_link_preview },
});
} catch (err) {
if (err instanceof MaxError) {
if (err.code === 'attachment.not.ready') {
console.log('Attachment not ready');
await setTimeout(1000);
return this.send({
chat_id, user_id, disable_link_preview, ...body,
});
send = async (
{
chat_id, user_id, disable_link_preview, ...body
}: FlattenReq<SendMessageDTO>,
options?: { signal?: AbortSignal },
): Promise<SendMessageResponse> => {
const signal = options?.signal;
let lastError: MaxError | undefined;

for (
let attempt = 0;
attempt < ATTACHMENT_NOT_READY_MAX_RETRIES;
attempt += 1
) {
signal?.throwIfAborted();

try {
return await this._post('messages', {
body,
query: { chat_id, user_id, disable_link_preview },
signal,
});
} catch (err) {
if (
!(err instanceof MaxError)
|| err.code !== 'attachment.not.ready'
) {
throw err;
}
lastError = err;
const delay = ATTACHMENT_NOT_READY_BASE_DELAY * (2 ** attempt);
debug(
'Attachment not ready (attempt %d/%d), retrying in %dms',
attempt + 1,
ATTACHMENT_NOT_READY_MAX_RETRIES,
delay,
);
await setTimeout(delay, undefined, { signal });
}
throw err;
}

throw lastError ?? new MaxError(500, {
code: 'attachment.not.ready',
message: `Attachment not ready after ${
ATTACHMENT_NOT_READY_MAX_RETRIES
} retries`,
});
};

edit = async ({ message_id, ...body }) => {
Expand Down
4 changes: 3 additions & 1 deletion src/core/network/api/modules/messages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export type SendMessageDTO = {
}
};

export type SendMessageExtra = Omit<FlattenReq<SendMessageDTO>, 'chat_id' | 'user_id' | 'text'>;
export type SendMessageExtra = Omit<FlattenReq<SendMessageDTO>, 'chat_id' | 'user_id' | 'text'> & {
signal?: AbortSignal;
};

export type SendMessageResponse = {
message: Message
Expand Down