Skip to content

Commit

Permalink
connection: Fix race condition when socket closes after timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
sonnyp committed Jan 8, 2025
1 parent aaaa4b1 commit 43699f9
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 16 deletions.
34 changes: 25 additions & 9 deletions packages/connection/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class Connection extends EventEmitter {
}

_reset() {
this.status = "offline";
this._detachSocket();
this._detachParser();
this.root = null;
Expand Down Expand Up @@ -57,16 +56,18 @@ class Connection extends EventEmitter {
this.emit("error", error);
}

#onSocketClosed(dirty, event) {
this._reset();
this._status("disconnect", { clean: !dirty, event });
}

_attachSocket(socket) {
this.socket = socket;
const listeners = this.socketListeners;

listeners.data = this._onData.bind(this);

listeners.close = (dirty, event) => {
this._reset();
this._status("disconnect", { clean: !dirty, event });
};
listeners.close = this.#onSocketClosed.bind(this);

listeners.connect = () => {
this._status("connect");
Expand Down Expand Up @@ -178,8 +179,23 @@ class Connection extends EventEmitter {
return this.jid;
}

/*
[
"offline",
// "disconnect",
"connecting",
"connected",
"opening",
"open",
"online",
"closing",
"close",
"disconnecting",
"disconnect",
"offline",
];
*/
_status(status, ...args) {
if (this.status === status) return;
this.status = status;
this.emit("status", status, ...args);
this.emit(status, ...args);
Expand All @@ -201,7 +217,9 @@ class Connection extends EventEmitter {

try {
await this.disconnect();
} catch {}
} catch (err) {
this.#onSocketClosed(true, err);
}

return el;
}
Expand Down Expand Up @@ -243,8 +261,6 @@ class Connection extends EventEmitter {
* https://tools.ietf.org/html/rfc7395#section-3.6
*/
async disconnect(timeout = this.timeout) {
if (!this.socket) return;

this._status("disconnecting");
this.socket.end();

Expand Down
7 changes: 0 additions & 7 deletions packages/connection/test/disconnect.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,3 @@ test("rejects if socket.end throws", (done) => {
done();
});
});

test("resolves if socket is absent", async () => {
const conn = new Connection();
conn.socket = null;

await expect(conn.disconnect()).toResolve();
});
30 changes: 30 additions & 0 deletions packages/connection/test/stop.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Connection from "../index.js";
import { EventEmitter } from "@xmpp/events";

test("resolves if socket property is undefined", async () => {
const conn = new Connection();
Expand Down Expand Up @@ -38,3 +39,32 @@ test("does not throw if connection is not established", async () => {
await conn.stop();
expect().pass();
});

// https://github.com/xmppjs/xmpp.js/issues/956
test("socket closes after timeout", (done) => {
const conn = new Connection();
conn.timeout = 100;

const socket = new EventEmitter();
socket.end = jest.fn(async () => {
// Mock receiving "close" event after timeout
setTimeout(() => {
socket.emit("close");
}, conn.timeout * 2);
});
conn._attachSocket(socket);

const statuses = [conn.status];
conn.on("status", (status) => {
statuses.push(status);
});

conn.stop();

// Wait a bit and assert that status is correct
setTimeout(() => {
expect(conn.status).toBe("offline");
expect(conn.status).not.toBe("disconnect");
done();
}, conn.timeout * 3);
});
40 changes: 40 additions & 0 deletions test/client.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// eslint-disable-next-line n/no-extraneous-import
import { promise } from "@xmpp/events";
import { client, xml, jid } from "../packages/client/index.js";
import debug from "../packages/debug/index.js";
import server from "../server/index.js";
Expand Down Expand Up @@ -143,6 +145,44 @@ test("does not reconnect when stop is called", (done) => {
xmpp.start();
});

test("statuses", async () => {
xmpp = client({ credentials, service: domain });
debug(xmpp);

let statuses = [xmpp.status];

xmpp.on("status", (status) => {
statuses.push(status);
});

xmpp.on("error", () => {});

await xmpp.start();

expect(statuses).toEqual([
"offline",
"connecting",
"connect",
"opening",
"open",
"online",
]);

// trigger reconnect
await xmpp.disconnect();

statuses = [xmpp.status];
await promise(xmpp, "open");

expect(statuses).toEqual([
"disconnect",
"connecting",
"connect",
"opening",
"open",
]);
});

test("anonymous authentication", (done) => {
expect.assertions(2);

Expand Down

0 comments on commit 43699f9

Please sign in to comment.