Compare commits

...

38 Commits
4.5.2 ... 4.6.1

Author SHA1 Message Date
Damien Arrachequesne
7952312911 chore(release): 4.6.1
Diff: https://github.com/socketio/socket.io/compare/4.6.0...4.6.1
2023-02-20 17:49:41 +01:00
Damien Arrachequesne
0d0a7a22b5 fix: properly handle manually created dynamic namespaces
Namespaces that match the regex of a parent namespace will now be added
as a child of this namespace:

```js
const parentNamespace = io.of(/^\/dynamic-\d+$/);
const childNamespace = io.of("/dynamic-101");
```

Related:

- https://github.com/socketio/socket.io/issues/4615
- https://github.com/socketio/socket.io/issues/4164
- https://github.com/socketio/socket.io/issues/4015
- https://github.com/socketio/socket.io/issues/3960
2023-02-20 01:19:01 +01:00
Damien Arrachequesne
2a8565fd1e refactor: catch errors when trying to restore the connection state 2023-02-20 01:18:08 +01:00
Igor Lins e Silva
d0b22c6302 fix(types): fix nodenext module resolution compatibility (#4625)
The import added in [1] was invalid, because it used an non-exported
class.

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

[1]: d4a9b2cdcb
2023-02-20 01:15:35 +01:00
Nabaraj Subedi
e71f3d7dbe docs: minor style fix (#4619) 2023-02-16 09:25:43 +01:00
Damien Arrachequesne
a2e5d1f77f chore(release): 4.6.0
Diff: https://github.com/socketio/socket.io/compare/4.5.4...4.6.0
2023-02-07 01:07:46 +01:00
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
b2dd7cf660 chore: bump engine.io to version 6.4.0
Diff: https://github.com/socketio/engine.io/compare/6.3.1...6.4.0
Release notes: https://github.com/socketio/engine.io/releases/tag/6.4.0
2023-02-06 17:43:02 +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
Waldemar Schlegel
0c0eb00163 fix: add timeout method to remote socket (#4558)
The RemoteSocket interface, which is returned when the client is
connected on another Socket.IO server of the cluster, was lacking the
`timeout()` method.

Syntax:

```js
const sockets = await io.fetchSockets();

for (const socket of sockets) {
  if (someCondition) {
    socket.timeout(1000).emit("some-event", (err) => {
      if (err) {
        // the client did not acknowledge the event in the given delay
      }
    });
  }
}
```

Related: https://github.com/socketio/socket.io/issues/4595
2023-01-24 09:24:19 +01:00
Damien Arrachequesne
f8640d9451 refactor: export DisconnectReason type
Related: https://github.com/socketio/socket.io/issues/4556
2023-01-23 09:27:32 +01:00
Damien Arrachequesne
93d446a545 refactor: add charset when serving the bundle files
Reference: https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Content-Type

Related: https://github.com/socketio/socket.io/discussions/4589
2023-01-23 09:27:06 +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
Steve Baum
5d9220b69a feat: add the ability to clean up empty child namespaces (#4602)
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
2023-01-23 07:56:14 +01:00
Damien Arrachequesne
129883958a test: add test with onAnyOutgoing() and binary attachments
Related:

- https://github.com/socketio/socket.io/issues/4374
- ae8dd88995
2023-01-19 12:05:35 +01:00
Damien Arrachequesne
6c27b8b0a6 test: add test with socket.disconnect(true)
Related: a65a047526
2023-01-19 11:53:53 +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
Marc Jansing
a21ad88828 docs(changelog): add note about maxHttpBufferSize default value (#4596)
Reference: https://github.com/socketio/socket.io/releases/tag/2.5.0
2023-01-18 08:16:27 +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
da2b542797 perf: precompute the WebSocket frames when broadcasting
Note:

- only packets without binary attachments are affected
- the permessage-deflate extension must be disabled (which is the default)

Related:

- 5f7b47d40f
- 5e34722b0b
2023-01-12 08:50:07 +01:00
Tristan F
b7d54dbe8d docs: add Rust client implementation (#4592)
client-only implementation -- it *may* add server-side support in the future.
2023-01-12 06:26:22 +01:00
Tristan F
d4a9b2cdcb refactor(typings): add types for io.engine (#4591)
This adds typings for the socket.io engine field, which offers better
IntelliSense when retrieving the server, as well as more confidence on
the developer-side of what types of fields are entering the server.

Related: https://github.com/socketio/socket.io/issues/4590
2023-01-11 10:45:57 +01:00
Damien Arrachequesne
547c541fb9 chore: add security policy 2022-12-14 07:47:51 +01:00
Damien Arrachequesne
3b7ced7af7 chore(release): 4.5.4
Diff: https://github.com/socketio/socket.io/compare/4.5.3...4.5.4
2022-11-22 22:45:13 +01:00
Damien Arrachequesne
c00bb9564c chore: bump engine.io to version 6.2.1
In order to fix CVE-2022-41940.

See also: https://github.com/socketio/engine.io/security/advisories/GHSA-r7qp-cfhv-p84w
2022-11-22 22:35:53 +01:00
Damien Arrachequesne
57e5f25e26 chore: bump socket.io-parser to version 4.2.1
In order to fix CVE-2022-2421.

See also: https://github.com/advisories/GHSA-qm95-pgcg-qqfq
2022-11-22 22:32:09 +01:00
Damien Arrachequesne
f4b698418a docs: add missing versions in the changelog 2022-11-02 08:40:26 +01:00
Damien Arrachequesne
945c84be47 chore(release): 4.5.3
Diff: https://github.com/socketio/socket.io/compare/4.5.2...4.5.3
2022-10-15 07:14:45 +02:00
Damien Arrachequesne
d3d0a2d5be fix(typings): accept an HTTP2 server in the constructor
Related:

- https://github.com/socketio/socket.io/issues/4434
- https://github.com/socketio/socket.io/issues/4494
2022-10-14 11:24:36 +02:00
Damien Arrachequesne
19b225b0c8 docs(examples): update dependencies of the basic CRUD example
Related: https://github.com/socketio/socket.io/issues/4409
2022-10-14 10:39:40 +02:00
Damien Arrachequesne
8fae95dd18 docs: add jsdoc for each public method 2022-10-14 10:30:08 +02:00
Damien Arrachequesne
e6f6b906db docs: add deprecation notice for the allSockets() method 2022-10-13 15:02:23 +02:00
Damien Arrachequesne
596eb88af7 ci: upgrade to actions/checkout@3 and actions/setup-node@3
Reference: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2022-10-13 14:50:09 +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
33 changed files with 3305 additions and 753 deletions

View File

@@ -12,18 +12,25 @@ permissions:
jobs:
test-node:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
CI: true

View File

@@ -1,3 +1,283 @@
# History
## 2023
- [4.6.1](#461-2023-02-20) (Feb 2023)
- [4.6.0](#460-2023-02-07) (Feb 2023)
## 2022
- [4.5.4](#454-2022-11-22) (Nov 2022)
- [4.5.3](#453-2022-10-15) (Oct 2022)
- [4.5.2](#452-2022-09-02) (Sep 2022)
- [2.5.0](#250-2022-06-26) (Jun 2022) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [4.5.1](#451-2022-05-17) (May 2022)
- [4.5.0](#450-2022-04-23) (Apr 2022)
- [4.4.1](#441-2022-01-06) (Jan 2022)
## 2021
- [4.4.0](#440-2021-11-18) (Nov 2021)
- [4.3.2](#432-2021-11-08) (Nov 2021)
- [4.3.1](#431-2021-10-16) (Oct 2021)
- [4.3.0](#430-2021-10-14) (Oct 2021)
- [4.2.0](#420-2021-08-30) (Aug 2021)
- [4.1.3](#413-2021-07-10) (Jul 2021)
- [4.1.2](#412-2021-05-17) (May 2021)
- [4.1.1](#411-2021-05-11) (May 2021)
- [4.1.0](#410-2021-05-11) (May 2021)
- [4.0.2](#402-2021-05-06) (May 2021)
- [4.0.1](#401-2021-03-31) (Mar 2021)
- [**4.0.0**](#400-2021-03-10) (Mar 2021)
- [3.1.2](#312-2021-02-26) (Feb 2021)
- [3.1.1](#311-2021-02-03) (Feb 2021)
- [3.1.0](#310-2021-01-15) (Jan 2021)
- [2.4.1](#241-2021-01-07) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [3.0.5](#305-2021-01-05) (Jan 2021)
- [2.4.0](#240-2021-01-04) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
## 2020
- [3.0.4](#304-2020-12-07) (Dec 2020)
- [3.0.3](#303-2020-11-19) (Nov 2020)
- [3.0.2](#302-2020-11-17) (Nov 2020)
- [3.0.1](#301-2020-11-09) (Nov 2020)
- [**3.0.0**](#300-2020-11-05) (Nov 2020)
## 2019
- [2.3.0](#230-2019-09-20) (Sep 2019)
## 2018
- [2.2.0](#220-2018-11-29) (Nov 2018)
- [2.1.1](#211-2018-05-17) (May 2018)
- [2.1.0](#210-2018-03-29) (Mar 2018)
# Release notes
## [4.6.1](https://github.com/socketio/socket.io/compare/4.6.0...4.6.1) (2023-02-20)
### Bug Fixes
* properly handle manually created dynamic namespaces ([0d0a7a2](https://github.com/socketio/socket.io/commit/0d0a7a22b5ff95f864216c529114b7dd41738d1e))
* **types:** fix nodenext module resolution compatibility ([#4625](https://github.com/socketio/socket.io/issues/4625)) ([d0b22c6](https://github.com/socketio/socket.io/commit/d0b22c630208669aceb7ae013180c99ef90279b0))
### Dependencies
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) (no change)
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) (no change)
## [4.6.0](https://github.com/socketio/socket.io/compare/4.5.4...4.6.0) (2023-02-07)
### Bug Fixes
* add timeout method to remote socket ([#4558](https://github.com/socketio/socket.io/issues/4558)) ([0c0eb00](https://github.com/socketio/socket.io/commit/0c0eb0016317218c2be3641e706cfaa9bea39a2d))
* **typings:** properly type emits with timeout ([f3ada7d](https://github.com/socketio/socket.io/commit/f3ada7d8ccc02eeced2b9b9ac8e4bc921eb630d2))
### Features
#### 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
}
```
Added in [184f3cf](https://github.com/socketio/socket.io/commit/184f3cf7af57acc4b0948eee307f25f8536eb6c8).
#### Connection state recovery
This feature 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
The in-memory adapter already supports this feature, and we will soon update the Postgres and MongoDB adapters. We will also create a new adapter based on [Redis Streams](https://redis.io/docs/data-types/streams/), which will support this feature.
Added in [54d5ee0](https://github.com/socketio/socket.io/commit/54d5ee05a684371191e207b8089f09fc24eb5107).
#### Compatibility (for real) with Express middlewares
This feature implements middlewares at the Engine.IO level, because Socket.IO middlewares are meant for namespace authorization and are not executed during a classic HTTP request/response cycle.
Syntax:
```js
io.engine.use((req, res, next) => {
// do something
next();
});
// with express-session
import session from "express-session";
io.engine.use(session({
secret: "keyboard cat",
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}));
// with helmet
import helmet from "helmet";
io.engine.use(helmet());
```
A workaround was possible by using the allowRequest option and the "headers" event, but this feels way cleaner and works with upgrade requests too.
Added in [24786e7](https://github.com/socketio/engine.io/commit/24786e77c5403b1c4b5a2bc84e2af06f9187f74a).
#### Error details in the disconnecting and disconnect events
The `disconnect` event will now contain additional details about the disconnection reason.
```js
io.on("connection", (socket) => {
socket.on("disconnect", (reason, description) => {
console.log(description);
});
});
```
Added in [8aa9499](https://github.com/socketio/socket.io/commit/8aa94991cee5518567d6254eec04b23f81510257).
#### Automatic removal of empty child namespaces
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.
```js
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cleanupEmptyChildNamespaces: true
});
```
Added in [5d9220b](https://github.com/socketio/socket.io/commit/5d9220b69adf73e086c27bbb63a4976b348f7c4c).
#### A new "addTrailingSlash" option
The trailing slash which was added by default can now be disabled:
```js
import { createServer } from "node:http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
addTrailingSlash: false
});
```
In the example above, the clients can omit the trailing slash and use `/socket.io` instead of `/socket.io/`.
Added in [d0fd474](https://github.com/socketio/engine.io/commit/d0fd4746afa396297f07bb62e539b0c1c4018d7c).
### Performance Improvements
* precompute the WebSocket frames when broadcasting ([da2b542](https://github.com/socketio/socket.io/commit/da2b54279749adc5279c9ac4742b01b36c01cff0))
### Dependencies
- [`engine.io@~6.4.0`](https://github.com/socketio/engine.io/releases/tag/6.4.0) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
- [`ws@~8.11.0`](https://github.com/websockets/ws/releases/tag/8.11.0) ([diff](https://github.com/websockets/ws/compare/8.2.3...8.11.0))
## [4.5.4](https://github.com/socketio/socket.io/compare/4.5.3...4.5.4) (2022-11-22)
This release contains a bump of:
- `engine.io` in order to fix [CVE-2022-41940](https://github.com/socketio/engine.io/security/advisories/GHSA-r7qp-cfhv-p84w)
- `socket.io-parser` in order to fix [CVE-2022-2421](https://github.com/advisories/GHSA-qm95-pgcg-qqfq).
### Dependencies
- [`engine.io@~6.2.1`](https://github.com/socketio/engine.io/releases/tag/6.2.1) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3) (no change)
## [4.5.3](https://github.com/socketio/socket.io/compare/4.5.2...4.5.3) (2022-10-15)
### Bug Fixes
* **typings:** accept an HTTP2 server in the constructor ([d3d0a2d](https://github.com/socketio/socket.io/commit/d3d0a2d5beaff51fd145f810bcaf6914213f8a06))
* **typings:** apply types to "io.timeout(...).emit()" calls ([e357daf](https://github.com/socketio/socket.io/commit/e357daf5858560bc84e7e50cd36f0278d6721ea1))
## [4.5.2](https://github.com/socketio/socket.io/compare/4.5.1...4.5.2) (2022-09-02)
@@ -8,6 +288,24 @@
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)
⚠️ WARNING ⚠️
The default value of the maxHttpBufferSize option has been decreased from 100 MB to 1 MB, in order to prevent attacks by denial of service.
Security advisory: [GHSA-j4f2-536g-r55m](https://github.com/advisories/GHSA-j4f2-536g-r55m)
### Bug Fixes
* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))
## [4.5.1](https://github.com/socketio/socket.io/compare/4.5.0...4.5.1) (2022-05-17)
@@ -244,6 +542,16 @@ we only add a field in the JSON-encoded handshake data:
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)
### Reverts
* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)
@@ -257,6 +565,17 @@ we only add a field in the JSON-encoded handshake data:
* restore the socket middleware functionality ([bf54327](https://github.com/socketio/socket.io/commit/bf5432742158e4d5ba2722cff4a614967dffa5b9))
# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)
### Bug Fixes
* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))
## [3.0.4](https://github.com/socketio/socket.io/compare/3.0.3...3.0.4) (2020-12-07)
@@ -542,3 +861,78 @@ io.of("/admin").use((socket, next) => {
This method was kept for backward-compatibility with pre-1.0 versions.
# [2.3.0](https://github.com/socketio/socket.io/compare/2.2.0...2.3.0) (2019-09-20)
This release mainly contains a bump of the `engine.io` and `ws` packages, but no additional features.
# [2.2.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-11-29)
### Features
- add cache-control header when serving the client source ([#2907](https://github.com/socketio/socket.io/pull/2907)) ([b00ae50](https://github.com/socketio/socket.io/commit/b00ae50be65d1bc88fa95145f1c486a6886a6b76))
### Bug fixes
- throw an error when trying to access the clients of a dynamic namespace ([#3355](https://github.com/socketio/socket.io/pull/3355)) ([a7fbd1a](https://github.com/socketio/socket.io/commit/a7fbd1ac4a47cafd832fc62e371754df924c5903))
# [2.1.1](https://github.com/socketio/socket.io/compare/2.1.0...2.1.1) (2018-05-17)
### Features
- add local flag to the socket object ([#3129](https://github.com/socketio/socket.io/pull/3219)) ([1decae3](https://github.com/socketio/socket.io/commit/1decae341c80c0417b32d3124ca30c005240b48a))
```js
socket.local.to('room101').emit(/* */);
```
# [2.1.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-03-29)
### Features
- add a 'binary' flag ([#3185](https://github.com/socketio/socket.io/pull/3185)) ([f48a06c](https://github.com/socketio/socket.io/commit/f48a06c040280b44f90fd225c888910544fd63b5))
```js
// by default, the object is recursively scanned to check whether it contains some binary data
// in the following example, the check is skipped in order to improve performance
socket.binary(false).emit('plain-object', object);
// it also works at the namespace level
io.binary(false).emit('plain-object', object);
```
- add support for dynamic namespaces ([#3195](https://github.com/socketio/socket.io/pull/3195)) ([c0c79f0](https://github.com/socketio/socket.io/commit/c0c79f019e7138194e438339f8192705957c8ec3))
```js
io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
// socket.nsp.name = '/dynamic-101'
});
// client-side
const client = require('socket.io-client')('/dynamic-101');
```
### Bug fixes
- properly emit 'connect' when using a custom namespace ([#3197](https://github.com/socketio/socket.io/pull/3197)) ([f4fc517](https://github.com/socketio/socket.io/commit/f4fc517e0fe25866c95b584291487b8cbdff889d))
- include the protocol in the origins check ([#3198](https://github.com/socketio/socket.io/pull/3198)) ([1f1d64b](https://github.com/socketio/socket.io/commit/1f1d64bab61a273712a199591a3f76210d8c0959))
### Important note :warning: from Engine.IO [3.2.0 release](https://github.com/socketio/engine.io/releases/tag/3.2.0)
There are two non-breaking changes that are somehow quite important:
- `ws` was reverted as the default wsEngine (https://github.com/socketio/engine.io/pull/550), as there was several blocking issues with `uws`. You can still use `uws` by running `npm install uws --save` in your project and using the `wsEngine` option:
```js
var engine = require('engine.io');
var server = engine.listen(3000, {
wsEngine: 'uws'
});
```
- `pingTimeout` now defaults to 5 seconds (instead of 60 seconds): https://github.com/socketio/engine.io/pull/551

View File

@@ -21,6 +21,7 @@ Some implementations in other languages are also available:
- [Dart](https://github.com/rikulo/socket.io-client-dart)
- [Python](https://github.com/miguelgrinberg/python-socketio)
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
- [Rust](https://github.com/1c3t3a/rust-socketio)
Its main features are:
@@ -125,7 +126,7 @@ io.listen(3000);
Starting with **3.0**, express applications have become request handler
functions that you pass to `http` or `http` `Server` instances. You need
to pass the `Server` to `socket.io`, and not the express application
to pass the `Server` to `socket.io`, not the express application
function. Also make sure to call `.listen` on the `server`, not the `app`.
```js

22
SECURITY.md Normal file
View File

@@ -0,0 +1,22 @@
# Security Policy
## Supported Versions
| Version | Supported |
|---------|--------------------|
| 4.x | :white_check_mark: |
| 3.x | :white_check_mark: |
| 2.4.x | :white_check_mark: |
| < 2.4.0 | :x: |
## Reporting a Vulnerability
To report a security vulnerability in this package, please send an email to [@darrachequesne](https://github.com/darrachequesne) (see address in profile) describing the vulnerability and how to reproduce it.
We will get back to you as soon as possible and publish a fix if necessary.
:warning: IMPORTANT :warning: please do not create an issue in this repository, as attackers might take advantage of it. Thank you in advance for your responsible disclosure.
## History
No security vulnerability were reported yet.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -24,14 +24,14 @@
"uuid": "^8.3.2"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
"@types/chai": "^4.2.16",
"@types/mocha": "^10.0.0",
"@types/uuid": "^8.3.0",
"chai": "^4.3.4",
"mocha": "^8.3.2",
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"socket.io-client": "^4.0.1",
"ts-node": "^9.1.1",
"ts-node": "^10.9.1",
"typescript": "^4.2.4"
}
}

View File

@@ -7,6 +7,11 @@ import type {
EventNames,
EventsMap,
TypedEventBroadcaster,
DecorateAcknowledgements,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
SecondArg,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
@@ -16,24 +21,35 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {}
private readonly flags: BroadcastFlags & {
expectSingleResponse?: boolean;
} = {}
) {}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public to(room: Room | Room[]) {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room);
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
rooms,
this.exceptRooms,
@@ -42,33 +58,43 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // disconnect all clients in the "room-101" room
* io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public in(room: Room | Room[]) {
return this.to(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room);
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
exceptRooms,
@@ -79,15 +105,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -100,12 +126,14 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -116,12 +144,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return a new BroadcastOperator instance
* @public
* @example
* // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents, SocketData> {
public get local() {
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -132,31 +163,45 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
/**
* Adds a timeout in milliseconds for the next operation
*
* <pre><code>
*
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
return new BroadcastOperator<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, this.rooms, this.exceptRooms, flags);
}
/**
* Emits to all clients.
*
* @example
* // the “foo” event will be broadcast to all connected clients
* io.emit("foo", "bar");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an acknowledgement expected from all connected clients
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
@@ -190,7 +235,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
const timer = setTimeout(() => {
timedOut = true;
ack.apply(this, [new Error("operation has timed out"), responses]);
ack.apply(this, [
new Error("operation has timed out"),
this.flags.expectSingleResponse ? null : responses,
]);
}, this.flags.timeout);
let expectedServerCount = -1;
@@ -204,7 +252,10 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
responses.length === expectedClientCount
) {
clearTimeout(timer);
ack.apply(this, [null, responses]);
ack.apply(this, [
null,
this.flags.expectSingleResponse ? null : responses,
]);
}
};
@@ -236,10 +287,41 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
return true;
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* 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
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
err.responses = responses;
return reject(err);
} else {
return resolve(responses);
}
});
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>));
});
}
/**
* Gets a list of clients.
*
* @public
* @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
@@ -251,13 +333,30 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}
/**
* Returns the matching socket instances
* Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.
*
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets<SocketData = any>(): Promise<
RemoteSocket<EmitEvents, SocketData>[]
> {
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
@@ -280,10 +379,19 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}
/**
* Makes the matching socket instances join the specified rooms
* Makes the matching socket instances join the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
@@ -297,10 +405,18 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}
/**
* Makes the matching socket instances leave the specified rooms
* Makes the matching socket instances leave the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
@@ -314,10 +430,18 @@ export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
}
/**
* Makes the matching socket instances disconnect
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
@@ -359,7 +483,44 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
this.handshake = details.handshake;
this.rooms = new Set(details.rooms);
this.data = details.data;
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
this.operator = new BroadcastOperator<EmitEvents, SocketData>(
adapter,
new Set([this.id]),
new Set(),
{
expectSingleResponse: true, // so that remoteSocket.emit() with acknowledgement behaves like socket.emit()
}
);
}
/**
* Adds a timeout in milliseconds for the next operation.
*
* @example
* const sockets = await io.fetchSockets();
*
* for (const socket of sockets) {
* if (someCondition) {
* socket.timeout(1000).emit("some-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* }
* }
*
* // note: if possible, using a room instead of looping over all sockets is preferable
* io.timeout(1000).to(someConditionRoom).emit("some-event", (err, responses) => {
* // ...
* });
*
* @param timeout
*/
public timeout(timeout: number) {
return this.operator.timeout(timeout) as BroadcastOperator<
DecorateAcknowledgements<EmitEvents>,
SocketData
>;
}
public emit<Ev extends EventNames<EmitEvents>>(
@@ -373,7 +534,6 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room);
@@ -383,7 +543,6 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room);
@@ -394,8 +553,6 @@ export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close);

View File

@@ -18,6 +18,13 @@ interface WriteOptions {
wsPreEncoded?: string;
}
type CloseReason =
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error";
export class Client<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
@@ -107,7 +114,7 @@ export class Client<
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}): void {
private connect(name: string, auth: Record<string, unknown> = {}): void {
if (this.server._nsps.has(name)) {
debug("connecting to namespace %s", name);
return this.doConnect(name, auth);
@@ -145,10 +152,10 @@ export class Client<
*
* @private
*/
private doConnect(name: string, auth: object): void {
private doConnect(name: string, auth: Record<string, unknown>): void {
const nsp = this.server.of(name);
const socket = nsp._add(this, auth, () => {
nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket);
this.nsps.set(nsp.name, socket);
@@ -221,7 +228,7 @@ export class Client<
}
private writeToEngine(
encodedPackets: Array<String | Buffer>,
encodedPackets: Array<string | Buffer>,
opts: WriteOptions
): void {
if (opts.volatile && !this.conn.transport.writable) {
@@ -260,7 +267,7 @@ export class Client<
*/
private ondecoded(packet: Packet): void {
let namespace: string;
let authPayload;
let authPayload: Record<string, unknown>;
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
namespace = parsed.pathname!;
@@ -304,9 +311,13 @@ export class Client<
* Called upon transport close.
*
* @param reason
* @param description
* @private
*/
private onclose(reason: string): void {
private onclose(
reason: CloseReason | "forced server close",
description?: any
): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event
@@ -314,7 +325,7 @@ export class Client<
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason);
socket._onclose(reason, description);
}
this.sockets.clear();

View File

@@ -1,26 +1,31 @@
import http = require("http");
import type { Server as HTTPSServer } from "https";
import type { Http2SecureServer } from "http2";
import { createReadStream } from "fs";
import { createDeflate, createGzip, createBrotliCompress } from "zlib";
import accepts = require("accepts");
import { pipeline } from "stream";
import path = require("path");
import {
attach,
Server as Engine,
import { attach, Server as Engine, uServer } from "engine.io";
import type {
ServerOptions as EngineOptions,
AttachOptions,
uServer,
BaseServer,
} from "engine.io";
import { Client } from "./client";
import { EventEmitter } from "events";
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace";
import { ParentNamespace } from "./parent-namespace";
import { Adapter, Room, SocketId } from "socket.io-adapter";
import {
Adapter,
SessionAwareAdapter,
Room,
SocketId,
} from "socket.io-adapter";
import * as parser from "socket.io-parser";
import type { Encoder } from "socket.io-parser";
import debugModule from "debug";
import { Socket } from "./socket";
import { Socket, DisconnectReason } from "./socket";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
import {
EventsMap,
@@ -28,6 +33,11 @@ import {
EventParams,
StrictEventEmitter,
EventNames,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
FirstArg,
SecondArg,
} from "./typed-events";
import { patchAdapter, restoreAdapter, serveFile } from "./uws";
@@ -70,8 +80,58 @@ interface ServerOptions extends EngineOptions, AttachOptions {
* @default 45000
*/
connectTimeout: number;
/**
* Whether to enable the recovery of connection state when a client temporarily disconnects.
*
* The connection state includes the missed packets, the rooms the socket was in and the `data` attribute.
*/
connectionStateRecovery: {
/**
* The backup duration of the sessions and the packets.
*
* @default 120000 (2 minutes)
*/
maxDisconnectionDuration?: number;
/**
* Whether to skip middlewares upon successful connection state recovery.
*
* @default true
*/
skipMiddlewares?: boolean;
};
/**
* Whether to remove child namespaces that have no sockets connected to them
* @default false
*/
cleanupEmptyChildNamespaces: boolean;
}
/**
* Represents a Socket.IO server.
*
* @example
* import { Server } from "socket.io";
*
* const io = new Server();
*
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*
* io.listen(3000);
*/
export class Server<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
@@ -96,14 +156,11 @@ export class Server<
/**
* A reference to the underlying Engine.IO server.
*
* Example:
*
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
* @example
* const clientsCount = io.engine.clientsCount;
*
*/
public engine: any;
public engine: BaseServer;
/** @private */
readonly _parser: typeof parser;
@@ -121,9 +178,21 @@ export class Server<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
/**
* A subset of the {@link parentNsps} map, only containing {@link ParentNamespace} which are based on a regular
* expression.
*
* @private
*/
private parentNamespacesFromRegExp: Map<
RegExp,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private _adapter?: AdapterConstructor;
private _serveClient: boolean;
private opts: Partial<EngineOptions>;
private readonly opts: Partial<ServerOptions>;
private eio: Engine;
private _path: string;
private clientPathRegex: RegExp;
@@ -132,18 +201,17 @@ export class Server<
* @private
*/
_connectTimeout: number;
private httpServer: http.Server | HTTPSServer;
private httpServer: http.Server | HTTPSServer | Http2SecureServer;
/**
* Server constructor.
*
* @param srv http server, port, or options
* @param [opts]
* @public
*/
constructor(opts?: Partial<ServerOptions>);
constructor(
srv?: http.Server | HTTPSServer | number,
srv?: http.Server | HTTPSServer | Http2SecureServer | number,
opts?: Partial<ServerOptions>
);
constructor(
@@ -152,6 +220,7 @@ export class Server<
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
opts?: Partial<ServerOptions>
);
@@ -161,6 +230,7 @@ export class Server<
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
opts: Partial<ServerOptions> = {}
) {
@@ -178,11 +248,29 @@ export class Server<
this.serveClient(false !== opts.serveClient);
this._parser = opts.parser || parser;
this.encoder = new this._parser.Encoder();
this.adapter(opts.adapter || Adapter);
this.sockets = this.of("/");
this.opts = opts;
if (opts.connectionStateRecovery) {
opts.connectionStateRecovery = Object.assign(
{
maxDisconnectionDuration: 2 * 60 * 1000,
skipMiddlewares: true,
},
opts.connectionStateRecovery
);
this.adapter(opts.adapter || SessionAwareAdapter);
} else {
this.adapter(opts.adapter || Adapter);
}
opts.cleanupEmptyChildNamespaces = !!opts.cleanupEmptyChildNamespaces;
this.sockets = this.of("/");
if (srv || typeof srv == "number")
this.attach(srv as http.Server | HTTPSServer | number);
this.attach(
srv as http.Server | HTTPSServer | Http2SecureServer | number
);
}
get _opts() {
return this.opts;
}
/**
@@ -190,7 +278,6 @@ export class Server<
*
* @param v - whether to serve client code
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): this;
public serveClient(): boolean;
@@ -239,8 +326,6 @@ export class Server<
}
const namespace = this.parentNsps.get(nextFn.value)!.createChild(name);
debug("dynamic namespace %s was created", name);
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace);
fn(namespace);
});
};
@@ -253,7 +338,6 @@ export class Server<
*
* @param {String} v pathname
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): this;
public path(): string;
@@ -275,7 +359,6 @@ export class Server<
/**
* Set the delay after which a client without namespace is closed
* @param v
* @public
*/
public connectTimeout(v: number): this;
public connectTimeout(): number;
@@ -291,7 +374,6 @@ export class Server<
*
* @param v pathname
* @return self when setting or value when getting
* @public
*/
public adapter(): AdapterConstructor | undefined;
public adapter(v: AdapterConstructor): this;
@@ -312,10 +394,9 @@ export class Server<
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public listen(
srv: http.Server | HTTPSServer | number,
srv: http.Server | HTTPSServer | Http2SecureServer | number,
opts: Partial<ServerOptions> = {}
): this {
return this.attach(srv, opts);
@@ -327,10 +408,9 @@ export class Server<
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public attach(
srv: http.Server | HTTPSServer | number,
srv: http.Server | HTTPSServer | Http2SecureServer | number,
opts: Partial<ServerOptions> = {}
): this {
if ("function" == typeof srv) {
@@ -416,7 +496,7 @@ export class Server<
res.writeHeader("cache-control", "public, max-age=0");
res.writeHeader(
"content-type",
"application/" + (isMap ? "json" : "javascript")
"application/" + (isMap ? "json" : "javascript") + "; charset=utf-8"
);
res.writeHeader("etag", expectedEtag);
@@ -436,7 +516,7 @@ export class Server<
* @private
*/
private initEngine(
srv: http.Server | HTTPSServer,
srv: http.Server | HTTPSServer | Http2SecureServer,
opts: EngineOptions & AttachOptions
): void {
// initialize engine
@@ -459,7 +539,9 @@ export class Server<
* @param srv http server
* @private
*/
private attachServe(srv: http.Server | HTTPSServer): void {
private attachServe(
srv: http.Server | HTTPSServer | Http2SecureServer
): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
@@ -507,7 +589,7 @@ export class Server<
res.setHeader("Cache-Control", "public, max-age=0");
res.setHeader(
"Content-Type",
"application/" + (isMap ? "json" : "javascript")
"application/" + (isMap ? "json" : "javascript") + "; charset=utf-8"
);
res.setHeader("ETag", expectedEtag);
@@ -559,11 +641,10 @@ export class Server<
/**
* Binds socket.io to an engine.io instance.
*
* @param {engine.Server} engine engine.io (or compatible) server
* @param engine engine.io (or compatible) server
* @return self
* @public
*/
public bind(engine): this {
public bind(engine: BaseServer): this {
this.engine = engine;
this.engine.on("connection", this.onconnection.bind(this));
return this;
@@ -589,9 +670,20 @@ export class Server<
/**
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @example
* // with a simple string
* const myNamespace = io.of("/my-namespace");
*
* // with a regex
* const dynamicNsp = io.of(/^\/dynamic-\d+$/).on("connection", (socket) => {
* const namespace = socket.nsp; // newNamespace.name === "/dynamic-101"
*
* // broadcast to all clients in the given sub-namespace
* namespace.emit("hello");
* });
*
* @param name - nsp name
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
@@ -609,6 +701,7 @@ export class Server<
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
parentNsp
);
this.parentNamespacesFromRegExp.set(name, parentNsp);
}
if (fn) {
// @ts-ignore
@@ -621,6 +714,13 @@ export class Server<
let nsp = this._nsps.get(name);
if (!nsp) {
for (const [regex, parentNamespace] of this.parentNamespacesFromRegExp) {
if (regex.test(name as string)) {
debug("attaching namespace %s to parent namespace %s", name, regex);
return parentNamespace.createChild(name as string);
}
}
debug("initializing namespace %s", name);
nsp = new Namespace(this, name);
this._nsps.set(name, nsp);
@@ -637,7 +737,6 @@ export class Server<
* Closes server connection
*
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
@@ -657,10 +756,15 @@ export class Server<
}
/**
* Sets up namespace middleware.
* Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
*
* @return self
* @public
* @example
* io.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
@@ -675,43 +779,91 @@ export class Server<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public to(room: Room | Room[]) {
return this.sockets.to(room);
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* // disconnect all clients in the "room-101" room
* io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public in(room: Room | Room[]) {
return this.sockets.in(room);
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(
name: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.except(name);
public except(room: Room | Room[]) {
return this.sockets.except(room);
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* 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
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return this.sockets.emitWithAck(ev, ...args);
}
/**
* Sends a `message` event to all clients.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.send("hello");
*
* // this is equivalent to
* io.emit("message", "hello");
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
@@ -719,10 +871,9 @@ export class Server<
}
/**
* Sends a `message` event to all clients.
* Sends a `message` event to all clients. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
@@ -730,23 +881,69 @@ export class Server<
}
/**
* Emit a packet to other Socket.IO servers
* Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* io.serverSideEmit("hello", "world");
*
* io.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* io.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some servers did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per server (except the current one)
* }
* });
*
* io.on("ping", (cb) => {
* cb("pong");
* });
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean {
return this.sockets.serverSideEmit(ev, ...args);
}
/**
* Sends a message and expect an acknowledgement from the other Socket.IO servers of the cluster.
*
* @example
* try {
* const responses = await io.serverSideEmitWithAck("ping");
* console.log(responses); // one response per server (except the current one)
* } catch (e) {
* // some servers did not acknowledge the event in the given delay
* }
*
* @param ev - the event name
* @param args - an array of arguments
*
* @return a Promise that will be fulfilled when all servers have acknowledged the event
*/
public serverSideEmitWithAck<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: AllButLast<EventParams<ServerSideEvents, Ev>>
): Promise<FirstArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
return this.sockets.serverSideEmitWithAck(ev, ...args);
}
/**
* Gets a list of socket ids.
*
* @public
* @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link Server#fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
return this.sockets.allSockets();
@@ -755,13 +952,13 @@ export class Server<
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
* @return a new {@link BroadcastOperator} instance for chaining
*/
public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
public compress(compress: boolean) {
return this.sockets.compress(compress);
}
@@ -770,34 +967,40 @@ export class Server<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
public get volatile() {
return this.sockets.volatile;
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
* @example
* // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents, SocketData> {
public get local() {
return this.sockets.local;
}
/**
* Adds a timeout in milliseconds for the next operation
*
* <pre><code>
* Adds a timeout in milliseconds for the next operation.
*
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
@@ -805,41 +1008,85 @@ export class Server<
}
/**
* Returns the matching socket instances
* Returns the matching socket instances.
*
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.sockets.fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
* Makes the matching socket instances join the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]): void {
public socketsJoin(room: Room | Room[]) {
return this.sockets.socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms
* Makes the matching socket instances leave the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]): void {
public socketsLeave(room: Room | Room[]) {
return this.sockets.socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
public disconnectSockets(close: boolean = false) {
return this.sockets.disconnectSockets(close);
}
}
@@ -865,5 +1112,12 @@ module.exports.Server = Server;
module.exports.Namespace = Namespace;
module.exports.Socket = Socket;
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };
export {
Socket,
DisconnectReason,
ServerOptions,
Namespace,
BroadcastOperator,
RemoteSocket,
};
export { Event } from "./socket";

View File

@@ -6,11 +6,16 @@ import {
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
AllButLast,
Last,
FirstArg,
SecondArg,
} from "./typed-events";
import type { Client } from "./client";
import debugModule from "debug";
import type { Adapter, Room, SocketId } from "socket.io-adapter";
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
import { BroadcastOperator } from "./broadcast-operator";
const debug = debugModule("socket.io:namespace");
@@ -52,6 +57,59 @@ export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never, never>
>(<const>["connect", "connection", "new_namespace"]);
/**
* A Namespace is a communication channel that allows you to split the logic of your application over a single shared
* connection.
*
* Each namespace has its own:
*
* - event handlers
*
* ```
* io.of("/orders").on("connection", (socket) => {
* socket.on("order:list", () => {});
* socket.on("order:create", () => {});
* });
*
* io.of("/users").on("connection", (socket) => {
* socket.on("user:list", () => {});
* });
* ```
*
* - rooms
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.on("connection", (socket) => {
* socket.join("room1");
* orderNamespace.to("room1").emit("hello");
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.on("connection", (socket) => {
* socket.join("room1"); // distinct from the room in the "orders" namespace
* userNamespace.to("room1").emit("holà");
* });
* ```
*
* - middlewares
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.use((socket, next) => {
* // ensure the socket has access to the "orders" namespace
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.use((socket, next) => {
* // ensure the socket has access to the "users" namespace
* });
* ```
*/
export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
@@ -123,10 +181,17 @@ export class Namespace<
}
/**
* Sets up namespace middleware.
* Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
*
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
@@ -171,36 +236,63 @@ export class Namespace<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* myNamespace.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* myNamespace.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).to(room);
public to(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room);
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // disconnect all clients in the "room-101" room
* myNamespace.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).in(room);
public in(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* myNamespace.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* myNamespace.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).except(room);
public except(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
);
}
/**
@@ -209,13 +301,25 @@ export class Namespace<
* @return {Socket}
* @private
*/
_add(
async _add(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: () => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
auth: Record<string, unknown>,
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) {
debug("adding socket to nsp %s", this.name);
const socket = new Socket(this, client, query);
const socket = await this._createSocket(client, auth);
if (
// @ts-ignore
this.server.opts.connectionStateRecovery?.skipMiddlewares &&
socket.recovered &&
client.conn.readyState === "open"
) {
return this._doConnect(socket, fn);
}
this.run(socket, (err) => {
process.nextTick(() => {
if ("open" !== client.conn.readyState) {
@@ -237,22 +341,56 @@ export class Namespace<
}
}
// track socket
this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect();
if (fn) fn();
// fire user-set events
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
this._doConnect(socket, fn);
});
});
return socket;
}
private async _createSocket(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
auth: Record<string, unknown>
) {
const sessionId = auth.pid;
const offset = auth.offset;
if (
// @ts-ignore
this.server.opts.connectionStateRecovery &&
typeof sessionId === "string" &&
typeof offset === "string"
) {
let session;
try {
session = await this.adapter.restoreSession(sessionId, offset);
} catch (e) {
debug("error while restoring session: %s", e);
}
if (session) {
debug("connection state recovered for sid %s", session.sid);
return new Socket(this, client, auth, session);
}
}
return new Socket(this, client, auth);
}
private _doConnect(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
) {
// track socket
this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect();
if (fn) fn(socket);
// fire user-set events
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
}
/**
@@ -271,10 +409,26 @@ export class Namespace<
}
/**
* Emits to all clients.
* Emits to all connected clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* myNamespace.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
*
* // with an acknowledgement from the clients
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
@@ -286,11 +440,46 @@ export class Namespace<
);
}
/**
* Emits an event and waits for an acknowledgement from all clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* try {
* const responses = await myNamespace.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
* }
*
* @return a Promise that will be fulfilled when all clients have acknowledged the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<SecondArg<Last<EventParams<EmitEvents, Ev>>>> {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).emitWithAck(ev, ...args);
}
/**
* Sends a `message` event to all clients.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.send("hello");
*
* // this is equivalent to
* myNamespace.emit("message", "hello");
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -298,10 +487,9 @@ export class Namespace<
}
/**
* Sends a `message` event to all clients.
* Sends a `message` event to all clients. Sends a `message` event. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -309,15 +497,39 @@ export class Namespace<
}
/**
* Emit a packet to other Socket.IO servers
* Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.serverSideEmit("hello", "world");
*
* myNamespace.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* myNamespace.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some servers did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per server (except the current one)
* }
* });
*
* myNamespace.on("ping", (cb) => {
* cb("pong");
* });
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
...args: EventParams<
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
Ev
>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`);
@@ -327,6 +539,44 @@ export class Namespace<
return true;
}
/**
* Sends a message and expect an acknowledgement from the other Socket.IO servers of the cluster.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* try {
* const responses = await myNamespace.serverSideEmitWithAck("ping");
* console.log(responses); // one response per server (except the current one)
* } catch (e) {
* // some servers did not acknowledge the event in the given delay
* }
*
* @param ev - the event name
* @param args - an array of arguments
*
* @return a Promise that will be fulfilled when all servers have acknowledged the event
*/
public serverSideEmitWithAck<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: AllButLast<EventParams<ServerSideEvents, Ev>>
): Promise<FirstArg<Last<EventParams<ServerSideEvents, Ev>>>[]> {
return new Promise((resolve, reject) => {
args.push((err, responses) => {
if (err) {
err.responses = responses;
return reject(err);
} else {
return resolve(responses);
}
});
this.serverSideEmit(
ev,
...(args as any[] as EventParams<ServerSideEvents, Ev>)
);
});
}
/**
* Called when a packet is received from another Socket.IO server
*
@@ -341,24 +591,30 @@ export class Namespace<
/**
* Gets a list of clients.
*
* @return self
* @public
* @deprecated this method will be removed in the next major release, please use {@link Namespace#serverSideEmit} or
* {@link Namespace#fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets();
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).allSockets();
}
/**
* Sets the compress flag.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).compress(compress);
public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress
);
}
/**
@@ -366,76 +622,149 @@ export class Namespace<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.volatile.emit("hello"); // the clients may or may not receive it
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).volatile;
public get volatile() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile;
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // the “foo” event will be broadcast to all connected clients on this node
* myNamespace.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).local;
public get local() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local;
}
/**
* Adds a timeout in milliseconds for the next operation
* Adds a timeout in milliseconds for the next operation.
*
* <pre><code>
* @example
* const myNamespace = io.of("/my-namespace");
*
* io.timeout(1000).emit("some-event", (err, responses) => {
* // ...
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* </pre></code>
*
* @param timeout
*/
public timeout(timeout: number) {
return new BroadcastOperator(this.adapter).timeout(timeout);
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout
);
}
/**
* Returns the matching socket instances
* Returns the matching socket instances.
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return new BroadcastOperator(this.adapter).fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms
* @example
* const myNamespace = io.of("/my-namespace");
*
* @param room
* @public
* // return all Socket instances
* const sockets = await myNamespace.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await myNamespace.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room);
public fetchSockets() {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).fetchSockets();
}
/**
* Makes the matching socket instances disconnect
* Makes the matching socket instances join the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances join the "room1" room
* myNamespace.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* myNamespace.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances leave the "room1" room
* myNamespace.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* myNamespace.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* myNamespace.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* myNamespace.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close);
public disconnectSockets(close: boolean = false) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).disconnectSockets(close);
}
}

View File

@@ -7,7 +7,25 @@ import type {
DefaultEventsMap,
} from "./typed-events";
import type { BroadcastOptions } from "socket.io-adapter";
import debugModule from "debug";
const debug = debugModule("socket.io:parent-namespace");
/**
* A parent namespace is a special {@link Namespace} that holds a list of child namespaces which were created either
* with a regular expression or with a function.
*
* @example
* const parentNamespace = io.of(/\/dynamic-\d+/);
*
* parentNamespace.on("connection", (socket) => {
* const childNamespace = socket.nsp;
* }
*
* // will reach all the clients that are in one of the child namespaces, like "/dynamic-101"
* parentNamespace.emit("hello", "world");
*
*/
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
@@ -52,6 +70,7 @@ export class ParentNamespace<
createChild(
name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
debug("creating child namespace %s", name);
const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) =>
@@ -61,7 +80,26 @@ export class ParentNamespace<
namespace.on("connection", listener)
);
this.children.add(namespace);
if (this.server._opts.cleanupEmptyChildNamespaces) {
const remove = namespace._remove;
namespace._remove = (socket) => {
remove.call(namespace, socket);
if (namespace.sockets.size === 0) {
debug("closing child namespace %s", name);
namespace.adapter.close();
this.server._nsps.delete(namespace.name);
this.children.delete(namespace);
}
};
}
this.server._nsps.set(name, namespace);
// @ts-ignore
this.server.sockets.emitReserved("new_namespace", namespace);
return namespace;
}

View File

@@ -2,19 +2,26 @@ import { Packet, PacketType } from "socket.io-parser";
import debugModule from "debug";
import type { Server } from "./index";
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
AllButLast,
DecorateAcknowledgements,
DecorateAcknowledgementsWithMultipleResponses,
DefaultEventsMap,
EventNames,
EventParams,
EventsMap,
FirstArg,
Last,
StrictEventEmitter,
} from "./typed-events";
import type { Client } from "./client";
import type { Namespace, NamespaceReservedEventsMap } from "./namespace";
import type { IncomingMessage, IncomingHttpHeaders } from "http";
import type { IncomingHttpHeaders, IncomingMessage } from "http";
import type {
Adapter,
BroadcastFlags,
PrivateSessionId,
Room,
Session,
SocketId,
} from "socket.io-adapter";
import base64id from "base64id";
@@ -25,9 +32,32 @@ const debug = debugModule("socket.io:socket");
type ClientReservedEvents = "connect_error";
// TODO for next major release: cleanup disconnect reasons
export type DisconnectReason =
// Engine.IO close reasons
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error"
// Socket.IO disconnect reasons
| "server shutting down"
| "forced server close"
| "client namespace disconnect"
| "server namespace disconnect";
const RECOVERABLE_DISCONNECT_REASONS: ReadonlySet<DisconnectReason> = new Set([
"transport error",
"transport close",
"forced close",
"ping timeout",
"server shutting down",
"forced server close",
]);
export interface SocketReservedEventsMap {
disconnect: (reason: string) => void;
disconnecting: (reason: string) => void;
disconnect: (reason: DisconnectReason, description?: any) => void;
disconnecting: (reason: DisconnectReason, description?: any) => void;
error: (err: Error) => void;
}
@@ -114,6 +144,37 @@ export type Event = [string, ...any[]];
function noop() {}
/**
* This is the main object for interacting with a client.
*
* A Socket belongs to a given {@link Namespace} and uses an underlying {@link Client} to communicate.
*
* Within each {@link Namespace}, you can also define arbitrary channels (called "rooms") that the {@link Socket} can
* join and leave. That provides a convenient way to broadcast to a group of socket instances.
*
* @example
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // join the room named "room1"
* socket.join("room1");
*
* // broadcast to everyone in the room named "room1"
* io.to("room1").emit("hello");
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*/
export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
@@ -124,15 +185,47 @@ export class Socket<
EmitEvents,
SocketReservedEventsMap
> {
/**
* An unique identifier for the session.
*/
public readonly id: SocketId;
/**
* Whether the connection state was recovered after a temporary disconnection. In that case, any missed packets will
* be transmitted to the client, the data attribute and the rooms will be restored.
*/
public readonly recovered: boolean = false;
/**
* The handshake details.
*/
public readonly handshake: Handshake;
/**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
* Additional information that can be attached to the Socket instance and which will be used in the
* {@link Server.fetchSockets()} method.
*/
public data: Partial<SocketData> = {};
/**
* Whether the socket is currently connected or not.
*
* @example
* io.use((socket, next) => {
* console.log(socket.connected); // false
* next();
* });
*
* io.on("connection", (socket) => {
* console.log(socket.connected); // true
* });
*/
public connected: boolean = false;
/**
* The session ID, which must not be shared (unlike {@link id}).
*
* @private
*/
private readonly pid: PrivateSessionId;
// TODO: remove this unused reference
private readonly server: Server<
ListenEvents,
EmitEvents,
@@ -157,16 +250,34 @@ export class Socket<
constructor(
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
auth: object
auth: Record<string, unknown>,
previousSession?: Session
) {
super();
this.server = nsp.server;
this.adapter = this.nsp.adapter;
if (client.conn.protocol === 3) {
// @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id;
if (previousSession) {
this.id = previousSession.sid;
this.pid = previousSession.pid;
previousSession.rooms.forEach((room) => this.join(room));
this.data = previousSession.data as Partial<SocketData>;
previousSession.missedPackets.forEach((packet) => {
this.packet({
type: PacketType.EVENT,
data: packet,
});
});
this.recovered = true;
} else {
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
if (client.conn.protocol === 3) {
// @ts-ignore
this.id = nsp.name !== "/" ? nsp.name + "#" + client.id : client.id;
} else {
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
}
if (this.server._opts.connectionStateRecovery) {
this.pid = base64id.generateId();
}
}
this.handshake = this.buildHandshake(auth);
}
@@ -195,8 +306,20 @@ export class Socket<
/**
* Emits to this client.
*
* @example
* io.on("connection", (socket) => {
* socket.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* socket.emit("hello", 1, "2", { 3: ["4"], 5: Buffer.from([6]) });
*
* // with an acknowledgement from the client
* socket.emit("hello", "world", (val) => {
* // ...
* });
* });
*
* @return Always returns `true`.
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
@@ -223,12 +346,58 @@ export class Socket<
const flags = Object.assign({}, this.flags);
this.flags = {};
this.notifyOutgoingListeners(packet);
this.packet(packet, flags);
// @ts-ignore
if (this.nsp.server.opts.connectionStateRecovery) {
// this ensures the packet is stored and can be transmitted upon reconnection
this.adapter.broadcast(packet, {
rooms: new Set([this.id]),
except: new Set(),
flags,
});
} else {
this.notifyOutgoingListeners(packet);
this.packet(packet, flags);
}
return true;
}
/**
* Emits an event and waits for an acknowledgement
*
* @example
* 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
* }
* });
*
* @return a Promise that will be fulfilled when the client acknowledges the event
*/
public emitWithAck<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: AllButLast<EventParams<EmitEvents, Ev>>
): Promise<FirstArg<Last<EventParams<EmitEvents, Ev>>>> {
// the timeout flag is optional
const withErr = this.flags.timeout !== undefined;
return new Promise((resolve, reject) => {
args.push((arg1, arg2) => {
if (withErr) {
return arg1 ? reject(arg1) : resolve(arg2);
} else {
return resolve(arg1);
}
});
this.emit(ev, ...(args as any[] as EventParams<EmitEvents, Ev>));
});
}
/**
* @private
*/
@@ -254,43 +423,83 @@ export class Socket<
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients in the “room-101” room, except this socket
* socket.to("room-101").emit("foo", "bar");
*
* // the code above is equivalent to:
* io.to("room-101").except(socket.id).emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* socket.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.to("room-101").to("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public to(room: Room | Room[]) {
return this.newBroadcastOperator().to(room);
}
/**
* Targets a room when broadcasting.
* Targets a room when broadcasting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // disconnect all clients in the "room-101" room, except this socket
* socket.in("room-101").disconnectSockets();
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
public in(room: Room | Room[]) {
return this.newBroadcastOperator().in(room);
}
/**
* Excludes a room when broadcasting.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* // and this socket
* socket.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* socket.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.except("room-101").except("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
public except(room: Room | Room[]) {
return this.newBroadcastOperator().except(room);
}
/**
* Sends a `message` event.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.on("connection", (socket) => {
* socket.send("hello");
*
* // this is equivalent to
* socket.emit("message", "hello");
* });
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -298,10 +507,9 @@ export class Socket<
}
/**
* Sends a `message` event.
* Sends a `message` event. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -327,9 +535,17 @@ export class Socket<
/**
* Joins a room.
*
* @example
* io.on("connection", (socket) => {
* // join a single room
* socket.join("room1");
*
* // join multiple rooms
* socket.join(["room1", "room2"]);
* });
*
* @param {String|Array} rooms - room or array of rooms
* @return a Promise or nothing, depending on the adapter
* @public
*/
public join(rooms: Room | Array<Room>): Promise<void> | void {
debug("join room %s", rooms);
@@ -343,9 +559,17 @@ export class Socket<
/**
* Leaves a room.
*
* @example
* io.on("connection", (socket) => {
* // leave a single room
* socket.leave("room1");
*
* // leave multiple rooms
* socket.leave("room1").leave("room2");
* });
*
* @param {String} room
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
debug("leave room %s", room);
@@ -377,7 +601,10 @@ export class Socket<
if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT });
} else {
this.packet({ type: PacketType.CONNECT, data: { sid: this.id } });
this.packet({
type: PacketType.CONNECT,
data: { sid: this.id, pid: this.pid },
});
}
}
@@ -505,19 +732,34 @@ export class Socket<
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @param description
* @throw {Error} optional error object
*
* @private
*/
_onclose(reason: string): this | undefined {
_onclose(reason: DisconnectReason, description?: any): this | undefined {
if (!this.connected) return this;
debug("closing socket - reason %s", reason);
this.emitReserved("disconnecting", reason);
this.emitReserved("disconnecting", reason, description);
if (
this.server._opts.connectionStateRecovery &&
RECOVERABLE_DISCONNECT_REASONS.has(reason)
) {
debug("connection state recovery is enabled for sid %s", this.id);
this.adapter.persistSession({
sid: this.id,
pid: this.pid,
rooms: [...this.rooms],
data: this.data,
});
}
this._cleanup();
this.nsp._remove(this);
this.client._remove(this);
this.connected = false;
this.emitReserved("disconnect", reason);
this.emitReserved("disconnect", reason, description);
return;
}
@@ -545,10 +787,17 @@ export class Socket<
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
* @example
* io.on("connection", (socket) => {
* // disconnect this socket (the connection might be kept alive for other namespaces)
* socket.disconnect();
*
* @public
* // disconnect this socket and close the underlying connection
* socket.disconnect(true);
* })
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return self
*/
public disconnect(close = false): this {
if (!this.connected) return this;
@@ -564,9 +813,13 @@ export class Socket<
/**
* Sets the compress flag.
*
* @example
* io.on("connection", (socket) => {
* socket.compress(false).emit("hello");
* });
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Socket} self
* @public
*/
public compress(compress: boolean): this {
this.flags.compress = compress;
@@ -578,8 +831,12 @@ export class Socket<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* io.on("connection", (socket) => {
* socket.volatile.emit("hello"); // the client may or may not receive it
* });
*
* @return {Socket} self
* @public
*/
public get volatile(): this {
this.flags.volatile = true;
@@ -590,20 +847,30 @@ export class Socket<
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender.
*
* @return {Socket} self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients, except this socket
* socket.broadcast.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get broadcast(): BroadcastOperator<EmitEvents, SocketData> {
public get broadcast() {
return this.newBroadcastOperator();
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients on this node, except this socket
* socket.local.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents, SocketData> {
public get local() {
return this.newBroadcastOperator().local;
}
@@ -611,18 +878,25 @@ export class Socket<
* 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
* }
* @example
* io.on("connection", (socket) => {
* 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 {
public timeout(
timeout: number
): Socket<
ListenEvents,
DecorateAcknowledgements<EmitEvents>,
ServerSideEvents,
SocketData
> {
this.flags.timeout = timeout;
return this;
}
@@ -652,9 +926,25 @@ export class Socket<
/**
* Sets up socket middleware.
*
* @example
* io.on("connection", (socket) => {
* socket.use(([event, ...args], next) => {
* if (isUnauthorized(event)) {
* return next(new Error("unauthorized event"));
* }
* // do not forget to call next
* next();
* });
*
* socket.on("error", (err) => {
* if (err && err.message === "unauthorized event") {
* socket.disconnect();
* }
* });
* });
*
* @param {Function} fn - middleware function (event, next)
* @return {Socket} self
* @public
*/
public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
this.fns.push(fn);
@@ -697,8 +987,6 @@ export class Socket<
/**
* A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/
public get request(): IncomingMessage {
return this.client.request;
@@ -707,14 +995,30 @@ export class Socket<
/**
* A reference to the underlying Client transport connection (Engine.IO Socket object).
*
* @public
* @example
* io.on("connection", (socket) => {
* console.log(socket.conn.transport.name); // prints "polling" or "websocket"
*
* socket.conn.once("upgrade", () => {
* console.log(socket.conn.transport.name); // prints "websocket"
* });
* });
*/
public get conn() {
return this.client.conn;
}
/**
* @public
* Returns the rooms the socket is currently in.
*
* @example
* io.on("connection", (socket) => {
* console.log(socket.rooms); // Set { <socket.id> }
*
* socket.join("room1");
*
* console.log(socket.rooms); // Set { <socket.id>, "room1" }
* });
*/
public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set();
@@ -724,8 +1028,14 @@ export class Socket<
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback.
*
* @example
* io.on("connection", (socket) => {
* socket.onAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
* });
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
@@ -738,7 +1048,6 @@ export class Socket<
* the callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
@@ -749,8 +1058,22 @@ export class Socket<
/**
* Removes the listener that will be fired when any event is received.
*
* @example
* io.on("connection", (socket) => {
* const catchAllListener = (event, ...args) => {
* console.log(`got event ${event}`);
* }
*
* socket.onAny(catchAllListener);
*
* // remove a specific listener
* socket.offAny(catchAllListener);
*
* // or remove all listeners
* socket.offAny();
* });
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
@@ -773,28 +1096,25 @@ export class Socket<
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || [];
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
* Adds a listener that will be fired when any event is sent. The event name is passed as the first argument to
* the callback.
*
* @param listener
* Note: acknowledgements sent to the client are not included.
*
* <pre><code>
*
* socket.onAnyOutgoing((event, ...args) => {
* console.log(event);
* @example
* io.on("connection", (socket) => {
* socket.onAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
* });
*
* </pre></code>
*
* @public
* @param listener
*/
public onAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
@@ -806,17 +1126,14 @@ export class Socket<
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
*
* <pre><code>
*
* socket.prependAnyOutgoing((event, ...args) => {
* console.log(event);
* @example
* io.on("connection", (socket) => {
* socket.prependAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
* });
*
* </pre></code>
*
* @public
* @param listener
*/
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
@@ -825,24 +1142,24 @@ export class Socket<
}
/**
* Removes the listener that will be fired when any event is emitted.
* Removes the listener that will be fired when any event is sent.
*
* @param listener
* @example
* io.on("connection", (socket) => {
* const catchAllListener = (event, ...args) => {
* console.log(`sent event ${event}`);
* }
*
* <pre><code>
* socket.onAnyOutgoing(catchAllListener);
*
* const handler = (event, ...args) => {
* console.log(event);
* }
* // remove a specific listener
* socket.offAnyOutgoing(catchAllListener);
*
* socket.onAnyOutgoing(handler);
* // or remove all listeners
* socket.offAnyOutgoing();
* });
*
* // then later
* socket.offAnyOutgoing(handler);
*
* </pre></code>
*
* @public
* @param listener - the catch-all listener
*/
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
if (!this._anyOutgoingListeners) {
@@ -865,8 +1182,6 @@ export class Socket<
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAnyOutgoing() {
return this._anyOutgoingListeners || [];
@@ -888,14 +1203,12 @@ export class Socket<
}
}
private newBroadcastOperator(): BroadcastOperator<EmitEvents, SocketData> {
private newBroadcastOperator() {
const flags = Object.assign({}, this.flags);
this.flags = {};
return new BroadcastOperator(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),
flags
);
return new BroadcastOperator<
DecorateAcknowledgementsWithMultipleResponses<EmitEvents>,
SocketData
>(this.adapter, new Set<Room>(), new Set<Room>([this.id]), flags);
}
}

View File

@@ -178,3 +178,66 @@ export abstract class StrictEventEmitter<
>[];
}
}
export type Last<T extends any[]> = T extends [...infer H, infer L] ? L : any;
export type AllButLast<T extends any[]> = T extends [...infer H, infer L]
? H
: any[];
export type FirstArg<T> = T extends (arg: infer Param) => infer Result
? Param
: any;
export type SecondArg<T> = T extends (
err: Error,
arg: infer Param
) => infer Result
? Param
: any;
type PrependTimeoutError<T extends any[]> = {
[K in keyof T]: T[K] extends (...args: infer Params) => infer Result
? (err: Error, ...args: Params) => Result
: T[K];
};
type ExpectMultipleResponses<T extends any[]> = {
[K in keyof T]: T[K] extends (err: Error, arg: infer Param) => infer Result
? (err: Error, arg: Param[]) => Result
: T[K];
};
/**
* Utility type to decorate the acknowledgement callbacks with a timeout error.
*
* This is needed because the timeout() flag breaks the symmetry between the sender and the receiver:
*
* @example
* interface Events {
* "my-event": (val: string) => void;
* }
*
* socket.on("my-event", (cb) => {
* cb("123"); // one single argument here
* });
*
* socket.timeout(1000).emit("my-event", (err, val) => {
* // two arguments there (the "err" argument is not properly typed)
* });
*
*/
export type DecorateAcknowledgements<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: PrependTimeoutError<Params>) => Result
: E[K];
};
export type DecorateAcknowledgementsWithTimeoutAndMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<PrependTimeoutError<Params>>) => Result
: E[K];
};
export type DecorateAcknowledgementsWithMultipleResponses<E> = {
[K in keyof E]: E[K] extends (...args: infer Params) => infer Result
? (...args: ExpectMultipleResponses<Params>) => Result
: E[K];
};

190
package-lock.json generated
View File

@@ -1,20 +1,20 @@
{
"name": "socket.io",
"version": "4.5.2",
"version": "4.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "socket.io",
"version": "4.5.2",
"version": "4.6.0",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.2.0",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.2.0"
"engine.io": "~6.4.1",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.1"
},
"devDependencies": {
"@types/mocha": "^9.0.0",
@@ -23,7 +23,7 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.5.2",
"socket.io-client": "4.6.1",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",
@@ -1334,9 +1334,9 @@
}
},
"node_modules/dezalgo": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
"integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"dependencies": {
"asap": "^2.0.0",
@@ -1377,9 +1377,9 @@
"dev": true
},
"node_modules/engine.io": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"dependencies": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -1390,22 +1390,22 @@
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
"ws": "~8.11.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
@@ -1613,32 +1613,20 @@
}
},
"node_modules/formidable": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz",
"integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==",
"dev": true,
"dependencies": {
"dezalgo": "1.0.3",
"hexoid": "1.0.0",
"once": "1.4.0",
"qs": "6.9.3"
"dezalgo": "^1.0.4",
"hexoid": "^1.0.0",
"once": "^1.4.0",
"qs": "^6.11.0"
},
"funding": {
"url": "https://ko-fi.com/tunnckoCore/commissions"
}
},
"node_modules/formidable/node_modules/qs": {
"version": "6.9.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==",
"dev": true,
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/fromentries": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
@@ -2254,9 +2242,9 @@
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"bin": {
"json5": "lib/cli.js"
@@ -3467,20 +3455,23 @@
}
},
"node_modules/socket.io-adapter": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
"integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
"dependencies": {
"ws": "~8.11.0"
}
},
"node_modules/socket.io-client": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.2.tgz",
"integrity": "sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==",
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz",
"integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.2.1",
"socket.io-parser": "~4.2.0"
"engine.io-client": "~6.4.0",
"socket.io-parser": "~4.2.1"
},
"engines": {
"node": ">=10.0.0"
@@ -3554,9 +3545,9 @@
"dev": true
},
"node_modules/socket.io-client-v2/node_modules/socket.io-parser": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
"dev": true,
"dependencies": {
"component-emitter": "~1.3.0",
@@ -3595,9 +3586,9 @@
}
},
"node_modules/socket.io-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
@@ -4205,9 +4196,9 @@
}
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"engines": {
"node": ">=10.0.0"
},
@@ -5359,9 +5350,9 @@
"dev": true
},
"dezalgo": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz",
"integrity": "sha512-K7i4zNfT2kgQz3GylDw40ot9GAE47sFZ9EXHFSPP6zONLgH6kWXE0KWJchkbQJLBkRazq4APwZ4OwiFFlT95OQ==",
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
"dev": true,
"requires": {
"asap": "^2.0.0",
@@ -5396,9 +5387,9 @@
"dev": true
},
"engine.io": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.4.1.tgz",
"integrity": "sha512-JFYQurD/nbsA5BSPmbaOSLa3tSVj8L6o4srSwXXY3NqE+gGUNmmPTbhn8tjzcCtSqhFgIeqef81ngny8JM25hw==",
"requires": {
"@types/cookie": "^0.4.1",
"@types/cors": "^2.8.12",
@@ -5409,19 +5400,19 @@
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3"
"ws": "~8.11.0"
}
},
"engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"ws": "~8.11.0",
"xmlhttprequest-ssl": "~2.0.0"
}
},
@@ -5577,23 +5568,15 @@
}
},
"formidable": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.0.1.tgz",
"integrity": "sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.1.tgz",
"integrity": "sha512-0EcS9wCFEzLvfiks7omJ+SiYJAiD+TzK4Pcw1UlUoGnhUxDcMKjt0P7x8wEb0u6OHu8Nb98WG3nxtlF5C7bvUQ==",
"dev": true,
"requires": {
"dezalgo": "1.0.3",
"hexoid": "1.0.0",
"once": "1.4.0",
"qs": "6.9.3"
},
"dependencies": {
"qs": {
"version": "6.9.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
"integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==",
"dev": true
}
"dezalgo": "^1.0.4",
"hexoid": "^1.0.0",
"once": "^1.4.0",
"qs": "^6.11.0"
}
},
"fromentries": {
@@ -6050,9 +6033,9 @@
"dev": true
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
"integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
},
"kind-of": {
@@ -6931,20 +6914,23 @@
"dev": true
},
"socket.io-adapter": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz",
"integrity": "sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg=="
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz",
"integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==",
"requires": {
"ws": "~8.11.0"
}
},
"socket.io-client": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.2.tgz",
"integrity": "sha512-naqYfFu7CLDiQ1B7AlLhRXKX3gdeaIMfgigwavDzgJoIUYulc1qHH5+2XflTsXTPY7BlPH5rppJyUjhjrKQKLg==",
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz",
"integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==",
"dev": true,
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.2.1",
"socket.io-parser": "~4.2.0"
"engine.io-client": "~6.4.0",
"socket.io-parser": "~4.2.1"
}
},
"socket.io-client-v2": {
@@ -7014,9 +7000,9 @@
"dev": true
},
"socket.io-parser": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz",
"integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.3.tgz",
"integrity": "sha512-qOg87q1PMWWTeO01768Yh9ogn7chB9zkKtQnya41Y355S0UmpXgpcrFwAgjYJxu9BdKug5r5e9YtVSeWhKBUZg==",
"dev": true,
"requires": {
"component-emitter": "~1.3.0",
@@ -7040,9 +7026,9 @@
}
},
"socket.io-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
@@ -7492,9 +7478,9 @@
}
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"requires": {}
},
"xmlhttprequest-ssl": {

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "4.5.2",
"version": "4.6.1",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -49,9 +49,9 @@
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.2",
"engine.io": "~6.2.0",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.2.0"
"engine.io": "~6.4.1",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.1"
},
"devDependencies": {
"@types/mocha": "^9.0.0",
@@ -60,7 +60,7 @@
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.5.2",
"socket.io-client": "4.6.1",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^8.0.0",
"supertest": "^6.1.6",

View File

@@ -4,46 +4,13 @@ import { join } from "path";
import { exec } from "child_process";
import { Server } from "..";
import expect from "expect.js";
import { createClient, getPort } from "./support/util";
import request from "supertest";
// TODO: update superagent as latest release now supports promises
const eioHandshake = (httpServer): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
const sid = JSON.parse(res.text.substring(1)).sid;
resolve(sid);
});
});
};
const eioPush = (httpServer, sid: string, body: string): Promise<void> => {
return new Promise((resolve) => {
request(httpServer)
.post("/socket.io/")
.send(body)
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end(() => {
resolve();
});
});
};
const eioPoll = (httpServer, sid): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end((err, res) => {
resolve(res.text);
});
});
};
import {
createClient,
eioHandshake,
eioPoll,
eioPush,
getPort,
} from "./support/util";
describe("close", () => {
it("should be able to close sio sending a srv", (done) => {

View File

@@ -0,0 +1,247 @@
import { Server, Socket } from "..";
import expect from "expect.js";
import { waitFor, eioHandshake, eioPush, eioPoll } from "./support/util";
import { createServer, Server as HttpServer } from "http";
import { Adapter } from "socket.io-adapter";
async function init(httpServer: HttpServer, io: Server) {
// Engine.IO handshake
const sid = await eioHandshake(httpServer);
// Socket.IO handshake
await eioPush(httpServer, sid, "40");
const handshakeBody = await eioPoll(httpServer, sid);
expect(handshakeBody.startsWith("40")).to.be(true);
const handshake = JSON.parse(handshakeBody.substring(2));
expect(handshake.sid).to.not.be(undefined);
// in that case, the handshake also contains a private session ID
expect(handshake.pid).to.not.be(undefined);
io.emit("hello");
const message = await eioPoll(httpServer, sid);
expect(message.startsWith('42["hello"')).to.be(true);
const offset = JSON.parse(message.substring(2))[1];
// in that case, each packet also includes an offset in the data array
expect(offset).to.not.be(undefined);
await eioPush(httpServer, sid, "1");
return [handshake.sid, handshake.pid, offset];
}
describe("connection state recovery", () => {
it("should restore session and missed packets", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer, {
connectionStateRecovery: {},
});
let serverSocket;
io.once("connection", (socket) => {
socket.join("room1");
serverSocket = socket;
});
const [sid, pid, offset] = await init(httpServer, io);
io.emit("hello1"); // broadcast
io.to("room1").emit("hello2"); // broadcast to room
serverSocket.emit("hello3"); // direct message
const newSid = await eioHandshake(httpServer);
await eioPush(
httpServer,
newSid,
`40{"pid":"${pid}","offset":"${offset}"}`
);
const payload = await eioPoll(httpServer, newSid);
const packets = payload.split("\x1e");
expect(packets.length).to.eql(4);
// note: EVENT packets are received before the CONNECT packet, which is a bit weird
// see also: https://github.com/socketio/socket.io-deno/commit/518f534e1c205b746b1cb21fe76b187dabc96f34
expect(packets[0].startsWith('42["hello1"')).to.be(true);
expect(packets[1].startsWith('42["hello2"')).to.be(true);
expect(packets[2].startsWith('42["hello3"')).to.be(true);
expect(packets[3]).to.eql(`40{"sid":"${sid}","pid":"${pid}"}`);
io.close();
});
it("should restore rooms and data attributes", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer, {
connectionStateRecovery: {},
});
io.once("connection", (socket) => {
expect(socket.recovered).to.eql(false);
socket.join("room1");
socket.join("room2");
socket.data.foo = "bar";
});
const [sid, pid, offset] = await init(httpServer, io);
const newSid = await eioHandshake(httpServer);
const [socket] = await Promise.all([
waitFor<Socket>(io, "connection"),
eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
]);
expect(socket.id).to.eql(sid);
expect(socket.recovered).to.eql(true);
expect(socket.rooms.has(socket.id)).to.eql(true);
expect(socket.rooms.has("room1")).to.eql(true);
expect(socket.rooms.has("room2")).to.eql(true);
expect(socket.data.foo).to.eql("bar");
await eioPoll(httpServer, newSid); // drain buffer
io.close();
});
it("should not run middlewares upon recovery by default", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer, {
connectionStateRecovery: {},
});
const [_, pid, offset] = await init(httpServer, io);
io.use((socket, next) => {
socket.data.middlewareWasCalled = true;
next();
});
const newSid = await eioHandshake(httpServer);
const [socket] = await Promise.all([
waitFor<Socket>(io, "connection"),
eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
]);
expect(socket.recovered).to.be(true);
expect(socket.data.middlewareWasCalled).to.be(undefined);
await eioPoll(httpServer, newSid); // drain buffer
io.close();
});
it("should run middlewares even upon recovery", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer, {
connectionStateRecovery: {
skipMiddlewares: false,
},
});
const [_, pid, offset] = await init(httpServer, io);
io.use((socket, next) => {
socket.data.middlewareWasCalled = true;
next();
});
const newSid = await eioHandshake(httpServer);
const [socket] = await Promise.all([
waitFor<Socket>(io, "connection"),
eioPush(httpServer, newSid, `40{"pid":"${pid}","offset":"${offset}"}`),
]);
expect(socket.recovered).to.be(true);
expect(socket.data.middlewareWasCalled).to.be(true);
await eioPoll(httpServer, newSid); // drain buffer
io.close();
});
it("should fail to restore an unknown session", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer, {
connectionStateRecovery: {},
});
// Engine.IO handshake
const sid = await eioHandshake(httpServer);
// Socket.IO handshake
await eioPush(httpServer, sid, '40{"pid":"foo","offset":"bar"}');
const handshakeBody = await eioPoll(httpServer, sid);
expect(handshakeBody.startsWith("40")).to.be(true);
const handshake = JSON.parse(handshakeBody.substring(2));
expect(handshake.sid).to.not.eql("foo");
expect(handshake.pid).to.not.eql("bar");
io.close();
});
it("should be disabled by default", async () => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer);
// Engine.IO handshake
const sid = await eioHandshake(httpServer);
// Socket.IO handshake
await eioPush(httpServer, sid, "40");
const handshakeBody = await eioPoll(httpServer, sid);
expect(handshakeBody.startsWith("40")).to.be(true);
const handshake = JSON.parse(handshakeBody.substring(2));
expect(handshake.sid).to.not.be(undefined);
expect(handshake.pid).to.be(undefined);
io.close();
});
it("should not call adapter#persistSession or adapter#restoreSession if disabled", async () => {
const httpServer = createServer().listen(0);
class DummyAdapter extends Adapter {
override persistSession(session) {
expect.fail();
}
override restoreSession(pid, offset) {
expect.fail();
return Promise.reject("should not happen");
}
}
const io = new Server(httpServer, {
adapter: DummyAdapter,
});
// Engine.IO handshake
const sid = await eioHandshake(httpServer);
await eioPush(httpServer, sid, '40{"pid":"foo","offset":"bar"}');
await eioPoll(httpServer, sid);
await eioPush(httpServer, sid, "1");
io.close();
});
});

View File

@@ -20,4 +20,5 @@ describe("socket.io", () => {
require("./socket-timeout");
require("./uws");
require("./utility-methods");
require("./connection-state-recovery");
});

View File

@@ -471,6 +471,74 @@ describe("messaging many", () => {
});
});
it("should broadcast and expect multiple acknowledgements (promise)", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", (cb) => {
cb(3);
});
Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]).then(async () => {
const responses = await io.timeout(2000).emitWithAck("some event");
expect(responses).to.contain(1, 2, 3);
success(done, io, socket1, socket2, socket3);
});
});
it("should fail when a client does not acknowledge the event in the given delay (promise)", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", () => {
// timeout
});
Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]).then(async () => {
try {
await io.timeout(200).emitWithAck("some event");
expect.fail();
} catch (err) {
expect(err).to.be.an(Error);
// @ts-ignore
expect(err.responses).to.have.length(2);
// @ts-ignore
expect(err.responses).to.contain(1, 2);
success(done, io, socket1, socket2, socket3);
}
});
});
it("should broadcast and return if the packet is sent to 0 client", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
@@ -498,4 +566,22 @@ describe("messaging many", () => {
success(done, io, socket1, socket2, socket3);
});
});
it("should precompute the WebSocket frame when broadcasting", (done) => {
const io = new Server(0);
const socket = createClient(io, "/chat", {
transports: ["websocket"],
});
const partialDone = createPartialDone(2, successFn(done, io, socket));
io.of("/chat").on("connection", (s) => {
s.conn.once("packetCreate", (packet) => {
expect(packet.options.wsPreEncodedFrame).to.be.an(Array);
partialDone();
});
io.of("/chat").compress(false).emit("woot", "hi");
});
socket.on("woot", partialDone);
});
});

