Skip to content

Commit

Permalink
feat: add timeout feature
Browse files Browse the repository at this point in the history
Usage:

```js
socket.timeout(5000).emit("my-event", (err) => {
  if (err) {
    // the client did not acknowledge the event in the given delay
  }
});
```
  • Loading branch information
darrachequesne committed Nov 16, 2021
1 parent b7213e7 commit f0ed42f
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 4 deletions.
51 changes: 47 additions & 4 deletions lib/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export class Socket<
private readonly adapter: Adapter;
private acks: Map<number, () => void> = new Map();
private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
private flags: BroadcastFlags = {};
private flags: BroadcastFlags & { timeout?: number } = {};
private _anyListeners?: Array<(...args: any[]) => void>;

/**
Expand Down Expand Up @@ -207,9 +207,11 @@ export class Socket<

// access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") {
debug("emitting packet with ack id %d", this.nsp._ids);
this.acks.set(this.nsp._ids, data.pop());
packet.id = this.nsp._ids++;
const id = this.nsp._ids++;
debug("emitting packet with ack id %d", id);

this.registerAckCallback(id, data.pop());
packet.id = id;
}

const flags = Object.assign({}, this.flags);
Expand All @@ -220,6 +222,27 @@ export class Socket<
return true;
}

/**
* @private
*/
private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
const timeout = this.flags.timeout;
if (timeout === undefined) {
this.acks.set(id, ack);
return;
}

const timer = setTimeout(() => {
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.set(id, (...args) => {
clearTimeout(timer);
ack.apply(this, [null, ...args]);
});
}

/**
* Targets a room when broadcasting.
*
Expand Down Expand Up @@ -567,6 +590,26 @@ export class Socket<
return this.newBroadcastOperator().local;
}

/**
* 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 client:
*
* ```
* socket.timeout(5000).emit("my-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* ```
*
* @returns self
* @public
*/
public timeout(timeout: number): this {
this.flags.timeout = timeout;
return this;
}

/**
* Dispatch incoming event to socket listeners.
*
Expand Down
34 changes: 34 additions & 0 deletions test/socket-timeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Server } from "..";
import { createClient, success } from "./support/util";
import expect from "expect.js";

describe("timeout", () => {
it("should timeout if the client does not acknowledge the event", (done) => {
const io = new Server(0);
const client = createClient(io, "/");

io.on("connection", (socket) => {
socket.timeout(50).emit("unknown", (err) => {
expect(err).to.be.an(Error);
success(done, io, client);
});
});
});

it("should not timeout if the client does acknowledge the event", (done) => {
const io = new Server(0);
const client = createClient(io, "/");

client.on("echo", (arg, cb) => {
cb(arg);
});

io.on("connection", (socket) => {
socket.timeout(50).emit("echo", 42, (err, value) => {
expect(err).to.be(null);
expect(value).to.be(42);
success(done, io, client);
});
});
});
});
2 changes: 2 additions & 0 deletions test/socket.io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2888,4 +2888,6 @@ describe("socket.io", () => {
});
});
});

require("./socket-timeout");
});
24 changes: 24 additions & 0 deletions test/support/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import type { Server } from "../..";
import {
io as ioc,
ManagerOptions,
Socket as ClientSocket,
SocketOptions,
} from "socket.io-client";

const expect = require("expect.js");
const i = expect.stringify;

Expand All @@ -20,3 +28,19 @@ expect.Assertion.prototype.contain = function (...args) {
}
return contain.apply(this, args);
};

export function createClient(
io: Server,
nsp: string,
opts?: ManagerOptions & SocketOptions
): ClientSocket {
// @ts-ignore
const port = io.httpServer.address().port;
return ioc(`http://localhost:${port}${nsp}`, opts);
}

export function success(done: Function, io: Server, client: ClientSocket) {
io.close();
client.disconnect();
done();
}

0 comments on commit f0ed42f

Please sign in to comment.