@overpod/mcp-telegram 1.26.0 → 1.27.0

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.
Files changed (80) hide show
  1. package/dist/client.d.ts +12 -0
  2. package/dist/client.js +136 -0
  3. package/dist/index.js +15 -24
  4. package/dist/ipc-protocol.d.ts +19 -0
  5. package/dist/ipc-protocol.js +22 -0
  6. package/dist/lock.d.ts +12 -0
  7. package/dist/lock.js +83 -0
  8. package/dist/master.d.ts +1 -0
  9. package/dist/master.js +105 -0
  10. package/dist/rate-limiter.d.ts +1 -1
  11. package/dist/rate-limiter.js +8 -8
  12. package/dist/telegram-client.d.ts +6 -470
  13. package/dist/telegram-client.js +17 -874
  14. package/dist/telegram-helpers.d.ts +470 -0
  15. package/dist/telegram-helpers.js +870 -0
  16. package/dist/tools/account.js +7 -7
  17. package/dist/tools/boosts.js +4 -4
  18. package/dist/tools/chats.js +7 -7
  19. package/dist/tools/contacts.js +3 -3
  20. package/dist/tools/extras.js +3 -3
  21. package/dist/tools/group-calls.js +3 -3
  22. package/dist/tools/messages.js +17 -17
  23. package/dist/tools/quick-replies.js +3 -3
  24. package/dist/tools/reactions.js +2 -2
  25. package/dist/tools/shared.d.ts +3 -3
  26. package/dist/tools/shared.js +8 -7
  27. package/dist/tools/stars.js +3 -3
  28. package/dist/tools/stickers.js +5 -5
  29. package/dist/tools/stories.js +5 -5
  30. package/package.json +1 -1
  31. package/dist/__tests__/admin-log.test.d.ts +0 -1
  32. package/dist/__tests__/admin-log.test.js +0 -41
  33. package/dist/__tests__/approve-join-request.test.d.ts +0 -1
  34. package/dist/__tests__/approve-join-request.test.js +0 -107
  35. package/dist/__tests__/boosts.test.d.ts +0 -1
  36. package/dist/__tests__/boosts.test.js +0 -310
  37. package/dist/__tests__/broadcast-stats.test.d.ts +0 -1
  38. package/dist/__tests__/broadcast-stats.test.js +0 -172
  39. package/dist/__tests__/business-chat-links.test.d.ts +0 -1
  40. package/dist/__tests__/business-chat-links.test.js +0 -102
  41. package/dist/__tests__/get-message-buttons.test.d.ts +0 -1
  42. package/dist/__tests__/get-message-buttons.test.js +0 -122
  43. package/dist/__tests__/group-calls.test.d.ts +0 -1
  44. package/dist/__tests__/group-calls.test.js +0 -503
  45. package/dist/__tests__/inline-query-send.test.d.ts +0 -1
  46. package/dist/__tests__/inline-query-send.test.js +0 -94
  47. package/dist/__tests__/inline-query.test.d.ts +0 -1
  48. package/dist/__tests__/inline-query.test.js +0 -115
  49. package/dist/__tests__/megagroup-stats.test.d.ts +0 -1
  50. package/dist/__tests__/megagroup-stats.test.js +0 -166
  51. package/dist/__tests__/press-button.test.d.ts +0 -1
  52. package/dist/__tests__/press-button.test.js +0 -123
  53. package/dist/__tests__/quick-replies.test.d.ts +0 -1
  54. package/dist/__tests__/quick-replies.test.js +0 -245
  55. package/dist/__tests__/rate-limiter.test.d.ts +0 -1
  56. package/dist/__tests__/rate-limiter.test.js +0 -81
  57. package/dist/__tests__/reactions.test.d.ts +0 -1
  58. package/dist/__tests__/reactions.test.js +0 -23
  59. package/dist/__tests__/set-chat-permissions-merge.test.d.ts +0 -1
  60. package/dist/__tests__/set-chat-permissions-merge.test.js +0 -107
  61. package/dist/__tests__/set-chat-reactions.test.d.ts +0 -1
  62. package/dist/__tests__/set-chat-reactions.test.js +0 -129
  63. package/dist/__tests__/stars-status.test.d.ts +0 -1
  64. package/dist/__tests__/stars-status.test.js +0 -205
  65. package/dist/__tests__/stars-transactions.test.d.ts +0 -1
  66. package/dist/__tests__/stars-transactions.test.js +0 -82
  67. package/dist/__tests__/stories.test.d.ts +0 -1
  68. package/dist/__tests__/stories.test.js +0 -361
  69. package/dist/__tests__/toggle-anti-spam.test.d.ts +0 -1
  70. package/dist/__tests__/toggle-anti-spam.test.js +0 -80
  71. package/dist/__tests__/toggle-channel-signatures.test.d.ts +0 -1
  72. package/dist/__tests__/toggle-channel-signatures.test.js +0 -80
  73. package/dist/__tests__/toggle-forum-mode.test.d.ts +0 -1
  74. package/dist/__tests__/toggle-forum-mode.test.js +0 -80
  75. package/dist/__tests__/toggle-prehistory-hidden.test.d.ts +0 -1
  76. package/dist/__tests__/toggle-prehistory-hidden.test.js +0 -80
  77. package/dist/__tests__/tools/shared.test.d.ts +0 -1
  78. package/dist/__tests__/tools/shared.test.js +0 -110
  79. package/dist/__tests__/updates.test.d.ts +0 -1
  80. package/dist/__tests__/updates.test.js +0 -221