View File

@@ -473,6 +473,24 @@ describe("namespaces", () => {
io.of("/nsp");
});
it("should not clean up a non-dynamic namespace", (done) => {
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
const c1 = createClient(io, "/chat");
c1.on("connect", () => {
c1.disconnect();
// Give it some time to disconnect the client
setTimeout(() => {
expect(io._nsps.has("/chat")).to.be(true);
expect(io._nsps.get("/chat")!.sockets.size).to.be(0);
success(done, io);
}, 100);
});
io.of("/chat");
});
describe("dynamic namespaces", () => {
it("should allow connections to dynamic namespaces with a regex", (done) => {
const io = new Server(0);
@@ -571,5 +589,79 @@ describe("namespaces", () => {
one.on("message", handler);
two.on("message", handler);
});
it("should clean up namespace when cleanupEmptyChildNamespaces is on and there are no more sockets in a namespace", (done) => {
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
const c1 = createClient(io, "/dynamic-101");
c1.on("connect", () => {
c1.disconnect();
// Give it some time to disconnect and clean up the namespace
setTimeout(() => {
expect(io._nsps.has("/dynamic-101")).to.be(false);
success(done, io);
}, 100);
});
io.of(/^\/dynamic-\d+$/);
});
it("should allow a client to connect to a cleaned up namespace", (done) => {
const io = new Server(0, { cleanupEmptyChildNamespaces: true });
const c1 = createClient(io, "/dynamic-101");
c1.on("connect", () => {
c1.disconnect();
// Give it some time to disconnect and clean up the namespace
setTimeout(() => {
expect(io._nsps.has("/dynamic-101")).to.be(false);
const c2 = createClient(io, "/dynamic-101");
c2.on("connect", () => {
success(done, io, c2);
});
c2.on("connect_error", () => {
done(
new Error("Client got error when connecting to dynamic namespace")
);
});
}, 100);
});
io.of(/^\/dynamic-\d+$/);
});
it("should not clean up namespace when cleanupEmptyChildNamespaces is off and there are no more sockets in a namespace", (done) => {
const io = new Server(0);
const c1 = createClient(io, "/dynamic-101");
c1.on("connect", () => {
c1.disconnect();
// Give it some time to disconnect and clean up the namespace
setTimeout(() => {
expect(io._nsps.has("/dynamic-101")).to.be(true);
expect(io._nsps.get("/dynamic-101")!.sockets.size).to.be(0);
success(done, io);
}, 100);
});
io.of(/^\/dynamic-\d+$/);
});
it("should attach a child namespace to its parent upon manual creation", () => {
const io = new Server(0);
const parentNamespace = io.of(/^\/dynamic-\d+$/);
const childNamespace = io.of("/dynamic-101");
// @ts-ignore
expect(parentNamespace.children.has(childNamespace)).to.be(true);
io.close();
});
});
});

