This commit fixes several issues with emit types:
- calling `emit()` without calling `timeout()` first is now only available for events without acknowledgement
- calling `emit()` after calling `timeout()` is now only available for events with an acknowledgement
- calling `emitWithAck()` is now only available for events with an acknowledgement
- `timeout()` must be called before calling `emitWithAck()`
This is a follow-up commit of [1]. Without it, adapter.persistSession()
would be called even if the connection state recovery feature was
disabled.
[1]: 54d5ee05a6
This reverts commit 4e64123862.
Using the id of the socket is not possible, since it is lost upon
reconnection (unless connection recovery is successful), so we revert
the previous change.
This commit adds some syntactic sugar around acknowledgements:
- `emitWithAck()`
```js
try {
const responses = await io.timeout(1000).emitWithAck("some-event");
console.log(responses); // one response per client
} catch (e) {
// some clients did not acknowledge the event in the given delay
}
io.on("connection", async (socket) => {
// without timeout
const response = await socket.emitWithAck("hello", "world");
// with a specific timeout
try {
const response = await socket.timeout(1000).emitWithAck("hello", "world");
} catch (err) {
// the client did not acknowledge the event in the given delay
}
});
```
- `serverSideEmitWithAck()`
```js
try {
const responses = await io.timeout(1000).serverSideEmitWithAck("some-event");
console.log(responses); // one response per server (except itself)
} catch (e) {
// some servers did not acknowledge the event in the given delay
}
```
Related:
- https://github.com/socketio/socket.io/issues/4175
- https://github.com/socketio/socket.io/issues/4577
- https://github.com/socketio/socket.io/issues/4583
This commit adds a new option, "cleanupEmptyChildNamespaces". With this
option enabled (disabled by default), when a socket disconnects from a
dynamic namespace and if there are no other sockets connected to it
then the namespace will be cleaned up and its adapter will be closed.
Note: the namespace can be connected to later (it will be recreated)
Related: https://github.com/socketio/socket.io-redis-adapter/issues/480
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:
- only packets without binary attachments are affected
- the permessage-deflate extension must be disabled (which is the default)
Related:
- 5f7b47d40f
- 5e34722b0b
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 should fix a rare case where the Engine.IO connection was upgraded
to WebSocket while the Socket.IO socket was disconnected, which would
result in the following exception:
> TypeError: Cannot read properties of undefined (reading 'forEach')
> at subscribe (/node_modules/socket.io/dist/uws.js:87:11)
> at Socket.<anonymous> (/node_modules/socket.io/dist/uws.js:28:17)
> at Socket.emit (node:events:402:35)
> at WebSocket.onPacket (/node_modules/engine.io/build/socket.js:214:22)
> at WebSocket.emit (node:events:390:28)
> at WebSocket.onPacket (/node_modules/engine.io/build/transport.js:92:14)
> at WebSocket.onData (/node_modules/engine.io/build/transport.js:101:14)
> at message (/node_modules/engine.io/build/userver.js:56:30)
Related: https://github.com/socketio/socket.io/issues/4443
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
Syntax:
```js
io.timeout(1000).emit("some-event", (err, responses) => {
// ...
});
```
The adapter exposes two additional methods:
- `broadcastWithAck(packets, opts, clientCountCallback, ack)`
Similar to `broadcast(packets, opts)`, but:
* `clientCountCallback()` is called with the number of clients that
received the packet (can be called several times in a cluster)
* `ack()` is called for each client response
- `serverCount()`
It returns the number of Socket.IO servers in the cluster (1 for the
in-memory adapter).
Those two methods will be implemented in the other adapters (Redis,
Postgres, MongoDB, ...).
Related:
- https://github.com/socketio/socket.io/issues/1811
- https://github.com/socketio/socket.io/issues/4163
- https://github.com/socketio/socket.io-redis-adapter/issues/445
The Socket instance is only considered connected when the "connection"
event is emitted, and not during the middleware(s) execution.
```js
io.use((socket, next) => {
console.log(socket.connected); // prints "false"
next();
});
io.on("connection", (socket) => {
console.log(socket.connected); // prints "true"
});
```
Related: https://github.com/socketio/socket.io/issues/4129
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
This header is useless, as the client bundle already contains a
sourceMappingURL field.
Besides, Firefox prints the following warning:
> <url> is being assigned a //# sourceMappingURL, but already has one
Related: https://github.com/socketio/socket.io/issues/3958