@@ -0,0 +1,12 @@
1
+ /** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
2
+ export declare class IpcClient {
3
+ private socket;
4
+ private pending;
5
+ private buf;
6
+ private connected;
7
+ connect(): Promise<boolean>;
8
+ isConnected(): boolean;
9
+ call(tool: string, args: Record<string, unknown>): Promise<unknown>;
10
+ destroy(): void;
11
+ }
12
+ export declare function runClient(apiId: number, apiHash: string, version: string): Promise<void>;
package/dist/client.js ADDED
@@ -0,0 +1,136 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { connect } from "node:net";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { encodeMessage, parseMessages } from "./ipc-protocol.js";
6
+ import { socketPath } from "./lock.js";
7
+ import { TelegramService } from "./telegram-client.js";
8
+ import { registerTools } from "./tools/index.js";
9
+ const CONNECT_TIMEOUT_MS = 5_000;
10
+ const IPC_CALL_TIMEOUT_MS = 30_000;
11
+ const MAX_RECONNECT_ATTEMPTS = 5;
12
+ /** Thin IPC proxy: forwards tool calls to the master process over Unix socket */
13
+ export class IpcClient {
14
+ socket = null;
15
+ pending = new Map();
16
+ buf = "";
17
+ connected = false;
18
+ async connect() {
19
+ return new Promise((resolve) => {
20
+ const sock = socketPath();
21
+ const s = connect(sock);
22
+ // One-shot connect timeout — cleared immediately on connect (HIGH-3)
23
+ const connectTimer = setTimeout(() => {
24
+ s.destroy();
25
+ resolve(false);
26
+ }, CONNECT_TIMEOUT_MS);
27
+ const onConnect = () => {
28
+ clearTimeout(connectTimer);
29
+ this.socket = s;
30
+ this.connected = true;
31
+ s.removeListener("error", onError);
32
+ s.on("data", (chunk) => {
33
+ this.buf += chunk.toString("utf-8");
34
+ const { messages, remaining } = parseMessages(this.buf);
35
+ this.buf = remaining;
36
+ for (const msg of messages) {
37
+ const res = msg;
38
+ const pending = this.pending.get(res.id);
39
+ if (!pending)
40
+ continue;
41
+ clearTimeout(pending.timer);
42
+ this.pending.delete(res.id);
43
+ if (res.error) {
44
+ pending.reject(new Error(res.error));
45
+ }
46
+ else {
47
+ pending.resolve(res.result);
48
+ }
49
+ }
50
+ });
51
+ s.on("close", () => {
52
+ this.connected = false;
53
+ for (const [, p] of this.pending) {
54
+ clearTimeout(p.timer);
55
+ p.reject(new Error("IPC connection closed"));
56
+ }
57
+ this.pending.clear();
58
+ });
59
+ s.on("error", () => { });
60
+ resolve(true);
61
+ };
62
+ const onError = () => {
63
+ clearTimeout(connectTimer);
64
+ resolve(false);
65
+ };
66
+ s.once("connect", onConnect);
67
+ s.once("error", onError);
68
+ });
69
+ }
70
+ isConnected() {
71
+ return this.connected;
72
+ }
73
+ async call(tool, args) {
74
+ if (!this.socket || !this.connected) {
75
+ throw new Error("IPC client not connected");
76
+ }
77
+ const id = randomUUID();
78
+ const socket = this.socket;
79
+ return new Promise((resolve, reject) => {
80
+ const timer = setTimeout(() => {
81
+ this.pending.delete(id);
82
+ reject(new Error(`IPC call timeout: ${tool}`));
83
+ }, IPC_CALL_TIMEOUT_MS);
84
+ this.pending.set(id, { resolve, reject, timer });
85
+ socket.write(encodeMessage({ id, tool, args }));
86
+ });
87
+ }
88
+ destroy() {
89
+ for (const [, p] of this.pending) {
90
+ clearTimeout(p.timer);
91
+ p.reject(new Error("IPC client destroyed"));
92
+ }
93
+ this.pending.clear();
94
+ this.socket?.destroy();
95
+ this.socket = null;
96
+ this.connected = false;
97
+ }
98
+ }
99
+ function wireIpcProxies(server, ipc) {
100
+ const s = server;
101
+ for (const [name, tool] of Object.entries(s._registeredTools)) {
102
+ Object.assign(tool, {
103
+ handler: (args) => ipc.call(name, args),
104
+ });
105
+ }
106
+ }
107
+ export async function runClient(apiId, apiHash, version) {
108
+ // Try to connect to master with retries — master may still be initializing its socket
109
+ let ipc = null;
110
+ for (let attempt = 0; attempt < MAX_RECONNECT_ATTEMPTS; attempt++) {
111
+ const candidate = new IpcClient();
112
+ if (await candidate.connect()) {
113
+ ipc = candidate;
114
+ break;
115
+ }
116
+ if (attempt < MAX_RECONNECT_ATTEMPTS - 1) {
117
+ await new Promise((r) => setTimeout(r, 150 * 2 ** attempt)); // 150ms, 300ms, 600ms, 1200ms
118
+ }
119
+ }
120
+ if (!ipc) {
121
+ // Master acquired lock but socket not ready — this process should not become master
122
+ // (it lost the lock race). Exit with clear message instead of creating two masters (CRITICAL-2)
123
+ console.error("[mcp-telegram] Cannot connect to master process. Try again in a moment.");
124
+ process.exit(1);
125
+ }
126
+ console.error(`[mcp-telegram] Client mode — proxying to master via ${socketPath()}`);
127
+ // Register all tools for MCP schema; dummy telegram instance is never used for actual calls
128
+ const telegram = new TelegramService(apiId, apiHash);
129
+ const server = new McpServer({ name: "mcp-telegram", version });
130
+ registerTools(server, telegram);
131
+ // Replace all handlers with IPC-forwarding versions
132
+ wireIpcProxies(server, ipc);
133
+ const transport = new StdioServerTransport();
134
+ await server.connect(transport);
135
+ console.error("[mcp-telegram] MCP server running on stdio (client)");
136
+ }
package/dist/index.js CHANGED
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  // Redirect console.log to stderr BEFORE any imports.
3
3
  // GramJS Logger uses console.log (stdout) which corrupts MCP JSON-RPC stream.
