Connection state recovery allows a client to reconnect after a
temporary disconnection and restore its state:
- id
- rooms
- data
- missed packets
Usage:
```js
import { Server } from "socket.io";
const io = new Server({
connectionStateRecovery: {
// default values
maxDisconnectionDuration: 2 * 60 * 1000,
skipMiddlewares: true,
},
});
io.on("connection", (socket) => {
console.log(socket.recovered); // whether the state was recovered or not
});
```
Here's how it works:
- the server sends a session ID during the handshake (which is
different from the current `id` attribute, which is public and can be
freely shared)
- the server also includes an offset in each packet (added at the end
of the data array, for backward compatibility)
- upon temporary disconnection, the server stores the client state for
a given delay (implemented at the adapter level)
- upon reconnection, the client sends both the session ID and the last
offset it has processed, and the server tries to restore the state
A few notes:
- the base adapter exposes two additional methods, persistSession() and
restoreSession(), that must be implemented by the other adapters in
order to allow the feature to work within a cluster
See: f5294126a8
- acknowledgements are not affected, because it won't work if the
client reconnects on another server (as the ack id is local)
- any disconnection that lasts longer than the
`maxDisconnectionDuration` value will result in a new session, so users
will still need to care for the state reconciliation between the server
and the client
Related: https://github.com/socketio/socket.io/discussions/4510
Note: some disconnection reasons could be merged in the next major
release, i.e. the Deno impl does not have "forced server close" and
"server shutting down"
Related: https://github.com/socketio/socket.io/issues/4387
This commit handles several edge cases that were silently ignored
before:
- receiving several CONNECT packets during a session
- receiving any packet without CONNECT packet first
Using an async operation with `io.use()` could lead to the creation of
several instances of a same namespace, each of them overriding the
previous one.
Example:
```js
io.use(async (nsp, auth, next) => {
await anOperationThatTakesSomeTime();
next();
});
```
Related: https://github.com/socketio/socket.io/pull/4136
Syntax:
```js
// server A
io.serverSideEmit("hello", "world");
// server B
io.on("hello", (arg) => {
console.log(arg); // prints "world"
});
```
With acknowledgements:
```js
// server A
io.serverSideEmit("hello", "world", (err, responses) => {
console.log(responses); // prints ["hi"]
});
// server B
io.on("hello", (arg, callback) => {
callback("hi");
});
```
This feature replaces the customHook/customRequest API from the Redis
adapter: https://github.com/socketio/socket.io-redis/issues/370
Packets that are sent to multiple clients will now be pre-encoded for
the WebSocket transport (which means simply prepending "4" - which is
the "message" packet type in Engine.IO).
Note: buffers are not pre-encoded, since they are sent without
modification over the WebSocket connection
See also: 7706b123df
engine.io diff: https://github.com/socketio/engine.io/compare/5.0.0...5.1.0
Before this change, `require("socket.io").Socket` would return
"undefined".
Note: having access to the Socket class allows users to modify its
prototype.
Related: https://github.com/socketio/socket.io/issues/3726
Syntax:
```ts
interface ClientToServerEvents {
"my-event": (a: number, b: string, c: number[]) => void;
}
interface ServerToClientEvents {
hello: (message: string) => void;
}
const io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer);
io.emit("hello", "world");
io.on("connection", (socket) => {
socket.on("my-event", (a, b, c) => {
// ...
});
socket.emit("hello", "again");
});
```
The events are not typed by default (inferred as any), so this change
is backward compatible.
Note: we could also have reused the method here ([1]) to add types to
the EventEmitter, instead of creating a StrictEventEmitter class.
Related: https://github.com/socketio/socket.io/issues/3742
[1]: https://github.com/binier/tiny-typed-emitter
In Socket.IO v2, the Socket query option was appended to the namespace
in the CONNECT packet:
{
type: 0,
nsp: "/my-namespace?abc=123"
}
Note: the "query" option on the client-side (v2) will be found in the
"auth" attribute on the server-side:
```
// client-side
const socket = io("/nsp1", {
query: {
abc: 123
}
});
socket.query = { abc: 456 };
// server-side
const io = require("socket.io")(httpServer, {
allowEIO3: true // enable compatibility mode
});
io.of("/nsp1").on("connection", (socket) => {
console.log(socket.handshake.auth); // { abc: 456 } (the Socket query)
console.log(socket.handshake.query.abc); // 123 (the Manager query)
});
More information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/#Add-a-clear-distinction-between-the-Manager-query-option-and-the-Socket-query-option
Related: https://github.com/socketio/socket.io/issues/3791
This commit restores the ability to send additional data in the
middleware functions, which was removed during the rewrite to
Typescript ([1]).
The only difference with the previous implementation is that the client
will now emit a "connect_error" (previously, "error") event with an
actual Error object, with both the message and an optional "data"
attribute.
```js
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
// client-side
socket.on("connect_error", err => {
console.log(err.message); // not authorized
console.log(err.data.content); // Please retry later
});
```
[1]: a5581a9789
After a given timeout, a client that did not join any namespace will be
closed in order to prevent malicious clients from using the server
resources.
The timeout defaults to 45 seconds, in order not to interfere with the
Engine.IO heartbeat mechanism (30 seconds).
In previous versions, a client was always connected to the default
namespace, even if it requested access to another namespace.
This meant that the middlewares registered for the default namespace
were triggered in any case, which is a surprising behavior for end
users.
This also meant that the query option of the Socket on the client-side
was not sent in the Socket.IO CONNECT packet for the default namespace:
```js
// default namespace: query sent in the query params
const socket = io({
query: {
abc: "def"
}
});
// another namespace: query sent in the query params + the CONNECT packet
const socket = io("/admin", {
query: {
abc: "def"
}
});
```
The client will now send a CONNECT packet in any case, and the query
option of the Socket is renamed to "auth", in order to make a clear
distinction with the query option of the Manager (included in the query
parameters of the HTTP requests).
```js
// server-side
io.use((socket, next) => {
// not triggered anymore
});
io.of("/admin").use((socket, next => {
// triggered
console.log(socket.handshake.query.abc); // "def"
console.log(socket.handshake.auth.abc); // "123"
});
// client-side
const socket = io("/admin", {
query: {
abc: "def"
},
auth: {
abc: "123"
}
});
```