Skip to content

Commit dbeb1e5

Browse files
add some tests
1 parent da2fa2b commit dbeb1e5

File tree

5 files changed

+79
-44
lines changed

5 files changed

+79
-44
lines changed

packages/engine.io-client/lib/socket.ts

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
CookieJar,
1111
createCookieJar,
1212
defaultBinaryType,
13+
nextTick,
1314
} from "./globals.node.js";
1415
import debugModule from "debug"; // debug()
15-
import { nextTick } from "./globals.js";
1616

1717
const debug = debugModule("engine.io-client:socket"); // debug()
1818

@@ -321,8 +321,11 @@ export class SocketWithoutUpgrade extends Emitter<
321321
private _pingTimeout: number = -1;
322322
private _maxPayload?: number = -1;
323323
private _pingTimeoutTimer: NodeJS.Timer;
324-
private _pingTimeoutTime: number | null = null;
325-
private _scheduledDisconnectFromIsResponsive = false;
324+
/**
325+
* The expiration timestamp of the {@link _pingTimeoutTimer} object is tracked, in case the timer is throttled and the
326+
* callback is not fired on time. This can happen for example when a laptop is suspended or when a phone is locked.
327+
*/
328+
private _pingTimeoutTime = Infinity;
326329
private clearTimeoutFn: typeof clearTimeout;
327330
private readonly _beforeunloadEventListener: () => void;
328331
private readonly _offlineEventListener: () => void;
@@ -577,7 +580,6 @@ export class SocketWithoutUpgrade extends Emitter<
577580

578581
// Socket is live - any packet counts
579582
this.emitReserved("heartbeat");
580-
this._resetPingTimeout();
581583