4
- const _origLog = console.log;
5
4
  console.log = (...args) => {
6
5
  console.error(...args);
7
6
  };
8
7
  import "dotenv/config";
9
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
- import { TelegramService } from "./telegram-client.js";
12
- import { registerTools } from "./tools/index.js";
8
+ import { createRequire } from "node:module";
9
+ import { tryAcquireLock } from "./lock.js";
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../package.json");
13
12
  // Telegram API credentials from env
14
13
  const API_ID = Number(process.env.TELEGRAM_API_ID);
15
14
  const API_HASH = process.env.TELEGRAM_API_HASH;
@@ -19,26 +18,18 @@ if (!API_ID || !API_HASH) {
19
18
  console.error("Set them in .env or export as environment variables");
20
19
  process.exit(1);
21
20
  }
22
- const telegram = new TelegramService(API_ID, API_HASH);
23
- const server = new McpServer({
24
- name: "mcp-telegram",
25
- version: "1.0.0",
26
- });
27
- registerTools(server, telegram);
28
21
  async function main() {
29
- const transport = new StdioServerTransport();
30
- await server.connect(transport);
31
- console.error("[mcp-telegram] MCP server running on stdio");
32
- // Auto-connect with saved session after MCP is ready (non-blocking)
33
- telegram.loadSession().then(async () => {
34
- if (await telegram.connect()) {
35
- const me = await telegram.getMe();
36
- console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
37
- }
38
- else if (telegram.lastError) {
39
- console.error(`[mcp-telegram] ${telegram.lastError}`);
40
- }
41
- });
22
+ const isMaster = tryAcquireLock();
23
+ if (isMaster) {
24
+ console.error("[mcp-telegram] Starting as master process");
25
+ const { runMaster } = await import("./master.js");
26
+ await runMaster(API_ID, API_HASH, version);
27
+ }
28
+ else {
29
+ console.error("[mcp-telegram] Starting as client process (master already running)");
30
+ const { runClient } = await import("./client.js");
31
+ await runClient(API_ID, API_HASH, version);
32
+ }
42
33
  }