View File

@@ -16,7 +16,9 @@ describe("server attachment", () => {
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/javascript");
expect(res.headers["content-type"]).to.be(
"application/javascript; charset=utf-8"
);
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.headers["x-sourcemap"]).to.be(undefined);
expect(res.text).to.match(/engine\.io/);
@@ -33,7 +35,9 @@ describe("server attachment", () => {
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/json");
expect(res.headers["content-type"]).to.be(
"application/json; charset=utf-8"
);
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.text).to.match(/engine\.io/);
expect(res.status).to.be(200);

View File

@@ -54,4 +54,34 @@ describe("timeout", () => {
});
});
});
it("should timeout if the client does not acknowledge the event (promise)", (done) => {
const io = new Server(0);
const client = createClient(io, "/");
io.on("connection", async (socket) => {
try {
await socket.timeout(50).emitWithAck("unknown");
expect.fail();
} catch (err) {
expect(err).to.be.an(Error);
success(done, io, client);
}
});
});
it("should not timeout if the client does acknowledge the event (promise)", (done) => {
const io = new Server(0);
const client = createClient(io, "/");
client.on("echo", (arg, cb) => {
cb(arg);
});
io.on("connection", async (socket) => {
const value = await socket.timeout(50).emitWithAck("echo", 42);
expect(value).to.be(42);
success(done, io, client);
});
});
});

