Skip to content

Commit d6ae840

Browse files
committed
feat(plugins): add reconnect plugin
1 parent ad4810c commit d6ae840

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as Part from "./plugins/part.ts";
1919
import * as Ping from "./plugins/ping.ts";
2020
import * as Privmsg from "./plugins/privmsg.ts";
2121
import * as Quit from "./plugins/quit.ts";
22+
import * as Reconnect from "./plugins/reconnect.ts";
2223
import * as Register from "./plugins/register.ts";
2324
import * as RegisterOnConnect from "./plugins/register_on_connect.ts";
2425
import * as ThrowOnError from "./plugins/throw_on_error.ts";
@@ -50,6 +51,7 @@ type ClientParams =
5051
& Ping.PingParams
5152
& Privmsg.PrivmsgParams
5253
& Quit.QuitParams
54+
& Reconnect.ReconnectParams
5355
& Register.RegisterParams
5456
& RegisterOnConnect.RegisterOnConnectParams
5557
& Time.TimeParams
@@ -79,6 +81,7 @@ const plugins = [
7981
Ping.ping,
8082
Privmsg.privmsg,
8183
Quit.quit,
84+
Reconnect.reconnect,
8285
Register.register,
8386
RegisterOnConnect.registerOnConnect,
8487
ThrowOnError.throwOnError,

plugins/reconnect.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { FatalError, Plugin, RemoteAddr } from "../core/client.ts";
2+
import { Raw } from "../core/parsers.ts";
3+
4+
export interface ReconnectParams {
5+
options: {
6+
/** Enables auto reconnect.
7+
*
8+
* Takes `boolean` or `{ attempts?: number, delay?: number }`.
9+
*
10+
* Default to `false`. */
11+
reconnect?: boolean | {
12+
/** Number of attempts before giving up.
13+
*
14+
* Default to `10` attempts. */
15+
attempts?: number;
16+
17+
/** Delay between two attempts, in seconds.
18+
*
19+
* Default to `5` seconds. */
20+
delay?: number;
21+
};
22+
};
23+
24+
events: {
25+
"reconnecting": RemoteAddr;
26+
};
27+
}
28+
29+
const DEFAULT_ATTEMPTS = 10;
30+
const DEFAULT_DELAY = 5;
31+
32+
export const reconnect: Plugin<ReconnectParams> = (client, options) => {
33+
let reconnect = options.reconnect ?? false;
34+
35+
if (!reconnect) {
36+
return;
37+
}
38+
39+
if (typeof reconnect === "boolean") {
40+
reconnect = {
41+
attempts: DEFAULT_ATTEMPTS,
42+
delay: DEFAULT_DELAY,
43+
};
44+
}
45+
46+
const attempts = reconnect.attempts ?? DEFAULT_ATTEMPTS;
47+
const delay = reconnect.delay ?? DEFAULT_DELAY;
48+
49+
let currentAttempts = 0;
50+
51+
client.on("error", reconnectOnConnectError);
52+
client.on("raw", reconnectOnServerError);
53+
client.on("connecting", incrementAttempt);
54+
client.on("raw", resetAttempts);
55+
56+
function reconnectOnConnectError(error: FatalError) {
57+
if (error.type === "connect") {
58+
delayReconnect();
59+
}
60+
}
61+
62+
function reconnectOnServerError(msg: Raw) {
63+
if (msg.command === "ERROR") {
64+
delayReconnect();
65+
}
66+
}
67+
68+
function incrementAttempt() {
69+
currentAttempts++;
70+
}
71+
72+
function resetAttempts(msg: Raw) {
73+
if (msg.command === "RPL_WELCOME") {
74+
currentAttempts = 0;
75+
}
76+
}
77+
78+
let timeout: number | undefined;
79+
80+
function delayReconnect() {
81+
clearTimeout(timeout);
82+
83+
if (currentAttempts === attempts) {
84+
return;
85+
}
86+
87+
const { remoteAddr } = client.state;
88+
const { hostname, port } = remoteAddr;
89+
90+
client.emit("reconnecting", remoteAddr);
91+
92+
timeout = setTimeout(
93+
async () => await client.connect(hostname, port),
94+
delay * 1000,
95+
);
96+
}
97+
};

plugins/reconnect_test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { assertEquals } from "../deps.ts";
2+
import { describe } from "../testing/helpers.ts";
3+
import { mock } from "../testing/mock.ts";
4+
import { reconnect } from "./reconnect.ts";
5+
6+
describe("plugins/reconnect", (test) => {
7+
const plugins = [reconnect];
8+
const noop = () => {};
9+
10+
test("reconnect on connect error", async () => {
11+
const { client } = await mock(
12+
plugins,
13+
{ reconnect: { attempts: 2, delay: 0 } },
14+
{ withConnection: false },
15+
);
16+
17+
let reconnecting = 0;
18+
client.on("reconnecting", () => reconnecting++);
19+
client.on("error", noop);
20+
21+
client.connect("bad_remote_host");
22+
await client.once("reconnecting");
23+
24+
assertEquals(reconnecting, 1);
25+
});
26+
27+
test("reconnect on server error", async () => {
28+
const { client, server } = await mock(
29+
plugins,
30+
{ reconnect: { attempts: 2, delay: 0 } },
31+
{ withConnection: false },
32+
);
33+
34+
let reconnecting = 0;
35+
client.on("reconnecting", () => reconnecting++);
36+
client.on("error", noop);
37+
38+
await client.connect("");
39+
server.send("ERROR :Closing link: (user@host) [Client exited]");
40+
await client.once("reconnecting");
41+
42+
assertEquals(reconnecting, 1);
43+
});
44+
45+
test("not reconnect if disabled", async () => {
46+
const { client } = await mock(
47+
plugins,
48+
{ reconnect: false },
49+
{ withConnection: false },
50+
);
51+
52+
let reconnecting = 0;
53+
client.on("reconnecting", () => reconnecting++);
54+
client.on("error", noop);
55+
56+
client.connect("bad_remote_host");
57+
58+
assertEquals(reconnecting, 0);
59+
});
60+
});

0 commit comments

Comments
 (0)