43
34
  main().catch((err) => {
44
35
  console.error("[mcp-telegram] Fatal:", err);
@@ -0,0 +1,19 @@
1
+ /** Request from Client → Master */
2
+ export interface IpcRequest {
3
+ id: string;
4
+ tool: string;
5
+ args: Record<string, unknown>;
6
+ }
7
+ /** Response from Master → Client */
8
+ export interface IpcResponse {
9
+ id: string;
10
+ result?: unknown;
11
+ error?: string;
12
+ }
13
+ /** Encode a message as newline-delimited JSON */
14
+ export declare function encodeMessage(msg: IpcRequest | IpcResponse): string;
15
+ /** Parse newline-delimited JSON messages from a buffer, returns parsed messages + leftover */
16
+ export declare function parseMessages(buf: string): {
17
+ messages: (IpcRequest | IpcResponse)[];
18
+ remaining: string;
19
+ };
@@ -0,0 +1,22 @@
1
+ /** Encode a message as newline-delimited JSON */
2
+ export function encodeMessage(msg) {
3
+ return `${JSON.stringify(msg)}\n`;
4
+ }
5
+ /** Parse newline-delimited JSON messages from a buffer, returns parsed messages + leftover */
6
+ export function parseMessages(buf) {
7
+ const lines = buf.split("\n");
8
+ const remaining = lines.pop() ?? "";
9
+ const messages = [];
10
+ for (const line of lines) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed)
13
+ continue;
14
+ try {
15
+ messages.push(JSON.parse(trimmed));
16
+ }
17
+ catch {
18
+ // Skip malformed lines
19
+ }
20
+ }
21
+ return { messages, remaining };
22
+ }
package/dist/lock.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ export declare function lockPath(): string;
2
+ export declare function socketPath(): string;
3
+ /**
4
+ * Try to acquire the master lock.
5
+ * Returns true if this process is now the master.
6
+ * Returns false if another live master process holds the lock.
7
+ *
8
+ * Uses PID file + kill -0 to detect stale locks after crashes.
9
+ */
10
+ export declare function tryAcquireLock(): boolean;
11
+ export declare function releaseLock(): void;
12
+ export declare function releaseSocket(): void;
package/dist/lock.js ADDED
@@ -0,0 +1,83 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ const DEFAULT_SESSION_DIR = join(homedir(), ".mcp-telegram");
5
+ function resolveSessionDir() {
6
+ const sessionPath = process.env.TELEGRAM_SESSION_PATH;
7
+ if (sessionPath)
8
+ return dirname(sessionPath);
9
+ return DEFAULT_SESSION_DIR;
10
+ }
11
+ export function lockPath() {
12
+ return join(resolveSessionDir(), "daemon.lock");
13
+ }
14
+ export function socketPath() {
15
+ return join(resolveSessionDir(), "daemon.sock");
16
+ }
17
+ /**
18
+ * Try to acquire the master lock.
19
+ * Returns true if this process is now the master.
20
+ * Returns false if another live master process holds the lock.
21
+ *
22
+ * Uses PID file + kill -0 to detect stale locks after crashes.
23
+ */
24
+ export function tryAcquireLock() {
25
+ const lock = lockPath();
26
+ const dir = dirname(lock);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ }
30
+ if (existsSync(lock)) {
31
+ try {
32
+ const pid = Number.parseInt(readFileSync(lock, "utf-8").trim(), 10);
33
+ if (!Number.isNaN(pid) && pid > 0) {
34
+ try {
35
+ // kill -0: check if process is alive without sending a signal
36
+ process.kill(pid, 0);
37
+ // Process is alive — another master owns the lock
38
+ return false;
39
+ }
40
+ catch {
41
+ // ESRCH: process not found — stale lock, take over
42
+ unlinkSync(lock);
43
+ }
44
+ }
45
+ }
46
+ catch {
47
+ // Unreadable lock — remove and take over
48
+ try {
49
+ unlinkSync(lock);
50
+ }
51
+ catch { }
52
+ }
53
+ }
54
+ try {
55
+ // O_EXCL flag: atomic exclusive create — prevents TOCTOU race between two simultaneous starts
56
+ writeFileSync(lock, String(process.pid), { flag: "wx", mode: 0o600 });
57
+ return true;
58
+ }
59
+ catch {
60
+ // EEXIST: another process just created the lock — we lost the race, become client
61
+ return false;
62
+ }
63
+ }
64
+ export function releaseLock() {
65
+ try {
66
+ const lock = lockPath();
67
+ if (existsSync(lock)) {
68
+ const pid = Number.parseInt(readFileSync(lock, "utf-8").trim(), 10);
69
+ // Only remove our own lock
70
+ if (pid === process.pid)
71
+ unlinkSync(lock);
72
+ }
73
+ }
74
+ catch { }
75
+ }
76
+ export function releaseSocket() {
77
+ try {
78
+ const sock = socketPath();
79
+ if (existsSync(sock))
80
+ unlinkSync(sock);
81
+ }
82
+ catch { }
83
+ }
@@ -0,0 +1 @@
1
+ export declare function runMaster(apiId: number, apiHash: string, version: string): Promise<void>;
package/dist/master.js ADDED
@@ -0,0 +1,105 @@
1
+ import { createServer } from "node:net";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { encodeMessage, parseMessages } from "./ipc-protocol.js";
5
+ import { releaseLock, releaseSocket, socketPath } from "./lock.js";
6
+ import { TelegramService } from "./telegram-client.js";
7
+ import { registerTools } from "./tools/index.js";
8
+ let socketServer = null;
9
+ let cleanedUp = false;
10
+ function cleanup() {
11
+ if (cleanedUp)
12
+ return;
13
+ cleanedUp = true;
14
+ releaseLock();
15
+ releaseSocket();
16
+ socketServer?.close();
17
+ }
18
+ process.on("exit", cleanup);
19
+ process.on("SIGINT", () => process.exit(0));
20
+ process.on("SIGTERM", () => process.exit(0));
21
+ function handleClient(socket, mcpServer) {
22
+ let buf = "";
23
+ // Processing queue — ensures sequential handling even when handler awaits
24
+ let processing = false;
25
+ const queue = [];
26
+ async function drainQueue() {
27
+ if (processing)
28
+ return;
29
+ processing = true;
30
+ while (queue.length > 0) {
31
+ const req = queue.shift();
32
+ if (!req)
33
+ break;
34
+ const tool = mcpServer._registeredTools[req.tool];
35
+ const response = { id: req.id };
36
+ if (!tool) {
37
+ response.error = `Unknown tool: ${req.tool}`;
38
+ }
39
+ else {
40
+ try {
41
+ response.result = await tool.handler(req.args ?? {}, {});
42
+ }
43
+ catch (err) {
44
+ response.error = err instanceof Error ? err.message : String(err);
45
+ }
46
+ }
47
+ if (!socket.destroyed) {
48
+ socket.write(encodeMessage(response));
49
+ }
50
+ }
51
+ processing = false;
52
+ }
53
+ socket.on("data", (chunk) => {
54
+ buf += chunk.toString("utf-8");
55
+ const { messages, remaining } = parseMessages(buf);
56
+ buf = remaining;
57
+ for (const msg of messages) {
58
+ const req = msg;
59
+ if (!req.id || !req.tool)
60
+ continue;
61
+ queue.push(req);
62
+ }
63
+ drainQueue();
64
+ });
65
+ socket.on("error", () => { });
66
+ }
67
+ export async function runMaster(apiId, apiHash, version) {
68
+ const telegram = new TelegramService(apiId, apiHash);
69
+ const server = new McpServer({ name: "mcp-telegram", version });
70
+ registerTools(server, telegram);
71
+ const mcpServer = server;
72
+ // Remove stale socket file from previous crash before attempting to listen (HIGH-2)
73
+ releaseSocket();
74
+ const sock = socketPath();
75
+ const srv = createServer((socket) => handleClient(socket, mcpServer));
76
+ socketServer = srv;
77
+ await new Promise((resolve, reject) => {
78
+ srv.listen(sock, resolve);
79
+ srv.once("error", reject);
80
+ });
81
+ const { chmod } = await import("node:fs/promises");
82
+ try {
83
+ await chmod(sock, 0o600);
84
+ }
85
+ catch { }
86
+ console.error(`[mcp-telegram] Master mode — IPC socket ready: ${sock}`);
87
+ const transport = new StdioServerTransport();
88
+ await server.connect(transport);
89
+ console.error("[mcp-telegram] MCP server running on stdio (master)");
90
+ // Auto-connect with saved session — catch to avoid unhandled rejection (MEDIUM-2)
91
+ telegram
92
+ .loadSession()
93
+ .then(async () => {
94
+ if (await telegram.connect()) {
95
+ const me = await telegram.getMe();
96
+ console.error(`[mcp-telegram] Auto-connected as @${me.username}`);
97
+ }
98
+ else if (telegram.lastError) {
99
+ console.error(`[mcp-telegram] ${telegram.lastError}`);
100
+ }
101
+ })
102
+ .catch((err) => {
103
+ console.error("[mcp-telegram] Auto-connect failed:", err);
104
+ });
105
+ }
@@ -13,11 +13,11 @@ export interface RateLimiterOptions {
13
13
  maxRetryDelay?: number;
14
14
  }