View File

@@ -4,6 +4,7 @@ import type { DefaultEventsMap } from "../lib/typed-events";
import { createServer } from "http";
import { expectError, expectType } from "tsd";
import { Adapter } from "socket.io-adapter";
import type { DisconnectReason } from "../lib/socket";
// This file is run by tsd, not mocha.
@@ -17,10 +18,10 @@ describe("server", () => {
sio.on("connection", (s) => {
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(s);
s.on("disconnect", (reason) => {
expectType<string>(reason);
expectType<DisconnectReason>(reason);
});
s.on("disconnecting", (reason) => {
expectType<string>(reason);
expectType<DisconnectReason>(reason);
});
});
sio.on("connect", (s) => {
@@ -91,6 +92,28 @@ describe("server", () => {
});
});
});
describe("emitWithAck", () => {
it("accepts any parameters", () => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(async () => {
const value = await sio
.timeout(1000)
.emitWithAck("ackFromServerSingleArg", true, "123");
expectType<any>(value);
sio.on("connection", async (s) => {
const value1 = await s.emitWithAck(
"ackFromServerSingleArg",
true,
"123"
);
expectType<any>(value1);
});
});
});
});
});
describe("single event map", () => {
@@ -166,10 +189,32 @@ describe("server", () => {
describe("listen and emit event maps", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
ackFromClient: (
a: string,
b: number,
ack: (c: string, d: number) => void
) => void;
}
interface ServerToClientEvents {
helloFromServer: (message: string, x: number) => void;
ackFromServer: (
a: boolean,
b: string,
ack: (c: boolean, d: string) => void
) => void;
ackFromServerSingleArg: (
a: boolean,
b: string,
ack: (c: string) => void
) => void;
multipleAckFromServer: (
a: boolean,
b: string,
ack: (c: string) => void
) => void;
}
describe("on", () => {
@@ -184,6 +229,13 @@ describe("server", () => {
expectType<string>(message);
done();
});
s.on("ackFromClient", (a, b, cb) => {
expectType<string>(a);
expectType<number>(b);
expectType<(c: string, d: number) => void>(cb);
cb("123", 456);
});
});
});
});
@@ -208,8 +260,45 @@ describe("server", () => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.emit("helloFromServer", "hi", 1);
sio.to("room").emit("helloFromServer", "hi", 1);
sio.timeout(1000).emit("helloFromServer", "hi", 1);
sio
.timeout(1000)
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});
sio.on("connection", (s) => {
s.emit("helloFromServer", "hi", 10);
s.emit("ackFromServer", true, "123", (c, d) => {
expectType<boolean>(c);
expectType<string>(d);
});
s.timeout(1000).emit("ackFromServer", true, "123", (err, c, d) => {
expectType<Error>(err);
expectType<boolean>(c);
expectType<string>(d);
});
s.timeout(1000)
.to("room")
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});
s.to("room")
.timeout(1000)
.emit("multipleAckFromServer", true, "123", (err, c) => {
expectType<Error>(err);
expectType<string[]>(c);
});
done();
});
});
@@ -219,6 +308,10 @@ describe("server", () => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
expectError(sio.emit("helloFromClient"));
expectError(sio.to("room").emit("helloFromClient"));
expectError(sio.timeout(1000).to("room").emit("helloFromClient"));
sio.on("connection", (s) => {
expectError(s.emit("helloFromClient", "hi"));
expectError(s.emit("helloFromServer", "hi", 10, "10"));
@@ -231,9 +324,45 @@ describe("server", () => {
});
});
});
describe("emitWithAck", () => {
it("accepts arguments of the correct types", (done) => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(async () => {
const value = await sio
.timeout(1000)
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value);
sio.on("connection", async (s) => {
const value1 = await s
.timeout(1000)
.to("room")
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value1);
const value2 = await s
.to("room")
.timeout(1000)
.emitWithAck("multipleAckFromServer", true, "123");
expectType<string[]>(value2);
const value3 = await s.emitWithAck(
"ackFromServerSingleArg",
true,
"123"
);
expectType<string>(value3);
done();
});
});
});
});
});
describe("listen and emit event maps", () => {
describe("listen and emit event maps for the serverSideEmit method", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
}
@@ -244,6 +373,7 @@ describe("server", () => {
interface InterServerEvents {
helloFromServerToServer: (message: string, x: number) => void;
ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void;
}
describe("on", () => {
@@ -258,7 +388,7 @@ describe("server", () => {
expectType<
Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents>
>(sio);
srv.listen(() => {
srv.listen(async () => {
sio.serverSideEmit("helloFromServerToServer", "hello", 10);
sio
.of("/test")
@@ -272,6 +402,22 @@ describe("server", () => {
expectType<string>(message);
expectType<number>(x);
});
sio.serverSideEmit("ackFromServerToServer", "foo", (err, bar) => {
expectType<Error>(err);
expectType<number[]>(bar);
});
const value = await sio.serverSideEmitWithAck(
"ackFromServerToServer",
"foo"
);
expectType<number[]>(value);
sio.on("ackFromServerToServer", (foo, cb) => {
expectType<string>(foo);
expectType<(bar: number) => void>(cb);
});
});
});
});

View File

@@ -1,6 +1,12 @@
import fs = require("fs");
import { join } from "path";
import { createClient, getPort, success, successFn } from "./support/util";
import {
createClient,
createPartialDone,
getPort,
success,
successFn,
} from "./support/util";
import { Server } from "..";
import expect from "expect.js";
@@ -599,6 +605,24 @@ describe("socket", () => {
});
});
it("should emit an event and wait for the acknowledgement", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", async (s) => {
socket.on("hi", (a, b, fn) => {
expect(a).to.be(1);
expect(b).to.be(2);
fn(3);
});
const val = await s.emitWithAck("hi", 1, 2);
expect(val).to.be(3);
success(done, io, socket);
});
});
it("should have access to the client", (done) => {
const io = new Server(0);
const socket = createClient(io);
@@ -731,7 +755,7 @@ describe("socket", () => {
});
});
it("should enable compresion by default", (done) => {
it("should enable compression by default", (done) => {
const io = new Server(0);
const socket = createClient(io, "/chat");
@@ -740,11 +764,11 @@ describe("socket", () => {
expect(packet.options.compress).to.be(true);
success(done, io, socket);
});
io.of("/chat").emit("woot", "hi");
s.emit("woot", "hi");
});
});
it("should disable compresion", (done) => {
it("should disable compression", (done) => {
const io = new Server(0);
const socket = createClient(io, "/chat");
@@ -753,7 +777,7 @@ describe("socket", () => {
expect(packet.options.compress).to.be(false);
success(done, io, socket);
});
io.of("/chat").compress(false).emit("woot", "hi");
s.compress(false).emit("woot", "hi");
});
});
@@ -995,6 +1019,22 @@ describe("socket", () => {
});
});
it("should call listener when broadcasting binary data", (done) => {
const io = new Server(0);
const clientSocket = createClient(io, "/", { multiplex: false });
io.on("connection", (socket) => {
socket.onAnyOutgoing((event, arg1) => {
expect(event).to.be("my-event");
expect(arg1).to.be.an(Uint8Array);
success(done, io, clientSocket);
});
io.emit("my-event", Uint8Array.of(1, 2, 3));
});
});
it("should prepend listener", (done) => {
const io = new Server(0);
const clientSocket = createClient(io, "/", { multiplex: false });
@@ -1039,5 +1079,30 @@ describe("socket", () => {
socket.emit("my-event", "123");
});
});
it("should disconnect all namespaces when calling disconnect(true)", (done) => {
const io = new Server(0);
io.of("/foo");
io.of("/bar");
const socket1 = createClient(io, "/", {
transports: ["websocket"],
});
const socket2 = createClient(io, "/foo");
const socket3 = createClient(io, "/bar");
io.of("/bar").on("connection", (socket) => {
socket.disconnect(true);
});
const partialDone = createPartialDone(
3,
successFn(done, io, socket1, socket2, socket3)
);
socket1.on("disconnect", partialDone);
socket2.on("disconnect", partialDone);
socket3.on("disconnect", partialDone);
});
});
});

