55 Commits

Author SHA1 Message Date
Damien Arrachequesne
d8143cc067 refactor: do not persist session if connection state recovery if disabled
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
2023-02-06 18:03:34 +01:00
Damien Arrachequesne
3734b74b45 revert: feat: expose current offset to allow deduplication
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.
2023-02-06 17:36:37 +01:00
Edouard Benauw
8aa94991ce feat: add description to the disconnecting and disconnect events (#4622)
See also: b862924b7f
2023-02-04 09:03:01 +01:00
Damien Arrachequesne
4e64123862 feat: expose current offset to allow deduplication
Related: 655dce9755
2023-02-04 08:56:55 +01:00
Damien Arrachequesne
115a9819fd refactor: do not include the pid by default
So that the client knows whether the connection state recovery feature
is enabled.

See also: 54d5ee05a6
2023-01-25 09:39:22 +01:00
Damien Arrachequesne
184f3cf7af feat: add promise-based acknowledgements
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
2023-01-23 09:06:25 +01:00
Damien Arrachequesne
f3ada7d8cc fix(typings): properly type emits with timeout
When emitting with a timeout (added in version 4.4.0), the "err"
argument was not properly typed and would require to split the client
and server typings. It will now be automatically inferred as an Error
object.

Workaround for previous versions:

```ts
type WithTimeoutAck<isEmitter extends boolean, args extends any[]> = isEmitter extends true ? [Error, ...args] : args;

interface ClientToServerEvents<isEmitter extends boolean = false> {
    withAck: (data: { argName: boolean }, callback: (...args: WithTimeoutAck<isEmitter, [string]>) => void) => void;
}

interface ServerToClientEvents<isEmitter extends boolean = false> {

}

const io = new Server<ClientToServerEvents, ServerToClientEvents<true>>(3000);

io.on("connection", (socket) => {
    socket.on("withAck", (val, cb) => {
        cb("123");
    });
});

const socket: Socket<ServerToClientEvents, ClientToServerEvents<true>> = ioc("http://localhost:3000");

socket.timeout(100).emit("withAck", { argName: true }, (err, val) => {
  // ...
});
```

Related: https://github.com/socketio/socket.io-client/issues/1555
2023-01-19 11:48:18 +01:00
Damien Arrachequesne
54d5ee05a6 feat: implement connection state recovery
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
2023-01-12 12:21:56 +01:00
Damien Arrachequesne
8fae95dd18 docs: add jsdoc for each public method 2022-10-14 10:30:08 +02:00
Damien Arrachequesne
e357daf585 fix(typings): apply types to "io.timeout(...).emit()" calls
Typed events were not applied when calling "io.timeout(...).emit()".

Related: https://github.com/socketio/socket.io-client/issues/1555#issuecomment-1277289733

Reference: https://socket.io/docs/v4/typescript/
2022-10-13 14:50:09 +02:00
Damien Arrachequesne
10fa4a2690 refactor: add list of possible disconnection reasons
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
2022-09-13 08:25:13 +02:00
Damien Arrachequesne
9890b036cf chore: bump dependencies
Production:

- socket.io-parser: ~4.0.4 => ~4.2.0

Development:

- superagent: ^6.1.0 => ^8.0.0
- tsd: ^0.17.0 => ^0.21.0

Related: https://github.com/socketio/socket.io/issues/3709
2022-06-27 09:16:08 +02:00
Damien Arrachequesne
18f3fdab12 fix: prevent the socket from joining a room after disconnection
Calling `socket.join()` after disconnection would lead to a memory
leak, because the room was never removed from the memory:

```js
io.on("connection", (socket) => {
  socket.disconnect();
  socket.join("room1"); // leak
});
```

Related:

- https://github.com/socketio/socket.io/issues/4067
- https://github.com/socketio/socket.io/issues/4380
2022-05-25 23:18:42 +02:00
Damien Arrachequesne
0b35dc77c0 refactor: make the protocol implementation stricter
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
2022-03-31 12:24:31 +02:00
Damien Arrachequesne
531104d332 feat: add support for catch-all listeners for outgoing packets
This is similar to `onAny()`, but for outgoing packets.

Syntax:

```js
socket.onAnyOutgoing((event, ...args) => {
  console.log(event);
});
```
2022-03-31 10:35:09 +02:00
Damien Arrachequesne
8b204570a9 feat: broadcast and expect multiple acks
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
2022-03-31 07:49:09 +02:00
Chris Swithinbank
02c87a8561 fix(typings): ensure compatibility with TypeScript 3.x (#4259)
Labeled tuple elements were added in TypeScript 4.0.

Reference: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-0.html#labeled-tuple-elements

Related: 44e20ba5bf
2022-01-25 01:25:05 +01:00
Orkhan Alikhanov
770ee5949f fix(types): make RemoteSocket.data type safe (#4234)
Related:

- https://github.com/socketio/socket.io/issues/4229
- fe8730ca0f
2022-01-06 07:14:55 +01:00
Shayan Yousefi
fc82e44f73 refactor(typings): export Event type (#4215)
So that it can be used by the end users:

```ts
const myMiddleware = ([eventName, ...args]: Event, next: (err?: Error) => void) => {
  console.log(eventName); // inferred as string
  next();
}

io.on("connection", (socket) => {
  socket.use(myMiddleware);
});
```
2022-01-05 08:08:18 +01:00
Damien Arrachequesne
b839a3b400 fix: prevent double ack when emitting with a timeout
The ack was not properly removed upon timeout, and could be called
twice.

Related: f0ed42f18c
2021-11-18 14:03:07 +01:00
Damien Arrachequesne
f0ed42f18c feat: add timeout feature
Usage:

```js
socket.timeout(5000).emit("my-event", (err) => {
  if (err) {
    // the client did not acknowledge the event in the given delay
  }
});
```
2021-11-16 20:07:53 +01:00
Damien Arrachequesne
02b0f73e2c fix: only set 'connected' to true after middleware execution
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
2021-11-12 07:31:52 +01:00
Damien Arrachequesne
c0d8c5ab23 feat: add an implementation based on uWebSockets.js
Usage:

```js
const { App } = require("uWebSockets.js");
const { Server } = require("socket.io");

const app = new App();
const server = new Server();

server.attachApp(app);

app.listen(3000);
```

The Adapter prototype is updated so we can benefit from the publish
functionality of uWebSockets.js, so this will apply to all adapters
extending the default adapter.

Reference: https://github.com/uNetworking/uWebSockets.js

Related:

- https://github.com/socketio/socket.io/issues/3601
- https://github.com/socketio/engine.io/issues/578
2021-11-12 07:01:55 +01:00
Nikita Kolmogorov
fe8730ca0f feat: add type information to socket.data (#4159)
Usage:

```js
interface SocketData {
  name: string;
  age: number;
}

const io = new Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents, SocketData>();

io.on("connection", (socket) => {
  socket.data.name = "john";
  socket.data.age = 42;
});
```
2021-11-08 15:21:48 +01:00
Naseem
44e20ba5bf refactor: add event type for use() (#4138) 2021-10-24 07:19:43 +02:00
Damien Arrachequesne
7c44893d78 chore: bump dependencies 2021-07-04 00:37:35 +02:00
Damien Arrachequesne
b84ed1e41c fix(typings): properly type server-side events
See also: 93cce05fb3
2021-05-11 23:59:18 +02:00
Damien Arrachequesne
93cce05fb3 feat: add support for inter-server communication
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
2021-05-11 00:07:20 +02:00
Maxime Kjaer
0107510ba8 feat: add support for typed events (#3822)
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
2021-03-10 00:18:13 +01:00
Damien Arrachequesne
b25495c069 feat: add some utility methods
This commit adds the following methods:

- fetchSockets: returns the matching socket instances

Syntax:

```js
// return all Socket instances
const sockets = await io.fetchSockets();

// return all Socket instances of the "admin" namespace in the "room1" room
const sockets = await io.of("/admin").in("room1").fetchSockets();
```

- socketsJoin: makes the matching socket instances join the specified rooms

Syntax:

```js
// make all Socket instances join the "room1" room
io.socketsJoin("room1");

// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
```

- socketsLeave: makes the matching socket instances leave the specified rooms

Syntax:

```js
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");

// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
```

- disconnectSockets: makes the matching socket instances disconnect

Syntax:

```js
// make all Socket instances disconnect
io.disconnectSockets();

// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
```

Those methods share the same semantics as broadcasting. They will also
work with multiple Socket.IO servers when using the Redis adapter. In
that case, the fetchSockets() method will return a list of RemoteSocket
instances, which expose a subset of the methods and attributes of the
Socket class (the "request" attribute cannot be mocked, for example).

Related:

- https://github.com/socketio/socket.io/issues/3042
- https://github.com/socketio/socket.io/issues/3418
- https://github.com/socketio/socket.io/issues/3570
- https://github.com/socketio/socket.io-redis/issues/283
2021-03-02 11:17:21 +01:00
Damien Arrachequesne
085d1de9df feat: allow to pass an array to io.to(...)
In some cases it is necessary to pass an array of rooms instead of a single room.

New syntax:

```
io.to(["room1", "room2"]).except(["room3"]).emit(...);

socket.to(["room1", "room2"]).except(["room3"]).emit(...);
```

Related: https://github.com/socketio/socket.io/issues/3048
2021-03-01 23:20:46 +01:00
Damien Arrachequesne
ac9e8ca6c7 fix: make io.to(...) immutable
Previously, broadcasting to a given room (by calling `io.to()`) would
mutate the io instance, which could lead to surprising behaviors, like:

```js
io.to("room1");
io.to("room2").emit(...); // also sent to room1

// or with async/await
io.to("room3").emit("details", await fetchDetails()); // random behavior: maybe in room3, maybe to all clients
```

Calling `io.to()` (or any other broadcast modifier) will now return an
immutable instance.

Related:

- https://github.com/socketio/socket.io/issues/3431
- https://github.com/socketio/socket.io/issues/3444
2021-03-01 23:17:08 +01:00
Sebastiaan Marynissen
7de2e87e88 feat: allow to exclude specific rooms when broadcasting (#3789)
New syntax:

```
io.except("room1").emit(...);
io.to("room1").except("room2").emit(...);

socket.broadcast.except("room1").emit(...);
socket.to("room1").except("room2").emit(...);
```

Related:

- https://github.com/socketio/socket.io/issues/3629
- https://github.com/socketio/socket.io/issues/3657
2021-03-01 09:30:58 +01:00
Damien Arrachequesne
494c64e44f fix: ignore packet received after disconnection
Related: https://github.com/socketio/socket.io/issues/3095
2021-02-26 00:58:30 +01:00
Damien Arrachequesne
4f2e9a716d fix(typings): update the types of "query", "auth" and "headers"
Related: https://github.com/socketio/socket.io/issues/3770
2021-02-03 22:53:38 +01:00
david-fong
9e8f288ca9 fix(typings): add return types and general-case overload signatures (#3776)
See also: https://stackoverflow.com/questions/52760509/typescript-returntype-of-overloaded-function/52760599#52760599
2021-02-02 11:50:08 +01:00
Damien Arrachequesne
9925746c8e feat: add support for Socket.IO v2 clients
In order to ease the migration to Socket.IO v3, the Socket.IO server
can now communicate with v2 clients.

```js
const io = require("socket.io")({
  allowEIO3: true
});
```

This feature is disabled by default.
2021-01-14 23:38:24 +01:00
Damien Arrachequesne
bf54327421 revert: restore the socket middleware functionality
This functionality was removed in [1] (included in 3.0.0), but
catch-all listeners and socket middleware features are complementary
rather than mutually exclusive.

The only difference with the previous implementation is that passing an
error to the `next` handler will create an error on the server-side,
and not on the client-side anymore.

```js
io.on("connection", (socket) => {

  socket.use(([ event, ...args ], next) => {
    next(new Error("stop"));
  });

  socket.on("error", (err) => {
    // to restore the previous behavior
    socket.emit("error", err);

    // or close the connection, depending on your use case
    socket.disconnect(true);
  });
});
```

This creates additional possibilities about custom error handlers, which
may be implemented in the future.

```js
// user-defined error handler
socket.use((err, [ event ], next) => {
  // either handle it
  socket.disconnect();

  // or forward the error to the default error handler
  next(err);
});

// default error handler
socket.use((err, _, next) => {
  socket.emit("error", err);
});
```

Related: https://github.com/socketio/socket.io/issues/3678

[1]: 5c73733985
2021-01-05 11:51:50 +01:00
David Fong
d1bfe40dbb refactor: add more typing info and upgrade prettier (#3725)
This upgrades prettier to 2.2.0 to gain support for TypeScript's new
type-only-imports feature.
2020-12-11 12:19:20 +01:00
Damien Arrachequesne
50671d984a fix(typings): update the signature of the emit method
The previous signature was not compatible with EventEmitter.emit(). The typescript compilation threw:

```
node_modules/socket.io/dist/namespace.d.ts(89,5): error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type 'EventEmitter'.
  Type '(ev: string, ...args: any[]) => Namespace' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
    Type 'Namespace' is not assignable to type 'boolean'.
node_modules/socket.io/dist/socket.d.ts(84,5): error TS2416: Property 'emit' in type 'Socket' is not assignable to the same property in base type 'EventEmitter'.
  Type '(ev: string, ...args: any[]) => this' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
    Type 'this' is not assignable to type 'boolean'.
      Type 'Socket' is not assignable to type 'boolean'.
```

Note: the emit calls cannot be chained anymore:

```js
socket.emit("hello").emit("world"); // will not work anymore
```
2020-11-08 00:07:56 +01:00
Damien Arrachequesne
d16c035d25 refactor: rename ERROR to CONNECT_ERROR
The meaning is not modified: this packet type is still used by the
server when the connection to a namespace is refused.
2020-10-26 00:29:11 +01:00
Damien Arrachequesne
5c73733985 feat: add support for catch-all listeners
Inspired from EventEmitter2 [1]

```js
io.on("connect", socket => {

  socket.onAny((event, ...args) => {});

  socket.prependAny((event, ...args) => {});

  socket.offAny(); // remove all listeners

  socket.offAny(listener);

  const listeners = socket.listenersAny();
});
```

Breaking change: the socket.use() method is removed

This method was introduced in [2] for the same feature (having a
catch-all listener), but there were two issues:

- the API is not very user-friendly, since the user has to know the structure of the packet argument
- it uses an ERROR packet, which is reserved for Namespace authentication issues (see [3])

[1]: https://github.com/EventEmitter2/EventEmitter2
[2]: https://github.com/socketio/socket.io/issues/434
[3]: https://github.com/socketio/socket.io-protocol
2020-10-25 23:44:01 +01:00
Damien Arrachequesne
129c6417bd feat: make Socket#join() and Socket#leave() synchronous
Depending on the adapter, Socket#join() may return:

- nothing (in-memory and Redis adapters)
- a promise (custom adapters)

Breaking change: Socket#join() and Socket#leave() do not accept a
callback argument anymore.

Before:

```js
socket.join("room1", () => {
 io.to("room1").emit("hello");
});
```

After:

```
socket.join("room1");
io.to("room1").emit("hello");
// or await socket.join("room1"); for custom adapters
```

Note: the need for an asynchronous method came from the Redis adapter,
which did override the Adapter#add() method in earlier versions, but
this is not the case anymore.

Reference:

- https://github.com/socketio/socket.io/blob/2.3.0/lib/socket.js#L236-L258
- https://github.com/socketio/socket.io-adapter/blob/1.1.2/index.js#L56-L65
- 05f926e13e

Related: https://github.com/socketio/socket.io/issues/3662
2020-10-22 01:50:13 +02:00
Damien Arrachequesne
8a5db7fa36 refactor: remove duplicate _sockets map
Both the "connected" and the "_sockets" maps were used to track the
Socket instances in the namespace.

Let's merge them into "sockets". It's a breaking change, but:

- the "sockets" object did already exist in Socket.IO v2 (and appears in some examples/tutorials)
- "sockets" makes more sense than "connected" in my opinion
- there was already a breaking change regarding the "connected" property (from object to Map)

Breaking change: the "connected" map is renamed to "sockets"
2020-10-15 12:45:42 +02:00
Damien Arrachequesne
58b66f8089 refactor: hide internal methods and properties
There is no concept of package-private methods in TypeScript, so we'll
just prefix them with "_" and mark them as private in the JSDoc.
2020-10-15 11:54:06 +02:00
Damien Arrachequesne
669592d120 feat: move binary detection back to the parser
See 285e7cd0d8

Breaking change: the Socket#binary() method is removed, as this use
case is now covered by the ability to provide your own parser.
2020-10-15 10:45:56 +02:00
Damien Arrachequesne
4bd5b2339a feat: throw upon reserved event names
These events cannot be used by the end users, because they are part of
the Socket.IO public API, so using them will now throw an error
explicitly.
2020-10-13 23:02:09 +02:00
Damien Arrachequesne
83a2356648 refactor: properly delegate to the main namespace 2020-10-13 23:02:08 +02:00
Damien Arrachequesne
2875d2cfdf feat: do not reuse the Engine.IO id
In previous versions, the Socket#id attribute was equal (or derived,
for a non-default namespace) to the underlying Engine.IO id, which is
used as a mean to authenticate the user throughout the Engine.IO
session and thus is sensitive information that should be kept secret.

The problem with reusing the Engine.IO id is that users could be
tempted to transmit this id to other clients, in order to implement
private messaging for example.

So we'll now generate a new random id for each new socket.

Please note that this id will now be different from the one found in
the query parameters of the HTTP requests.
2020-10-13 23:02:07 +02:00
Damien Arrachequesne
3289f7ec37 feat: remove the implicit connection to the default namespace
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"
  }
});
```
2020-10-13 23:02:07 +02:00