582584
switch (packet.type) {
583585
case "open":
@@ -588,6 +590,7 @@ export class SocketWithoutUpgrade extends Emitter<
588590
this._sendPacket("pong");
589591
this.emitReserved("ping");
590592
this.emitReserved("pong");
593+
this._resetPingTimeout();
591594
break;
592595

593596
case "error":
@@ -633,8 +636,6 @@ export class SocketWithoutUpgrade extends Emitter<
633636
*/
634637
private _resetPingTimeout() {
635638
this.clearTimeoutFn(this._pingTimeoutTimer);
636-
if (this._pingInterval <= 0 || this._pingTimeout <= 0) return;
637-
638639
const delay = this._pingInterval + this._pingTimeout;
639640
this._pingTimeoutTime = Date.now() + delay;
640641
this._pingTimeoutTimer = this.setTimeoutFn(() => {
@@ -718,29 +719,28 @@ export class SocketWithoutUpgrade extends Emitter<
718719
}
719720

720721
/**
721-
* Returns `true` if the connection is responding to heartbeats.
722+
* Checks whether the heartbeat timer has expired but the socket has not yet been notified.
722723
*
723-
* If heartbeats are disabled this will always return `true`.
724+
* Note: this method is private for now because it does not really fit the WebSocket API, but if we put it in the
725+
* `write()` method then the message would not be buffered by the Socket.IO client.
724726
*
725727
* @return {boolean}
728+
* @private
726729
*/
727-
public isResponsive() {
728-
if (this.readyState === "closed") return false;
729-
if (this._pingTimeoutTime === null) return true;
730+
/* private */ _hasPingExpired() {
731+
if (!this._pingTimeoutTime) return true;
730732

731-
const responsive = Date.now() < this._pingTimeoutTime;
732-
if (!responsive && !this._scheduledDisconnectFromIsResponsive) {
733-
debug(
734-
"`isResponsive` detected missed ping so scheduling connection close"
735-
);
736-
this._scheduledDisconnectFromIsResponsive = true;
733+
const hasExpired = Date.now() > this._pingTimeoutTime;
734+
if (hasExpired) {
735+
debug("throttled timer detected, scheduling connection close");
736+
this._pingTimeoutTime = 0;
737737

738738
nextTick(() => {
739739
this._onClose("ping timeout");
740740
}, this.setTimeoutFn);
741741
}
742742

743-
return responsive;
743+
return hasExpired;
744744
}
745745

746746
/**
@@ -752,9 +752,6 @@ export class SocketWithoutUpgrade extends Emitter<
752752
* @return {Socket} for chaining.
753753
*/
754754
public write(msg: RawData, options?: WriteOptions, fn?: () => void) {
755-
// this will schedule a disconnection on next tick if heartbeat missed
756-
this.isResponsive();
757-
758755
this._sendPacket("message", msg, options, fn);
759756
return this;
760757
}
@@ -768,7 +765,8 @@ export class SocketWithoutUpgrade extends Emitter<
768765
* @return {Socket} for chaining.
769766
*/
770767
public send(msg: RawData, options?: WriteOptions, fn?: () => void) {
771-
return this.write(msg, options, fn);
768+
this._sendPacket("message", msg, options, fn);
769+
return this;
772770
}
773771

774772
/**
@@ -895,7 +893,6 @@ export class SocketWithoutUpgrade extends Emitter<
895893

896894
// clear timers
897895
this.clearTimeoutFn(this._pingTimeoutTimer);
898-
this._pingTimeoutTime = null;
899896

900897
// stop event from firing again for transport
901898
this.transport.removeAllListeners("close");

packages/engine.io-client/test/socket.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,30 @@ describe("Socket", function () {
270270
});
271271
});
272272
});
273+
274+
describe("throttled timer", () => {
275+
it("checks the state of the timer", (done) => {
276+
const socket = new Socket();
277+
278+
expect(socket._hasPingExpired()).to.be(false);
279+
280+
socket.on("open", () => {
281+
expect(socket._hasPingExpired()).to.be(false);
282+
283+
// simulate a throttled timer
284+
socket._pingTimeoutTime = Date.now() - 1;
285+
286+
expect(socket._hasPingExpired()).to.be(true);
287+
288+
// subsequent calls should not trigger more 'close' events
289+
expect(socket._hasPingExpired()).to.be(true);
290+
expect(socket._hasPingExpired()).to.be(true);
291+
});
292+
293+
socket.on("close", (reason) => {
294+
expect(reason).to.eql("ping timeout");
295+
done();
296+
});
297+
});
298+
});
273299
});

packages/socket.io-client/lib/manager.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -492,17 +492,6 @@ export class Manager<
492492
this._close();
493493
}
494494

495-
/**
496-
* Returns `true` if the connection is responding to heartbeats.
497-
*
498-
* If heartbeats are disabled this will always return `true`.
499-
*
500-
* @return {boolean}
501-
*/
502-
isResponsive() {
503-
return this.engine.isResponsive ? this.engine.isResponsive() : true;
504-
}
505-
506495
/**
507496
* Writes a packet.
508497
*

packages/socket.io-client/lib/socket.ts

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -440,19 +440,13 @@ export class Socket<
440440
packet.id = id;
441441
}
442442

443-
const isTransportWritable =
444-
this.io.engine &&
445-
this.io.engine.transport &&
446-
this.io.engine.transport.writable;
443+
const isTransportWritable = this.io.engine?.transport?.writable;
444+
const isConnected = this.connected && !this.io.engine?._hasPingExpired();
447445

448-
const discardPacket =
449-
this.flags.volatile && (!isTransportWritable || !this.connected);
446+
const discardPacket = this.flags.volatile && !isTransportWritable;
450447
if (discardPacket) {
451448
debug("discard packet as the transport is not currently writable");
452-
} else if (
453-
this.connected &&
454-
(this.flags.volatile || this.io.isResponsive())
455-
) {
449+
} else if (isConnected) {
456450
this.notifyOutgoingListeners(packet);
457451
this.packet(packet);
458452
} else {

packages/socket.io-client/test/socket.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,4 +787,33 @@ describe("socket", () => {
787787
});
788788
});
789789
});
790+
791+
describe("throttled timer", () => {
792+
it("should buffer the event and send it upon reconnection", () => {
793+
return wrap((done) => {
794+
let hasReconnected = false;
795+
796+
const socket = io(BASE_URL, {
797+
forceNew: true,
798+
reconnectionDelay: 10,
799+
});
800+
801+
socket.once("connect", () => {
802+
// @ts-expect-error simulate a throttled timer
803+
socket.io.engine._pingTimeoutTime = Date.now() - 1;
804+
805+
socket.emit("echo", "123", (value) => {
806+
expect(hasReconnected).to.be(true);
807+
expect(value).to.eql("123");
808+
809+
success(done, socket);
810+
});
811+
});
812+
813+
socket.io.once("reconnect", () => {
814+
hasReconnected = true;
815+
});
816+
});
817+
});
818+
});
790819
});

0 commit comments

Comments
 (0)