View File

@@ -5,6 +5,7 @@ import {
Socket as ClientSocket,
SocketOptions,
} from "socket.io-client";
import request from "supertest";
const expect = require("expect.js");
const i = expect.stringify;
@@ -73,8 +74,46 @@ export function createPartialDone(count: number, done: (err?: Error) => void) {
};
}
export function waitFor(emitter, event) {
return new Promise((resolve) => {
export function waitFor<T = unknown>(emitter, event) {
return new Promise<T>((resolve) => {
emitter.once(event, resolve);
});
}
// TODO: update superagent as latest release now supports promises
export function eioHandshake(httpServer): Promise<string> {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
const sid = JSON.parse(res.text.substring(1)).sid;
resolve(sid);
});
});
}
export function eioPush(httpServer, sid: string, body: string): Promise<void> {
return new Promise((resolve) => {
request(httpServer)
.post("/socket.io/")
.send(body)
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end(() => {
resolve();
});
});
}
export function eioPoll(httpServer, sid): Promise<string> {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end((err, res) => {
resolve(res.text);
});
});
}

View File

@@ -53,7 +53,7 @@ describe("socket.io with uWebSocket.js-based engine", () => {
const partialDone = createPartialDone(done, 4);
client.on("connect", partialDone);
clientWSOnly.on("connect", partialDone);
clientWSOnly.once("connect", partialDone);
clientPollingOnly.on("connect", partialDone);
clientCustomNamespace.on("connect", partialDone);
});
@@ -200,7 +200,9 @@ describe("socket.io with uWebSocket.js-based engine", () => {
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/javascript");
expect(res.headers["content-type"]).to.be(
"application/javascript; charset=utf-8"
);
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.headers["x-sourcemap"]).to.be(undefined);
expect(res.text).to.match(/engine\.io/);