From ccf7998cc5049d02022567aedfb263de875a06a5 Mon Sep 17 00:00:00 2001 From: Damien Arrachequesne Date: Tue, 16 Nov 2021 17:41:30 +0100 Subject: [PATCH] feat: add timeout feature Usage: ```js socket.timeout(5000).emit("my-event", (err) => { if (err) { // the server did not acknowledge the event in the given delay } }); ``` --- lib/socket.ts | 59 +++++++++++++++++++++++++++++++++++++++++++++++--- test/socket.ts | 38 ++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/lib/socket.ts b/lib/socket.ts index 3d40c82e..17a71f73 100644 --- a/lib/socket.ts +++ b/lib/socket.ts @@ -36,6 +36,7 @@ const RESERVED_EVENTS = Object.freeze({ interface Flags { compress?: boolean; volatile?: boolean; + timeout?: number; } interface SocketReservedEvents { @@ -164,9 +165,12 @@ export class Socket< // event ack callback if ("function" === typeof args[args.length - 1]) { - debug("emitting packet with ack id %d", this.ids); - this.acks[this.ids] = args.pop(); - packet.id = this.ids++; + const id = this.ids++; + debug("emitting packet with ack id %d", id); + + const ack = args.pop() as Function; + this._registerAckCallback(id, ack); + packet.id = id; } const isTransportWritable = @@ -189,6 +193,35 @@ export class Socket< return this; } + /** + * @private + */ + private _registerAckCallback(id: number, ack: Function) { + const timeout = this.flags.timeout; + if (timeout === undefined) { + this.acks[id] = ack; + return; + } + + // @ts-ignore + const timer = this.io.setTimeoutFn(() => { + for (let i = 0; i < this.sendBuffer.length; i++) { + if (this.sendBuffer[i].id === id) { + debug("removing packet with ack id %d from the buffer", id); + this.sendBuffer.splice(i, 1); + } + } + debug("event with ack id %d has timed out after %d ms", id, timeout); + ack.call(this, new Error("operation has timed out")); + }, timeout); + + this.acks[id] = (...args) => { + // @ts-ignore + this.io.clearTimeoutFn(timer); + ack.apply(this, [null, ...args]); + }; + } + /** * Sends a packet. * @@ -478,6 +511,26 @@ export class Socket< return this; } + /** + * Sets a modifier for a subsequent event emission that the callback will be called with an error when the + * given number of milliseconds have elapsed without an acknowledgement from the server: + * + * ``` + * socket.timeout(5000).emit("my-event", (err) => { + * if (err) { + * // the server did not acknowledge the event in the given delay + * } + * }); + * ``` + * + * @returns self + * @public + */ + public timeout(timeout: number): this { + this.flags.timeout = timeout; + return this; + } + /** * Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the * callback. diff --git a/test/socket.ts b/test/socket.ts index dae4082d..f46a6abf 100644 --- a/test/socket.ts +++ b/test/socket.ts @@ -1,6 +1,11 @@ import expect from "expect.js"; import { io } from ".."; +const success = (done, socket) => { + socket.disconnect(); + done(); +}; + describe("socket", function () { this.timeout(70000); @@ -327,4 +332,37 @@ describe("socket", function () { }); }); }); + + describe("timeout", () => { + it("should timeout after the given delay when socket is not connected", (done) => { + const socket = io("/", { + autoConnect: false, + }); + + socket.timeout(50).emit("event", (err) => { + expect(err).to.be.an(Error); + expect(socket.sendBuffer).to.be.empty(); + done(); + }); + }); + + it("should timeout after the given delay when server does not acknowledge the event", (done) => { + const socket = io("/"); + + socket.timeout(50).emit("unknown", (err) => { + expect(err).to.be.an(Error); + success(done, socket); + }); + }); + + it("should not timeout after the given delay when server does acknowledge", (done) => { + const socket = io("/"); + + socket.timeout(50).emit("echo", 42, (err, value) => { + expect(err).to.be(null); + expect(value).to.be(42); + success(done, socket); + }); + }); + }); });