Files
socket.io/packages/socket.io-client/test/socket.ts
Tom Jenkinson 8adcfbfde5 fix(sio-client): do not send a packet on an expired connection (#5134)
When a laptop is suspended or a phone is locked, the timer that is used
to check the liveness of the connection is paused and is not able to
detect that the heartbeat has failed.

Previously, emitting a message after resuming the page would lose the
message. The status of the timer will now be checked before sending the
message, so that it gets buffered and sent upon reconnection.

Note: we could also have used the Page Visibility API or a custom
setTimeout() method based on setInterval(), but this would not be as
reliable as the current solution.

Reference: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API

Related: https://github.com/socketio/socket.io/issues/5135
2024-09-18 11:11:22 +02:00

820 lines
21 KiB
TypeScript

import expect = require("expect.js");
import { io } from "..";
import { wrap, BASE_URL, success } from "./support/util";
describe("socket", () => {
it("should have an accessible socket id equal to the server-side socket id (default namespace)", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.emit("getId", (id) => {
expect(socket.id).to.be.ok();
expect(socket.id).to.be.eql(id);
expect(socket.id).to.not.eql(socket.io.engine.id);
socket.disconnect();
done();
});
});
});
it("should have an accessible socket id equal to the server-side socket id (custom namespace)", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/foo", { forceNew: true });
socket.emit("getId", (id) => {
expect(socket.id).to.be.ok();
expect(socket.id).to.be.eql(id);
expect(socket.id).to.not.eql(socket.io.engine.id);
socket.disconnect();
done();
});
});
});
it("clears socket.id upon disconnection", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.on("connect", () => {
socket.on("disconnect", () => {
expect(socket.id).to.not.be.ok();
done();
});
socket.disconnect();
});
});
});
it("doesn't fire an error event if we force disconnect in opening state", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true, timeout: 100 });
socket.disconnect();
socket.io.on("error", () => {
throw new Error("Unexpected");
});
setTimeout(() => {
done();
}, 300);
});
});
it("fire a connect_error event when the connection cannot be established", () => {
return wrap((done) => {
const socket = io("http://localhost:9823", {
forceNew: true,
timeout: 100,
});
socket.on("connect_error", () => {
socket.close();
done();
});
});
});
it("fire a connect_error event on open timeout (polling)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
transports: ["polling"],
timeout: 0,
});
socket.on("connect_error", (err) => {
expect(err.message).to.eql("timeout");
socket.disconnect();
done();
});
});
});
it("fire a connect_error event on open timeout (websocket)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
transports: ["websocket"],
timeout: 0,
});
socket.on("connect_error", (err) => {
expect(err.message).to.eql("timeout");
socket.disconnect();
done();
});
});
});
it("doesn't fire a connect_error event when the connection is already established", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.on("connect", () => {
socket.io.engine.close();
});
socket.on("connect_error", () => {
done(new Error("should not happen"));
});
setTimeout(() => {
socket.close();
done();
}, 300);
});
});
it("should change socket.id upon reconnection", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true, reconnectionDelay: 10 });
socket.on("connect", () => {
const id = socket.id;
socket.io.on("reconnect_attempt", () => {
expect(socket.id).to.not.be.ok();
});
socket.io.on("reconnect", () => {
expect(socket.id).to.not.eql(id);
socket.disconnect();
done();
});
socket.io.engine.close();
});
});
});
it("should enable compression by default", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.on("connect", () => {
socket.io.engine.once("packetCreate", (packet) => {
expect(packet.options.compress).to.be(true);
socket.disconnect();
done();
});
socket.emit("hi");
});
});
});
it("should disable compression", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.on("connect", () => {
socket.io.engine.once("packetCreate", (packet) => {
expect(packet.options.compress).to.be(false);
socket.disconnect();
done();
});
socket.compress(false).emit("hi");
});
});
});
describe("query option", () => {
it("should accept an object (default namespace)", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/", {
forceNew: true,
query: { e: "f" },
});
socket.emit("getHandshake", (handshake) => {
expect(handshake.query.e).to.be("f");
socket.disconnect();
done();
});
});
});
it("should accept a query string (default namespace)", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/?c=d", { forceNew: true });
socket.emit("getHandshake", (handshake) => {
expect(handshake.query.c).to.be("d");
socket.disconnect();
done();
});
});
});
it("should accept an object", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
query: { a: "b" },
});
socket.on("handshake", (handshake) => {
expect(handshake.query.a).to.be("b");
socket.disconnect();
done();
});
});
});
it("should accept a query string", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc?b=c&d=e", { forceNew: true });
socket.on("handshake", (handshake) => {
expect(handshake.query.b).to.be("c");
expect(handshake.query.d).to.be("e");
socket.disconnect();
done();
});
});
});
it("should properly encode the parameters", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
query: { "&a": "&=?a" },
});
socket.on("handshake", (handshake) => {
expect(handshake.query["&a"]).to.be("&=?a");
socket.disconnect();
done();
});
});
});
});
describe("auth option", () => {
it("should accept an object", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
auth: { a: "b", c: "d" },
});
socket.on("handshake", (handshake) => {
expect(handshake.auth.a).to.be("b");
expect(handshake.auth.c).to.be("d");
expect(handshake.query.a).to.be(undefined);
socket.disconnect();
done();
});
});
});
it("should accept an function", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
auth: (cb) => cb({ e: "f" }),
});
socket.on("handshake", (handshake) => {
expect(handshake.auth.e).to.be("f");
expect(handshake.query.e).to.be(undefined);
socket.disconnect();
done();
});
});
});
});
it("should fire an error event on middleware failure from custom namespace", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/no", { forceNew: true });
socket.on("connect_error", (err) => {
expect(err).to.be.an(Error);
expect(err.message).to.eql("Auth failed (custom namespace)");
socket.disconnect();
done();
});
});
});
it("should not try to reconnect after a middleware failure", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/no", {
forceNew: true,
reconnectionDelay: 10,
});
let count = 0;
socket.on("connect_error", () => {
count++;
// force reconnection
socket.io.engine.close();
});
setTimeout(() => {
expect(count).to.eql(1);
done();
}, 100);
});
});
it("should properly disconnect then reconnect", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/", {
forceNew: true,
transports: ["websocket"],
});
let count = 0;
socket.once("connect", () => {
socket.disconnect().connect();
});
socket.on("disconnect", () => {
count++;
});
setTimeout(() => {
expect(count).to.eql(1);
success(done, socket);
}, 100);
});
});
it("should throw on reserved event", () => {
const socket = io(BASE_URL + "/no", { forceNew: true });
expect(() => socket.emit("disconnecting", "goodbye")).to.throwException(
/"disconnecting" is a reserved event name/,
);
});
it("should emit events in order", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/", { autoConnect: false });
let i = 0;
socket.on("connect", () => {
socket.emit("echo", "second", () => {
expect(++i).to.eql(2);
socket.disconnect();
done();
});
});
socket.emit("echo", "first", () => {
expect(++i).to.eql(1);
});
socket.connect();
});
});
it("should emit an event and wait for the acknowledgement", () => {
return wrap(async (done) => {
const socket = io(BASE_URL, { forceNew: true });
const val = await socket.emitWithAck("echo", 123);
expect(val).to.be(123);
success(done, socket);
});
});
describe("volatile packets", () => {
it("should discard a volatile packet when the socket is not connected", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true, autoConnect: false });
socket.volatile.emit("getId", () => {
done(new Error("should not happen"));
});
socket.emit("getId", () => {
socket.disconnect();
done();
});
socket.connect();
});
});
it("should discard a volatile packet when the pipe is not ready", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
socket.on("connect", () => {
socket.emit("getId", () => {
socket.disconnect();
done();
});
socket.volatile.emit("getId", () => {
done(new Error("should not happen"));
});
});
});
});
it("should send a volatile packet when the socket is connected and the pipe is ready", () => {
return wrap((done) => {
const socket = io(BASE_URL, { forceNew: true });
const interval = setInterval(() => {
socket.volatile.emit("getId", () => {
clearInterval(interval);
socket.disconnect();
done();
});
}, 200);
});
});
});
describe("onAny", () => {
it("should call listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
socket.onAny((event, arg1) => {
expect(event).to.be("handshake");
expect(arg1).to.be.an(Object);
success(done, socket);
});
});
});
it("should prepend listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", { forceNew: true });
let count = 0;
socket.onAny((event, arg1) => {
expect(count).to.be(2);
success(done, socket);
});
socket.prependAny(() => {
expect(count++).to.be(1);
});
socket.prependAny(() => {
expect(count++).to.be(0);
});
});
});
it("should remove listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
let count = 0;
const fail = () => done(new Error("fail"));
socket.onAny(fail);
socket.offAny(fail);
expect(socket.listenersAny.length).to.be(0);
socket.onAny(() => {
success(done, socket);
});
});
});
});
describe("onAnyOutgoing", () => {
it("should call listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
socket.on("connect", () => {
socket.onAnyOutgoing((event, arg1) => {
expect(event).to.be("my-event");
expect(arg1).to.be("123");
success(done, socket);
});
socket.emit("my-event", "123");
});
});
});
it("should call listener with binary data", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
socket.on("connect", () => {
socket.onAnyOutgoing((event, arg1) => {
expect(event).to.be("my-event");
expect(arg1).to.be.an(Uint8Array);
success(done, socket);
});
socket.emit("my-event", Uint8Array.of(1, 2, 3));
});
});
});
it("should prepend listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
let count = 0;
socket.onAnyOutgoing(() => {
expect(count).to.be(2);
success(done, socket);
});
socket.prependAnyOutgoing(() => {
expect(count++).to.be(1);
});
socket.prependAnyOutgoing(() => {
expect(count++).to.be(0);
});
socket.emit("my-event", "123");
});
});
it("should remove listener", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/abc", {
forceNew: true,
});
const fail = () => done(new Error("fail"));
socket.onAnyOutgoing(fail);
socket.offAnyOutgoing(fail);
expect(socket.listenersAnyOutgoing.length).to.be(0);
socket.onAnyOutgoing(() => {
success(done, socket);
});
socket.emit("my-event", "123");
});
});
});
describe("timeout", () => {
it("should timeout after the given delay when socket is not connected", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/", {
autoConnect: false,
});
socket.timeout(50).emit("event", (err) => {
expect(err).to.be.an(Error);
expect(socket.sendBuffer).to.be.empty();
success(done, socket);
});
});
});
it("should timeout when the server does not acknowledge the event", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/");
socket.timeout(50).emit("unknown", (err) => {
expect(err).to.be.an(Error);
success(done, socket);
});
});
});
it("should timeout when the server does not acknowledge the event in time", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/");
let count = 0;
socket.timeout(0).emit("echo", 42, (err) => {
expect(err).to.be.an(Error);
count++;
});
setTimeout(() => {
expect(count).to.eql(1);
success(done, socket);
}, 200);
});
});
it("should not timeout when the server does acknowledge the event", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/");
socket.timeout(50).emit("echo", 42, (err, value) => {
expect(err).to.be(null);
expect(value).to.be(42);
success(done, socket);
});
});
});
it("should timeout when the server does not acknowledge the event (promise)", () => {
return wrap(async (done) => {
const socket = io(BASE_URL + "/");
try {
await socket.timeout(50).emitWithAck("unknown");
done(new Error("should not happen"));
} catch (e) {
success(done, socket);
}
});
});
it("should not timeout when the server does acknowledge the event (promise)", () => {
return wrap(async (done) => {
const socket = io(BASE_URL + "/");
try {
const value = await socket.timeout(50).emitWithAck("echo", 42);
expect(value).to.be(42);
success(done, socket);
} catch (e) {
done(new Error("should not happen"));
}
});
});
it("should use the default timeout value", () => {
return wrap((done) => {
const socket = io(BASE_URL + "/", {
ackTimeout: 50,
});
socket.emit("unknown", (err) => {
expect(err).to.be.an(Error);
success(done, socket);
});
});
});
describe("acknowledgement upon disconnection", () => {
it("should not ack upon disconnection (callback)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});
socket.on("connect", () => {
socket.emit("echo", "a", (_value) => {
done(new Error("should not happen"));
});
socket.disconnect();
// @ts-ignore property 'acks' is private
expect(Object.keys(socket.acks).length).to.eql(0);
setTimeout(() => success(done, socket), 100);
});
});
});
it("should ack with an error upon disconnection (callback & timeout)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});
socket.on("connect", () => {
socket.timeout(10_000).emit("echo", "a", (err) => {
expect(err).to.be.an(Error);
// @ts-ignore property 'acks' is private
expect(Object.keys(socket.acks).length).to.eql(0);
success(done, socket);
});
socket.disconnect();
});
});
});
it("should ack with an error upon disconnection (callback & ackTimeout)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
ackTimeout: 10_000,
});
socket.on("connect", () => {
socket.emit("echo", "a", (err) => {
expect(err).to.be.an(Error);
success(done, socket);
});
socket.disconnect();
});
});
});
it("should ack with an error upon disconnection (promise)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});
socket.on("connect", () => {
socket.emitWithAck("echo", "a").catch((err) => {
expect(err).to.be.an(Error);
success(done, socket);
});
socket.disconnect();
});
});
});
it("should ack with an error upon disconnection (promise & timeout)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});
socket.on("connect", () => {
socket
.timeout(10_000)
.emitWithAck("echo", "a")
.catch((err) => {
expect(err).to.be.an(Error);
success(done, socket);
});
socket.disconnect();
});
});
});
it("should not discard an unsent ack (callback)", () => {
return wrap((done) => {
const socket = io(BASE_URL, {
forceNew: true,
});
socket.once("connect", () => {
socket.disconnect();
// the packet will be buffered and sent upon reconnection
socket.emit("echo", "a", (value) => {
expect(value).to.eql("a");
success(done, socket);
});
setTimeout(() => socket.connect(), 100);
});
});
});
});
});
describe("throttled timer", () => {
it("should buffer the event and send it upon reconnection", () => {
return wrap((done) => {
let hasReconnected = false;
const socket = io(BASE_URL, {
forceNew: true,
reconnectionDelay: 10,
});
socket.once("connect", () => {
// @ts-expect-error simulate a throttled timer
socket.io.engine._pingTimeoutTime = Date.now() - 1;
socket.emit("echo", "123", (value) => {
expect(hasReconnected).to.be(true);
expect(value).to.eql("123");
success(done, socket);
});
});
socket.io.once("reconnect", () => {
hasReconnected = true;
});
});
});
});
});