15
15
  export declare class RateLimiter {
16
- private lastRequestTime;
17
16
  private minInterval;
18
17
  private maxRetries;
19
18
  private initialRetryDelay;
20
19
  private maxRetryDelay;
20
+ private slotQueue;
21
21
  constructor(options?: RateLimiterOptions);
22
22
  /**
23
23
  * Execute a function with rate limiting and automatic retry.
@@ -3,11 +3,12 @@
3
3
  * Handles FLOOD_WAIT errors and implements exponential backoff.
4
4
  */
5
5
  export class RateLimiter {
6
- lastRequestTime = 0;
7
6
  minInterval;
8
7
  maxRetries;
9
8
  initialRetryDelay;
10
9
  maxRetryDelay;
10
+ // Serializes concurrent calls so each waits for the previous slot to clear
11
+ slotQueue = Promise.resolve();
11
12
  constructor(options = {}) {
12
13
  const maxRequestsPerSecond = options.maxRequestsPerSecond ?? 20;
13
14
  this.minInterval = 1000 / maxRequestsPerSecond;
@@ -67,13 +68,12 @@ export class RateLimiter {
67
68
  throw error;
68
69
  }
69
70
  }
70
- async waitForSlot() {
71
- const now = Date.now();
72
- const elapsed = now - this.lastRequestTime;
73
- if (elapsed < this.minInterval) {
74
- await sleep(this.minInterval - elapsed);
75
- }
76
- this.lastRequestTime = Date.now();
71
+ waitForSlot() {
72
+ // Chain onto the previous slot so concurrent callers queue up sequentially.
73
+ // Each turn: wait minInterval from when the previous turn started, then resolve.
74
+ const nextSlot = this.slotQueue.then(() => sleep(this.minInterval));
75
+ this.slotQueue = nextSlot;
76
+ return nextSlot;
77
77
  }
78
78
  }
79
79
  function isNetworkError(msg) {