mirror of
https://github.com/socketio/socket.io.git
synced 2026-01-11 16:08:24 -05:00
Compare commits
115 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2e5d1f77f | ||
|
|
d8143cc067 | ||
|
|
b2dd7cf660 | ||
|
|
3734b74b45 | ||
|
|
8aa94991ce | ||
|
|
4e64123862 | ||
|
|
115a9819fd | ||
|
|
0c0eb00163 | ||
|
|
f8640d9451 | ||
|
|
93d446a545 | ||
|
|
184f3cf7af | ||
|
|
5d9220b69a | ||
|
|
129883958a | ||
|
|
6c27b8b0a6 | ||
|
|
f3ada7d8cc | ||
|
|
a21ad88828 | ||
|
|
54d5ee05a6 | ||
|
|
da2b542797 | ||
|
|
b7d54dbe8d | ||
|
|
d4a9b2cdcb | ||
|
|
547c541fb9 | ||
|
|
3b7ced7af7 | ||
|
|
c00bb9564c | ||
|
|
57e5f25e26 | ||
|
|
f4b698418a | ||
|
|
945c84be47 | ||
|
|
d3d0a2d5be | ||
|
|
19b225b0c8 | ||
|
|
8fae95dd18 | ||
|
|
e6f6b906db | ||
|
|
596eb88af7 | ||
|
|
e357daf585 | ||
|
|
10fa4a2690 | ||
|
|
8be95b3bd3 | ||
|
|
ba497ee3eb | ||
|
|
28038715cb | ||
|
|
134226e96c | ||
|
|
9890b036cf | ||
|
|
713a6b451b | ||
|
|
18f3fdab12 | ||
|
|
5ab8289c0a | ||
|
|
30430f0985 | ||
|
|
9b43c9167c | ||
|
|
8ecfcba5c1 | ||
|
|
572133a58d | ||
|
|
6e1bb62982 | ||
|
|
06e6838b18 | ||
|
|
1f03a44d1f | ||
|
|
be3d7f0f1f | ||
|
|
d12aab2d69 | ||
|
|
9f758689f6 | ||
|
|
0b35dc77c0 | ||
|
|
531104d332 | ||
|
|
8b204570a9 | ||
|
|
0b7d70ca42 | ||
|
|
2f96438952 | ||
|
|
02c87a8561 | ||
|
|
37b6d8fff0 | ||
|
|
af54565b2d | ||
|
|
aa5312a4b6 | ||
|
|
c82a4bdf1f | ||
|
|
770ee5949f | ||
|
|
3bf5d92735 | ||
|
|
fc82e44f73 | ||
|
|
c840bad43a | ||
|
|
f2b8de7191 | ||
|
|
51784d0305 | ||
|
|
c196689545 | ||
|
|
7a70f63499 | ||
|
|
e5897dd7dc | ||
|
|
2071a66c5a | ||
|
|
0f11c4745f | ||
|
|
b839a3b400 | ||
|
|
f0ed42f18c | ||
|
|
b7213e71e4 | ||
|
|
2da82103d2 | ||
|
|
02b0f73e2c | ||
|
|
c0d8c5ab23 | ||
|
|
fe8730ca0f | ||
|
|
ed8483da4d | ||
|
|
9d86397243 | ||
|
|
44e20ba5bf | ||
|
|
ccc5ec39a8 | ||
|
|
0ef2a4d02c | ||
|
|
95810aa62d | ||
|
|
60edecb3bd | ||
|
|
eb5fdbd03e | ||
|
|
4974e9077c | ||
|
|
033c5d399a | ||
|
|
7a74b66872 | ||
|
|
dc81fcf461 | ||
|
|
c100b7b61c | ||
|
|
f03eeca39a | ||
|
|
d8cc8aef7e | ||
|
|
ccfd8caba6 | ||
|
|
24fee27ba3 | ||
|
|
310f8557a7 | ||
|
|
dbd2a07cda | ||
|
|
94e27cd072 | ||
|
|
a4dffc6527 | ||
|
|
7c44893d78 | ||
|
|
b833f918c8 | ||
|
|
24d8d1f67f | ||
|
|
6f2a50b932 | ||
|
|
1633150b2b | ||
|
|
0cb6ac95b4 | ||
|
|
a2cf2486c3 | ||
|
|
995f38f4cc | ||
|
|
891b1870e9 | ||
|
|
b84ed1e41c | ||
|
|
fb6b0efec9 | ||
|
|
95d9e4a42f | ||
|
|
499c89250d | ||
|
|
93cce05fb3 | ||
|
|
dc381b72c6 |
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
labels: 'to triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -6,21 +6,31 @@ on:
|
||||
schedule:
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test-node:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [10.x, 12.x, 14.x, 15.x]
|
||||
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@v1
|
||||
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
|
||||
|
||||
555
CHANGELOG.md
555
CHANGELOG.md
@@ -1,3 +1,462 @@
|
||||
# History
|
||||
|
||||
## 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.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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* prevent the socket from joining a room after disconnection ([18f3fda](https://github.com/socketio/socket.io/commit/18f3fdab12947a9fee3e9c37cfc1da97027d1473))
|
||||
* **uws:** prevent the server from crashing after upgrade ([ba497ee](https://github.com/socketio/socket.io/commit/ba497ee3eb52c4abf1464380d015d8c788714364))
|
||||
|
||||
|
||||
|
||||
# [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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* forward the local flag to the adapter when using fetchSockets() ([30430f0](https://github.com/socketio/socket.io/commit/30430f0985f8e7c49394543d4c84913b6a15df60))
|
||||
* **typings:** add HTTPS server to accepted types ([#4351](https://github.com/socketio/socket.io/issues/4351)) ([9b43c91](https://github.com/socketio/socket.io/commit/9b43c9167cff817c60fa29dbda2ef7cd938aff51))
|
||||
|
||||
|
||||
|
||||
# [4.5.0](https://github.com/socketio/socket.io/compare/4.4.1...4.5.0) (2022-04-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** ensure compatibility with TypeScript 3.x ([#4259](https://github.com/socketio/socket.io/issues/4259)) ([02c87a8](https://github.com/socketio/socket.io/commit/02c87a85614e217b8e7b93753f315790ae9d99f6))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for catch-all listeners for outgoing packets ([531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4))
|
||||
|
||||
This is similar to `onAny()`, but for outgoing packets.
|
||||
|
||||
Syntax:
|
||||
|
||||
```js
|
||||
socket.onAnyOutgoing((event, ...args) => {
|
||||
console.log(event);
|
||||
});
|
||||
```
|
||||
|
||||
* broadcast and expect multiple acks ([8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0))
|
||||
|
||||
Syntax:
|
||||
|
||||
```js
|
||||
io.timeout(1000).emit("some-event", (err, responses) => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
* add the "maxPayload" field in the handshake details ([088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38))
|
||||
|
||||
So that clients in HTTP long-polling can decide how many packets they have to send to stay under the maxHttpBufferSize
|
||||
value.
|
||||
|
||||
This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as
|
||||
we only add a field in the JSON-encoded handshake data:
|
||||
|
||||
```
|
||||
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## [4.4.1](https://github.com/socketio/socket.io/compare/4.4.0...4.4.1) (2022-01-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** make `RemoteSocket.data` type safe ([#4234](https://github.com/socketio/socket.io/issues/4234)) ([770ee59](https://github.com/socketio/socket.io/commit/770ee5949fb47c2556876c622f06c862573657d6))
|
||||
* **types:** pass `SocketData` type to custom namespaces ([#4233](https://github.com/socketio/socket.io/issues/4233)) ([f2b8de7](https://github.com/socketio/socket.io/commit/f2b8de71919e1b4d3e57f15a459972c1d1064787))
|
||||
|
||||
|
||||
|
||||
# [4.4.0](https://github.com/socketio/socket.io/compare/4.3.2...4.4.0) (2021-11-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* only set 'connected' to true after middleware execution ([02b0f73](https://github.com/socketio/socket.io/commit/02b0f73e2c64b09c72c5fbf7dc5f059557bdbe50))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add an implementation based on uWebSockets.js ([c0d8c5a](https://github.com/socketio/socket.io/commit/c0d8c5ab234d0d2bef0d0dec472973cc9662f647))
|
||||
* add timeout feature ([f0ed42f](https://github.com/socketio/socket.io/commit/f0ed42f18cabef20ad976aeec37077b6bf3837a5))
|
||||
* add type information to `socket.data` ([#4159](https://github.com/socketio/socket.io/issues/4159)) ([fe8730c](https://github.com/socketio/socket.io/commit/fe8730ca0f15bc92d5de81cf934c89c76d6af329))
|
||||
|
||||
|
||||
|
||||
## [4.3.2](https://github.com/socketio/socket.io/compare/4.3.1...4.3.2) (2021-11-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix race condition in dynamic namespaces ([#4137](https://github.com/socketio/socket.io/issues/4137)) ([9d86397](https://github.com/socketio/socket.io/commit/9d86397243bcbb5775a29d96e5ef03e17148a8e7))
|
||||
|
||||
|
||||
## [4.3.1](https://github.com/socketio/socket.io/compare/4.3.0...4.3.1) (2021-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix server attachment ([#4127](https://github.com/socketio/socket.io/issues/4127)) ([0ef2a4d](https://github.com/socketio/socket.io/commit/0ef2a4d02c9350aff163df9cb61aece89c4dac0f))
|
||||
|
||||
|
||||
# [4.3.0](https://github.com/socketio/socket.io/compare/4.2.0...4.3.0) (2021-10-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** add name field to cookie option ([#4099](https://github.com/socketio/socket.io/issues/4099)) ([033c5d3](https://github.com/socketio/socket.io/commit/033c5d399a2b985afad32c1e4b0c16d764e248cd))
|
||||
* send volatile packets with binary attachments ([dc81fcf](https://github.com/socketio/socket.io/commit/dc81fcf461cfdbb5b34b1a5a96b84373754047d5))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* serve ESM bundle ([60edecb](https://github.com/socketio/socket.io/commit/60edecb3bd33801803cdcba0aefbafa381a2abb3))
|
||||
|
||||
|
||||
# [4.2.0](https://github.com/socketio/socket.io/compare/4.1.3...4.2.0) (2021-08-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** allow async listener in typed events ([ccfd8ca](https://github.com/socketio/socket.io/commit/ccfd8caba6d38b7ba6c5114bd8179346ed07671c))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ignore the query string when serving client JavaScript ([#4024](https://github.com/socketio/socket.io/issues/4024)) ([24fee27](https://github.com/socketio/socket.io/commit/24fee27ba36485308f8e995879c10931532c814e))
|
||||
|
||||
|
||||
## [4.1.3](https://github.com/socketio/socket.io/compare/4.1.2...4.1.3) (2021-07-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix io.except() method ([94e27cd](https://github.com/socketio/socket.io/commit/94e27cd072c8a4eeb9636f6ffbb7a21d382f36b0))
|
||||
* remove x-sourcemap header ([a4dffc6](https://github.com/socketio/socket.io/commit/a4dffc6527f412d51a786ae5bf2e9080fe1ca63c))
|
||||
|
||||
|
||||
## [4.1.2](https://github.com/socketio/socket.io/compare/4.1.1...4.1.2) (2021-05-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** ensure compatibility with TypeScript 3.x ([0cb6ac9](https://github.com/socketio/socket.io/commit/0cb6ac95b49a27483b6f1b6402fa54b35f82e36f))
|
||||
* ensure compatibility with previous versions of the adapter ([a2cf248](https://github.com/socketio/socket.io/commit/a2cf2486c366cb62293101c10520c57f6984a3fc))
|
||||
|
||||
|
||||
## [4.1.1](https://github.com/socketio/socket.io/compare/4.1.0...4.1.1) (2021-05-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **typings:** properly type server-side events ([b84ed1e](https://github.com/socketio/socket.io/commit/b84ed1e41c9053792caf58974c5de9395bfd509f))
|
||||
* **typings:** properly type the adapter attribute ([891b187](https://github.com/socketio/socket.io/commit/891b1870e92d1ec38910f03bb839817e2d6be65a))
|
||||
|
||||
|
||||
# [4.1.0](https://github.com/socketio/socket.io/compare/4.0.2...4.1.0) (2021-05-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add support for inter-server communication ([93cce05](https://github.com/socketio/socket.io/commit/93cce05fb3faf91f21fa71212275c776aa161107))
|
||||
* notify upon namespace creation ([499c892](https://github.com/socketio/socket.io/commit/499c89250d2db1ab7725ab2b74840e188c267c46))
|
||||
* add a "connection_error" event ([7096e98](https://github.com/socketio/engine.io/commit/7096e98a02295a62c8ea2aa56461d4875887092d), from `engine.io`)
|
||||
* add the "initial_headers" and "headers" events ([2527543](https://github.com/socketio/engine.io/commit/252754353a0e88eb036ebb3082e9d6a9a5f497db), from `engine.io`)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* add support for the "wsPreEncoded" writing option ([dc381b7](https://github.com/socketio/socket.io/commit/dc381b72c6b2f8172001dedd84116122e4cc95b3))
|
||||
|
||||
|
||||
## [4.0.2](https://github.com/socketio/socket.io/compare/4.0.1...4.0.2) (2021-05-06)
|
||||
|
||||
|
||||
@@ -66,6 +525,16 @@
|
||||
* 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)
|
||||
|
||||
|
||||
@@ -79,6 +548,17 @@
|
||||
* 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)
|
||||
|
||||
|
||||
@@ -364,3 +844,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
|
||||
|
||||
15
Readme.md
15
Readme.md
@@ -1,9 +1,7 @@
|
||||
# socket.io
|
||||
[](https://repl.it/github/socketio/socket.io)
|
||||
[](https://replit.com/@socketio/socketio-minimal-example)
|
||||
[](#backers) [](#sponsors)
|
||||
[](https://github.com/socketio/socket.io/actions)
|
||||
[](https://david-dm.org/socketio/socket.io)
|
||||
[](https://david-dm.org/socketio/socket.io#info=devDependencies)
|
||||
[](https://www.npmjs.com/package/socket.io)
|
||||

|
||||
[](https://slackin-socketio.now.sh)
|
||||
@@ -22,7 +20,8 @@ Some implementations in other languages are also available:
|
||||
- [Swift](https://github.com/socketio/socket.io-client-swift)
|
||||
- [Dart](https://github.com/rikulo/socket.io-client-dart)
|
||||
- [Python](https://github.com/miguelgrinberg/python-socketio)
|
||||
- [.Net](https://github.com/Quobject/SocketIoClientDotNet)
|
||||
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
|
||||
- [Rust](https://github.com/1c3t3a/rust-socketio)
|
||||
|
||||
Its main features are:
|
||||
|
||||
@@ -115,6 +114,14 @@ io.on('connection', client => { ... });
|
||||
io.listen(3000);
|
||||
```
|
||||
|
||||
### Module syntax
|
||||
|
||||
```js
|
||||
import { Server } from "socket.io";
|
||||
const io = new Server(server);
|
||||
io.listen(3000);
|
||||
```
|
||||
|
||||
### In conjunction with Express
|
||||
|
||||
Starting with **3.0**, express applications have become request handler
|
||||
|
||||
22
SECURITY.md
Normal file
22
SECURITY.md
Normal 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.
|
||||
7
client-dist/socket.io.esm.min.js
vendored
Normal file
7
client-dist/socket.io.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
client-dist/socket.io.esm.min.js.map
Normal file
1
client-dist/socket.io.esm.min.js.map
Normal file
File diff suppressed because one or more lines are too long
10492
client-dist/socket.io.js
10492
client-dist/socket.io.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
8
client-dist/socket.io.min.js
vendored
8
client-dist/socket.io.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
client-dist/socket.io.msgpack.min.js
vendored
8
client-dist/socket.io.msgpack.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,6 +2,13 @@
|
||||
|
||||
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
|
||||
|
||||
This repository contains several implementations of the server:
|
||||
|
||||
| Directory | Language | Database | Cluster? |
|
||||
|----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
|
||||
| `server/` | TypeScript | in-memory | No |
|
||||
| `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
|
||||
|
||||
## Running the frontend
|
||||
|
||||
```
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.0.3",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-jasmine-html-reporter": "~1.5.0",
|
||||
"protractor": "~7.0.0",
|
||||
"ts-node": "~8.3.0",
|
||||
"tslint": "~6.1.0",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
A basic TODO project.
|
||||
|
||||
| Characteristic | |
|
||||
|----------------|-------------------------------------------------------------------------------------------|
|
||||
| Language | plain JavaScript |
|
||||
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) |
|
||||
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ docker-compose up -d
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: "changeit"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Server } from "socket.io";
|
||||
import createTodoHandlers from "./todo-management/todo.handlers.js";
|
||||
import { setupWorker } from "@socket.io/sticky";
|
||||
import { createAdapter } from "@socket.io/postgres-adapter";
|
||||
|
||||
export function createApplication(httpServer, components, serverOptions = {}) {
|
||||
const io = new Server(httpServer, serverOptions);
|
||||
|
||||
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } =
|
||||
createTodoHandlers(components);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("todo:create", createTodo);
|
||||
socket.on("todo:read", readTodo);
|
||||
socket.on("todo:update", updateTodo);
|
||||
socket.on("todo:delete", deleteTodo);
|
||||
socket.on("todo:list", listTodo);
|
||||
});
|
||||
|
||||
// enable sticky session in the cluster (to remove in standalone mode)
|
||||
setupWorker(io);
|
||||
|
||||
io.adapter(createAdapter(components.connectionPool));
|
||||
|
||||
return io;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import cluster from "cluster";
|
||||
import { createServer } from "http";
|
||||
import { setupMaster } from "@socket.io/sticky";
|
||||
import { cpus } from "os";
|
||||
|
||||
if (cluster.isMaster) {
|
||||
console.log(`Master ${process.pid} is running`);
|
||||
const httpServer = createServer();
|
||||
|
||||
setupMaster(httpServer, {
|
||||
loadBalancingMethod: "least-connection",
|
||||
});
|
||||
|
||||
httpServer.listen(3000);
|
||||
|
||||
for (let i = 0; i < cpus().length; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
|
||||
cluster.on("exit", (worker) => {
|
||||
console.log(`Worker ${worker.process.pid} died`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
console.log(`Worker ${process.pid} started`);
|
||||
|
||||
import("./index.js");
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createServer } from "http";
|
||||
import { createApplication } from "./app.js";
|
||||
import { Sequelize } from "sequelize";
|
||||
import pg from "pg";
|
||||
import { PostgresTodoRepository } from "./todo-management/todo.repository.js";
|
||||
|
||||
const httpServer = createServer();
|
||||
|
||||
const sequelize = new Sequelize("postgres", "postgres", "changeit", {
|
||||
dialect: "postgres",
|
||||
});
|
||||
|
||||
const connectionPool = new pg.Pool({
|
||||
user: "postgres",
|
||||
host: "localhost",
|
||||
database: "postgres",
|
||||
password: "changeit",
|
||||
port: 5432,
|
||||
});
|
||||
|
||||
createApplication(
|
||||
httpServer,
|
||||
{
|
||||
connectionPool,
|
||||
todoRepository: new PostgresTodoRepository(sequelize),
|
||||
},
|
||||
{
|
||||
cors: {
|
||||
origin: ["http://localhost:4200"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const main = async () => {
|
||||
// create the tables if they do not exist already
|
||||
await sequelize.sync();
|
||||
|
||||
// create the table needed by the postgres adapter
|
||||
await connectionPool.query(`
|
||||
CREATE TABLE IF NOT EXISTS socket_io_attachments (
|
||||
id bigserial UNIQUE,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
payload bytea
|
||||
);
|
||||
`);
|
||||
|
||||
// uncomment when running in standalone mode
|
||||
// httpServer.listen(3000);
|
||||
};
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Joi from "joi";
|
||||
|
||||
const idSchema = Joi.string().guid({
|
||||
version: "uuidv4",
|
||||
});
|
||||
|
||||
const todoSchema = Joi.object({
|
||||
id: idSchema.alter({
|
||||
create: (schema) => schema.forbidden(),
|
||||
update: (schema) => schema.required(),
|
||||
}),
|
||||
title: Joi.string().max(256).required(),
|
||||
completed: Joi.boolean().required(),
|
||||
});
|
||||
|
||||
export default function (components) {
|
||||
const { todoRepository } = components;
|
||||
return {
|
||||
createTodo: async function (payload, callback) {
|
||||
const socket = this;
|
||||
|
||||
// validate the payload
|
||||
const { error, value } = todoSchema.tailor("create").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
value.id = uuid();
|
||||
|
||||
// persist the entity
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
// acknowledge the creation
|
||||
callback({
|
||||
data: value.id,
|
||||
});
|
||||
|
||||
// notify the other users
|
||||
socket.broadcast.emit("todo:created", value);
|
||||
},
|
||||
|
||||
readTodo: async function (id, callback) {
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const todo = await todoRepository.findById(id);
|
||||
callback({
|
||||
data: todo,
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateTodo: async function (payload, callback) {
|
||||
const socket = this;
|
||||
|
||||
const { error, value } = todoSchema.tailor("update").validate(payload, {
|
||||
abortEarly: false,
|
||||
stripUnknown: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.INVALID_PAYLOAD,
|
||||
errorDetails: mapErrorDetails(error.details),
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.save(value);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:updated", value);
|
||||
},
|
||||
|
||||
deleteTodo: async function (id, callback) {
|
||||
const socket = this;
|
||||
|
||||
const { error } = idSchema.validate(id);
|
||||
|
||||
if (error) {
|
||||
return callback({
|
||||
error: Errors.ENTITY_NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await todoRepository.deleteById(id);
|
||||
} catch (e) {
|
||||
return callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
socket.broadcast.emit("todo:deleted", id);
|
||||
},
|
||||
|
||||
listTodo: async function (callback) {
|
||||
try {
|
||||
callback({
|
||||
data: await todoRepository.findAll(),
|
||||
});
|
||||
} catch (e) {
|
||||
callback({
|
||||
error: sanitizeErrorMessage(e),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Errors } from "../util.js";
|
||||
import { Model, DataTypes } from "sequelize";
|
||||
|
||||
class CrudRepository {
|
||||
findAll() {}
|
||||
findById(id) {}
|
||||
save(entity) {}
|
||||
deleteById(id) {}
|
||||
}
|
||||
|
||||
export class TodoRepository extends CrudRepository {}
|
||||
|
||||
class Todo extends Model {}
|
||||
|
||||
export class PostgresTodoRepository extends TodoRepository {
|
||||
constructor(sequelize) {
|
||||
super();
|
||||
this.sequelize = sequelize;
|
||||
|
||||
Todo.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
completed: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: "todos",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return this.sequelize.transaction((transaction) => {
|
||||
return Todo.findAll({ transaction });
|
||||
});
|
||||
}
|
||||
|
||||
async findById(id) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const todo = await Todo.findByPk(id, { transaction });
|
||||
|
||||
if (!todo) {
|
||||
throw Errors.ENTITY_NOT_FOUND;
|
||||
}
|
||||
|
||||
return todo;
|
||||
});
|
||||
}
|
||||
|
||||
save(entity) {
|
||||
return this.sequelize.transaction((transaction) => {
|
||||
return Todo.upsert(entity, { transaction });
|
||||
});
|
||||
}
|
||||
|
||||
async deleteById(id) {
|
||||
return this.sequelize.transaction(async (transaction) => {
|
||||
const count = await Todo.destroy({ where: { id }, transaction });
|
||||
|
||||
if (count === 0) {
|
||||
throw Errors.ENTITY_NOT_FOUND;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const Errors = {
|
||||
ENTITY_NOT_FOUND: "entity not found",
|
||||
INVALID_PAYLOAD: "invalid payload",
|
||||
};
|
||||
|
||||
const errorValues = Object.values(Errors);
|
||||
|
||||
export function sanitizeErrorMessage(message) {
|
||||
if (typeof message === "string" && errorValues.includes(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return "an unknown error has occurred";
|
||||
}
|
||||
}
|
||||
|
||||
export function mapErrorDetails(details) {
|
||||
return details.map((item) => ({
|
||||
message: item.message,
|
||||
path: item.path,
|
||||
type: item.type,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "basic-crud-server",
|
||||
"version": "0.0.1",
|
||||
"description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)",
|
||||
"main": "lib/cluster.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node lib/cluster.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/socketio/socket.io.git"
|
||||
},
|
||||
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/socketio/socket.io/issues"
|
||||
},
|
||||
"homepage": "https://github.com/socketio/socket.io#readme",
|
||||
"dependencies": {
|
||||
"@socket.io/postgres-adapter": "^0.2.0",
|
||||
"@socket.io/sticky": "^1.0.1",
|
||||
"joi": "^17.4.0",
|
||||
"pg": "^8.7.3",
|
||||
"pg-hstore": "^2.3.4",
|
||||
"sequelize": "^6.18.0",
|
||||
"socket.io": "^4.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ export enum Errors {
|
||||
|
||||
const errorValues: string[] = Object.values(Errors);
|
||||
|
||||
export function sanitizeErrorMessage(message: string) {
|
||||
if (errorValues.includes(message)) {
|
||||
export function sanitizeErrorMessage(message: any) {
|
||||
if (typeof message === "string" && errorValues.includes(message)) {
|
||||
return message;
|
||||
} else {
|
||||
return "an unknown error has occurred";
|
||||
|
||||
@@ -25,12 +25,13 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ A simple chat demo for Socket.IO
|
||||
## How to use
|
||||
|
||||
```
|
||||
$ npm ci
|
||||
$ npm i
|
||||
$ npm start
|
||||
```
|
||||
|
||||
|
||||
@@ -264,14 +264,14 @@ $(function() {
|
||||
log('you have been disconnected');
|
||||
});
|
||||
|
||||
socket.on('reconnect', () => {
|
||||
socket.io.on('reconnect', () => {
|
||||
log('you have been reconnected');
|
||||
if (username) {
|
||||
socket.emit('add user', username);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('reconnect_error', () => {
|
||||
socket.io.on('reconnect_error', () => {
|
||||
log('attempt to reconnect has failed');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
nginx:
|
||||
build: ./nginx
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
links:
|
||||
- server-john
|
||||
- server-paul
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
@@ -1,15 +1,18 @@
|
||||
// Setup basic express server
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var server = require('http').createServer(app);
|
||||
var io = require('socket.io')(server);
|
||||
var redis = require('socket.io-redis');
|
||||
var port = process.env.PORT || 3000;
|
||||
var serverName = process.env.NAME || 'Unknown';
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const server = require('http').createServer(app);
|
||||
const io = require('socket.io')(server);
|
||||
const { createAdapter } = require('@socket.io/redis-adapter');
|
||||
const { createClient } = require('redis');
|
||||
const port = process.env.PORT || 3000;
|
||||
const serverName = process.env.NAME || 'Unknown';
|
||||
|
||||
io.adapter(redis({ host: 'redis', port: 6379 }));
|
||||
const pubClient = createClient({ host: 'redis', port: 6379 });
|
||||
const subClient = pubClient.duplicate();
|
||||
|
||||
server.listen(port, function () {
|
||||
io.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log('Server listening at port %d', port);
|
||||
console.log('Hello, I\'m %s, how can I help?', serverName);
|
||||
});
|
||||
@@ -19,15 +22,15 @@ app.use(express.static(__dirname + '/public'));
|
||||
|
||||
// Chatroom
|
||||
|
||||
var numUsers = 0;
|
||||
let numUsers = 0;
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
io.on('connection', socket => {
|
||||
socket.emit('my-name-is', serverName);
|
||||
|
||||
var addedUser = false;
|
||||
let addedUser = false;
|
||||
|
||||
// when the client emits 'new message', this listens and executes
|
||||
socket.on('new message', function (data) {
|
||||
socket.on('new message', data => {
|
||||
// we tell the client to execute 'new message'
|
||||
socket.broadcast.emit('new message', {
|
||||
username: socket.username,
|
||||
@@ -36,7 +39,7 @@ io.on('connection', function (socket) {
|
||||
});
|
||||
|
||||
// when the client emits 'add user', this listens and executes
|
||||
socket.on('add user', function (username) {
|
||||
socket.on('add user', username => {
|
||||
if (addedUser) return;
|
||||
|
||||
// we store the username in the socket session for this client
|
||||
@@ -54,21 +57,21 @@ io.on('connection', function (socket) {
|
||||
});
|
||||
|
||||
// when the client emits 'typing', we broadcast it to others
|
||||
socket.on('typing', function () {
|
||||
socket.on('typing', () => {
|
||||
socket.broadcast.emit('typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the client emits 'stop typing', we broadcast it to others
|
||||
socket.on('stop typing', function () {
|
||||
socket.on('stop typing', () => {
|
||||
socket.broadcast.emit('stop typing', {
|
||||
username: socket.username
|
||||
});
|
||||
});
|
||||
|
||||
// when the user disconnects.. perform this
|
||||
socket.on('disconnect', function () {
|
||||
socket.on('disconnect', () => {
|
||||
if (addedUser) {
|
||||
--numUsers;
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/redis-adapter": "^7.0.1",
|
||||
"express": "4.13.4",
|
||||
"socket.io": "^4.0.0",
|
||||
"socket.io-redis": "^6.0.1"
|
||||
"redis": "^3.1.2",
|
||||
"socket.io": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import io from 'socket.io-client';
|
||||
|
||||
@@ -23,7 +24,7 @@ function App() {
|
||||
socket.off('disconnect');
|
||||
socket.off('message');
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const sendMessage = () => {
|
||||
socket.emit('hello!');
|
||||
@@ -32,9 +33,21 @@ function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>Connected: { '' + isConnected }</p>
|
||||
<p>Last message: { lastMessage || '-' }</p>
|
||||
<button onClick={ sendMessage }>Say hello!</button>
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
|
||||
16
examples/express-session-example/README.md
Normal file
16
examples/express-session-example/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Example with [express-session](https://www.npmjs.com/package/express-session)
|
||||
|
||||
This example shows how to share a session context between [Express](http://expressjs.com/) and [Socket.IO](https://socket.io/docs/v4/):
|
||||
|
||||

|
||||
|
||||
Please read the related guide: https://socket.io/how-to/use-with-express-session
|
||||
|
||||
## How to use
|
||||
|
||||
```
|
||||
$ npm install
|
||||
$ npm start
|
||||
```
|
||||
|
||||
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.
|
||||
BIN
examples/express-session-example/assets/demo.gif
Normal file
BIN
examples/express-session-example/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
57
examples/express-session-example/index.html
Normal file
57
examples/express-session-example/index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Example with express-session</title>
|
||||
</head>
|
||||
<body>
|
||||
<button onclick="incrementWithFetch()">Increment with fetch()</button>
|
||||
<button onclick="logout()">Logout</button>
|
||||
<p>Count: <span id="httpCount">0</span></p>
|
||||
|
||||
<button onclick="incrementWithEmit()">
|
||||
Increment with Socket.IO emit()
|
||||
</button>
|
||||
<p>Status: <span id="ioStatus">disconnected</span></p>
|
||||
<p>Count: <span id="ioCount">0</span></p>
|
||||
|
||||
<script src="/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const httpCount = document.getElementById("httpCount");
|
||||
const ioStatus = document.getElementById("ioStatus");
|
||||
const ioCount = document.getElementById("ioCount");
|
||||
|
||||
const socket = io({
|
||||
// with WebSocket only
|
||||
// transports: ["websocket"],
|
||||
});
|
||||
|
||||
async function incrementWithFetch() {
|
||||
const response = await fetch("/incr", {
|
||||
method: "post",
|
||||
});
|
||||
httpCount.innerText = await response.text();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch("/logout", {
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
|
||||
async function incrementWithEmit() {
|
||||
socket.emit("incr", (count) => {
|
||||
ioCount.innerText = count;
|
||||
});
|
||||
}
|
||||
|
||||
socket.on("connect", () => {
|
||||
ioStatus.innerText = "connected";
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
ioStatus.innerText = "disconnected";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
91
examples/express-session-example/index.js
Normal file
91
examples/express-session-example/index.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import { Server } from "socket.io";
|
||||
import session from "express-session";
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const sessionMiddleware = session({
|
||||
secret: "changeit",
|
||||
resave: true,
|
||||
saveUninitialized: true,
|
||||
});
|
||||
|
||||
app.use(sessionMiddleware);
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
res.sendFile("./index.html", { root: process.cwd() });
|
||||
});
|
||||
|
||||
app.post("/incr", (req, res) => {
|
||||
const session = req.session;
|
||||
session.count = (session.count || 0) + 1;
|
||||
res.status(200).end("" + session.count);
|
||||
});
|
||||
|
||||
app.post("/logout", (req, res) => {
|
||||
const sessionId = req.session.id;
|
||||
req.session.destroy(() => {
|
||||
// disconnect all Socket.IO connections linked to this session ID
|
||||
io.to(sessionId).disconnectSockets();
|
||||
res.status(204).end();
|
||||
});
|
||||
});
|
||||
|
||||
const io = new Server(httpServer, {
|
||||
allowRequest: (req, callback) => {
|
||||
// with HTTP long-polling, we have access to the HTTP response here, but this is not
|
||||
// the case with WebSocket, so we provide a dummy response object
|
||||
const fakeRes = {
|
||||
getHeader() {
|
||||
return [];
|
||||
},
|
||||
setHeader(key, values) {
|
||||
req.cookieHolder = values[0];
|
||||
},
|
||||
writeHead() {},
|
||||
};
|
||||
sessionMiddleware(req, fakeRes, () => {
|
||||
if (req.session) {
|
||||
// trigger the setHeader() above
|
||||
fakeRes.writeHead();
|
||||
// manually save the session (normally triggered by res.end())
|
||||
req.session.save();
|
||||
}
|
||||
callback(null, true);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
io.engine.on("initial_headers", (headers, req) => {
|
||||
if (req.cookieHolder) {
|
||||
headers["set-cookie"] = req.cookieHolder;
|
||||
delete req.cookieHolder;
|
||||
}
|
||||
});
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
const req = socket.request;
|
||||
|
||||
socket.join(req.session.id);
|
||||
|
||||
socket.on("incr", (cb) => {
|
||||
req.session.reload((err) => {
|
||||
if (err) {
|
||||
// session has expired
|
||||
return socket.disconnect();
|
||||
}
|
||||
req.session.count = (req.session.count || 0) + 1;
|
||||
req.session.save(() => {
|
||||
cb(req.session.count);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`application is running at: http://localhost:${port}`);
|
||||
});
|
||||
15
examples/express-session-example/package.json
Normal file
15
examples/express-session-example/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "express-session-example",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Example with express-session (https://github.com/expressjs/session)",
|
||||
"scripts": {
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "~4.17.3",
|
||||
"express-session": "~1.17.2",
|
||||
"socket.io": "~4.4.1"
|
||||
}
|
||||
}
|
||||
1
examples/rollup-server-bundle/.gitignore
vendored
Normal file
1
examples/rollup-server-bundle/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
bundle.js
|
||||
3
examples/rollup-server-bundle/index.js
Normal file
3
examples/rollup-server-bundle/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Server } from "socket.io";
|
||||
|
||||
new Server(0);
|
||||
19
examples/rollup-server-bundle/package.json
Normal file
19
examples/rollup-server-bundle/package.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "rollup-server-bundle",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"build": "rollup --config rollup.config.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^21.0.3",
|
||||
"@rollup/plugin-json": "^4.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.1.3",
|
||||
"rollup": "^2.70.1",
|
||||
"socket.io": "^4.4.1"
|
||||
}
|
||||
}
|
||||
12
examples/rollup-server-bundle/rollup.config.js
Normal file
12
examples/rollup-server-bundle/rollup.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import resolve from "@rollup/plugin-node-resolve";
|
||||
import commonjs from "@rollup/plugin-commonjs";
|
||||
import json from "@rollup/plugin-json";
|
||||
|
||||
export default {
|
||||
input: "index.js",
|
||||
output: {
|
||||
file: "bundle.js",
|
||||
format: "esm",
|
||||
},
|
||||
plugins: [resolve(), commonjs(), json({ compact: true })],
|
||||
};
|
||||
20
examples/webpack-build-server/index.js
Normal file
20
examples/webpack-build-server/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const { Server } = require("socket.io");
|
||||
|
||||
const clientFile = require("./node_modules/socket.io/client-dist/socket.io.min?raw");
|
||||
const clientMap = require("./node_modules/socket.io/client-dist/socket.io.min.js.map?raw");
|
||||
|
||||
Server.sendFile = (filename, req, res) => {
|
||||
res.end(filename.endsWith(".map") ? clientMap : clientFile);
|
||||
};
|
||||
|
||||
const io = new Server();
|
||||
|
||||
io.on("connection", socket => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect ${socket.id} due to ${reason}`);
|
||||
});
|
||||
});
|
||||
|
||||
io.listen(3000);
|
||||
@@ -1,15 +0,0 @@
|
||||
|
||||
const server = require('http').createServer();
|
||||
const io = require('socket.io')(server, {
|
||||
serveClient: false
|
||||
});
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
io.on('connect', onConnect);
|
||||
server.listen(port, () => console.log('server listening on port ' + port));
|
||||
|
||||
function onConnect(socket){
|
||||
console.log('connect ' + socket.id);
|
||||
|
||||
socket.on('disconnect', () => console.log('disconnect ' + socket.id));
|
||||
}
|
||||
@@ -4,13 +4,15 @@
|
||||
"description": "A sample Webpack build (for the server)",
|
||||
"scripts": {
|
||||
"start": "node dist/server.js",
|
||||
"build": "webpack --config ./support/webpack.config.js"
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "Damien Arrachequesne",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"bufferutil": "^4.0.3",
|
||||
"socket.io": "^4.0.0",
|
||||
"webpack": "~4.43.0",
|
||||
"webpack-cli": "~3.3.11"
|
||||
"utf-8-validate": "^5.0.5",
|
||||
"webpack": "^5.39.0",
|
||||
"webpack-cli": "^4.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
target: 'node',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'server.js'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
19
examples/webpack-build-server/webpack.config.js
Normal file
19
examples/webpack-build-server/webpack.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
entry: "./index.js",
|
||||
target: "node",
|
||||
mode: "production",
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
filename: "index.js",
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
resourceQuery: /raw/,
|
||||
type: "asset/source",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -7,14 +7,5 @@ A sample Webpack build for the browser.
|
||||
|
||||
```
|
||||
$ npm i
|
||||
$ npm run build-all
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
There are two WebPack configuration:
|
||||
|
||||
- the minimal configuration, just bundling the application and its dependencies. The `app.js` file in the `dist` folder is the result of that build.
|
||||
|
||||
- a slimmer one, where:
|
||||
- the JSON polyfill needed for IE6/IE7 support has been removed.
|
||||
- the `debug` calls and import have been removed (the [debug](https://github.com/visionmedia/debug) library is included in the build by default).
|
||||
- the source has been uglified (dropping IE8 support), and an associated SourceMap has been generated.
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- <script src="dist/app.js"></script> -->
|
||||
<script src="dist/app.slim.js"></script>
|
||||
<script src="dist/bundle.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
15
examples/webpack-build/index.js
Normal file
15
examples/webpack-build/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { io } from "socket.io-client";
|
||||
|
||||
const socket = io("http://localhost:3000");
|
||||
|
||||
socket.on("connect", () => {
|
||||
console.log(`connect ${socket.id}`);
|
||||
});
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
console.log(`connect_error due to ${err.message}`);
|
||||
});
|
||||
|
||||
socket.on("disconnect", (reason) => {
|
||||
console.log(`disconnect due to ${reason}`);
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
|
||||
import io from 'socket.io-client';
|
||||
|
||||
const socket = io('http://localhost:3000');
|
||||
|
||||
console.log('init');
|
||||
|
||||
socket.on('connect', onConnect);
|
||||
|
||||
function onConnect(){
|
||||
console.log('connect ' + socket.id);
|
||||
}
|
||||
@@ -2,20 +2,15 @@
|
||||
"name": "webpack-build",
|
||||
"version": "1.0.0",
|
||||
"description": "A sample Webpack build",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "webpack --config ./support/webpack.config.js",
|
||||
"build-slim": "webpack --config ./support/webpack.config.slim.js",
|
||||
"build-json-parser": "webpack --config ./support/webpack.config.json-parser.js",
|
||||
"build-all": "npm run build && npm run build-slim && npm run build-json-parser"
|
||||
"build": "webpack"
|
||||
},
|
||||
"author": "Damien Arrachequesne",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^2.0.2",
|
||||
"socket.io-json-parser": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"strip-loader": "^0.1.2",
|
||||
"webpack": "^2.6.1"
|
||||
"socket.io-client": "^4.4.1",
|
||||
"webpack": "^5.72.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
module.exports = function () { return function () {}; };
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'app.js'
|
||||
}
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'app.json-parser.js'
|
||||
},
|
||||
// generate sourcemap
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
// replace require('debug')() with an noop function
|
||||
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
|
||||
// replace socket.io-parser with socket.io-json-parser
|
||||
new webpack.NormalModuleReplacementPlugin(/socket\.io-parser/, 'socket.io-json-parser'),
|
||||
// use uglifyJS (IE9+ support)
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
// strip `debug()` calls
|
||||
test: /\.js$/,
|
||||
loader: 'strip-loader?strip[]=debug'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
|
||||
var webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: './lib/index.js',
|
||||
output: {
|
||||
path: require('path').join(__dirname, '../dist'),
|
||||
filename: 'app.slim.js'
|
||||
},
|
||||
// generate sourcemap
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
// replace require('debug')() with an noop function
|
||||
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
|
||||
// use uglifyJS (IE9+ support)
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
}
|
||||
})
|
||||
],
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
// strip `debug()` calls
|
||||
test: /\.js$/,
|
||||
loader: 'strip-loader?strip[]=debug'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
7
examples/webpack-build/webpack.config.js
Normal file
7
examples/webpack-build/webpack.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
entry: "./index.js",
|
||||
mode: "production",
|
||||
output: {
|
||||
filename: "bundle.js",
|
||||
},
|
||||
};
|
||||
@@ -7,32 +7,49 @@ import type {
|
||||
EventNames,
|
||||
EventsMap,
|
||||
TypedEventBroadcaster,
|
||||
DecorateAcknowledgements,
|
||||
DecorateAcknowledgementsWithTimeoutAndMultipleResponses,
|
||||
AllButLast,
|
||||
Last,
|
||||
SecondArg,
|
||||
} from "./typed-events";
|
||||
|
||||
export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
constructor(
|
||||
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> {
|
||||
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,
|
||||
@@ -41,31 +58,43 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
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> {
|
||||
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,
|
||||
@@ -76,13 +105,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
/**
|
||||
* 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> {
|
||||
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,
|
||||
@@ -95,12 +126,14 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
* receive messages (because of network slowness or other issues, or because they’re 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> {
|
||||
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,
|
||||
@@ -111,12 +144,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
/**
|
||||
* 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> {
|
||||
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,
|
||||
@@ -124,18 +160,55 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param timeout
|
||||
*/
|
||||
public timeout(timeout: number) {
|
||||
const flags = Object.assign({}, this.flags, { timeout });
|
||||
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,
|
||||
...args: EventParams<EmitEvents, Ev>
|
||||
): boolean {
|
||||
if (RESERVED_EVENTS.has(ev)) {
|
||||
throw new Error(`"${ev}" is a reserved event name`);
|
||||
throw new Error(`"${String(ev)}" is a reserved event name`);
|
||||
}
|
||||
// set up packet object
|
||||
const data = [ev, ...args];
|
||||
@@ -144,23 +217,111 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
data: data,
|
||||
};
|
||||
|
||||
if ("function" == typeof data[data.length - 1]) {
|
||||
throw new Error("Callbacks are not supported when broadcasting");
|
||||
const withAck = typeof data[data.length - 1] === "function";
|
||||
|
||||
if (!withAck) {
|
||||
this.adapter.broadcast(packet, {
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
this.adapter.broadcast(packet, {
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
const ack = data.pop() as (...args: any[]) => void;
|
||||
let timedOut = false;
|
||||
let responses: any[] = [];
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
timedOut = true;
|
||||
ack.apply(this, [
|
||||
new Error("operation has timed out"),
|
||||
this.flags.expectSingleResponse ? null : responses,
|
||||
]);
|
||||
}, this.flags.timeout);
|
||||
|
||||
let expectedServerCount = -1;
|
||||
let actualServerCount = 0;
|
||||
let expectedClientCount = 0;
|
||||
|
||||
const checkCompleteness = () => {
|
||||
if (
|
||||
!timedOut &&
|
||||
expectedServerCount === actualServerCount &&
|
||||
responses.length === expectedClientCount
|
||||
) {
|
||||
clearTimeout(timer);
|
||||
ack.apply(this, [
|
||||
null,
|
||||
this.flags.expectSingleResponse ? null : responses,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
this.adapter.broadcastWithAck(
|
||||
packet,
|
||||
{
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
},
|
||||
(clientCount) => {
|
||||
// each Socket.IO server in the cluster sends the number of clients that were notified
|
||||
expectedClientCount += clientCount;
|
||||
actualServerCount++;
|
||||
checkCompleteness();
|
||||
},
|
||||
(clientResponse) => {
|
||||
// each client sends an acknowledgement
|
||||
responses.push(clientResponse);
|
||||
checkCompleteness();
|
||||
}
|
||||
);
|
||||
|
||||
this.adapter.serverCount().then((serverCount) => {
|
||||
expectedServerCount = serverCount;
|
||||
checkCompleteness();
|
||||
});
|
||||
|
||||
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) {
|
||||
@@ -172,71 +333,122 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(): Promise<RemoteSocket<EmitEvents>[]> {
|
||||
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
|
||||
return this.adapter
|
||||
.fetchSockets({
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
})
|
||||
.then((sockets) => {
|
||||
return sockets.map((socket) => {
|
||||
if (socket instanceof Socket) {
|
||||
// FIXME the TypeScript compiler complains about missing private properties
|
||||
return (socket as unknown) as RemoteSocket<EmitEvents>;
|
||||
return socket as unknown as RemoteSocket<EmitEvents, SocketData>;
|
||||
} else {
|
||||
return new RemoteSocket(this.adapter, socket as SocketDetails);
|
||||
return new RemoteSocket(
|
||||
this.adapter,
|
||||
socket as SocketDetails<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(
|
||||
{
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
},
|
||||
Array.isArray(room) ? room : [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 {
|
||||
this.adapter.delSockets(
|
||||
{
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
},
|
||||
Array.isArray(room) ? room : [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 {
|
||||
this.adapter.disconnectSockets(
|
||||
{
|
||||
rooms: this.rooms,
|
||||
except: this.exceptRooms,
|
||||
flags: this.flags,
|
||||
},
|
||||
close
|
||||
);
|
||||
@@ -246,31 +458,69 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
|
||||
/**
|
||||
* Format of the data when the Socket instance exists on another Socket.IO server
|
||||
*/
|
||||
interface SocketDetails {
|
||||
interface SocketDetails<SocketData> {
|
||||
id: SocketId;
|
||||
handshake: Handshake;
|
||||
rooms: Room[];
|
||||
data: any;
|
||||
data: SocketData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose of subset of the attributes and methods of the Socket class
|
||||
*/
|
||||
export class RemoteSocket<EmitEvents extends EventsMap>
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
public readonly id: SocketId;
|
||||
public readonly handshake: Handshake;
|
||||
public readonly rooms: Set<Room>;
|
||||
public readonly data: any;
|
||||
public readonly data: SocketData;
|
||||
|
||||
private readonly operator: BroadcastOperator<EmitEvents>;
|
||||
private readonly operator: BroadcastOperator<EmitEvents, SocketData>;
|
||||
|
||||
constructor(adapter: Adapter, details: SocketDetails) {
|
||||
constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
|
||||
this.id = details.id;
|
||||
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>>(
|
||||
@@ -284,7 +534,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
|
||||
* Joins a room.
|
||||
*
|
||||
* @param {String|Array} room - room or array of rooms
|
||||
* @public
|
||||
*/
|
||||
public join(room: Room | Room[]): void {
|
||||
return this.operator.socketsJoin(room);
|
||||
@@ -294,7 +543,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
|
||||
* Leaves a room.
|
||||
*
|
||||
* @param {String} room
|
||||
* @public
|
||||
*/
|
||||
public leave(room: Room): void {
|
||||
return this.operator.socketsLeave(room);
|
||||
@@ -305,8 +553,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
|
||||
*
|
||||
* @param {Boolean} close - if `true`, closes the underlying connection
|
||||
* @return {Socket} self
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
public disconnect(close = false): this {
|
||||
this.operator.disconnectSockets(close);
|
||||
|
||||
156
lib/client.ts
156
lib/client.ts
@@ -7,21 +7,49 @@ import type { Namespace } from "./namespace";
|
||||
import type { EventsMap } from "./typed-events";
|
||||
import type { Socket } from "./socket";
|
||||
import type { SocketId } from "socket.io-adapter";
|
||||
import type { Socket as RawSocket } from "engine.io";
|
||||
|
||||
const debug = debugModule("socket.io:client");
|
||||
|
||||
interface WriteOptions {
|
||||
compress?: boolean;
|
||||
volatile?: boolean;
|
||||
preEncoded?: boolean;
|
||||
wsPreEncoded?: string;
|
||||
}
|
||||
|
||||
type CloseReason =
|
||||
| "transport error"
|
||||
| "transport close"
|
||||
| "forced close"
|
||||
| "ping timeout"
|
||||
| "parse error";
|
||||
|
||||
export class Client<
|
||||
ListenEvents extends EventsMap,
|
||||
EmitEvents extends EventsMap
|
||||
EmitEvents extends EventsMap,
|
||||
ServerSideEvents extends EventsMap,
|
||||
SocketData = any
|
||||
> {
|
||||
public readonly conn;
|
||||
public readonly conn: RawSocket;
|
||||
|
||||
private readonly id: string;
|
||||
private readonly server: Server<ListenEvents, EmitEvents>;
|
||||
private readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
private readonly encoder: Encoder;
|
||||
private readonly decoder: Decoder;
|
||||
private sockets: Map<SocketId, Socket<ListenEvents, EmitEvents>> = new Map();
|
||||
private nsps: Map<string, Socket<ListenEvents, EmitEvents>> = new Map();
|
||||
private sockets: Map<
|
||||
SocketId,
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
private nsps: Map<
|
||||
string,
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
private connectTimeout?: NodeJS.Timeout;
|
||||
|
||||
/**
|
||||
@@ -31,7 +59,10 @@ export class Client<
|
||||
* @param conn
|
||||
* @package
|
||||
*/
|
||||
constructor(server: Server<ListenEvents, EmitEvents>, conn: any) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
conn: any
|
||||
) {
|
||||
this.server = server;
|
||||
this.conn = conn;
|
||||
this.encoder = server.encoder;
|
||||
@@ -83,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);
|
||||
@@ -92,9 +123,12 @@ export class Client<
|
||||
this.server._checkNamespace(
|
||||
name,
|
||||
auth,
|
||||
(dynamicNspName: Namespace<ListenEvents, EmitEvents> | false) => {
|
||||
(
|
||||
dynamicNspName:
|
||||
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
| false
|
||||
) => {
|
||||
if (dynamicNspName) {
|
||||
debug("dynamic namespace %s was created", dynamicNspName);
|
||||
this.doConnect(name, auth);
|
||||
} else {
|
||||
debug("creation of namespace %s was denied", name);
|
||||
@@ -118,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);
|
||||
|
||||
@@ -150,7 +184,9 @@ export class Client<
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
|
||||
_remove(
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
): void {
|
||||
if (this.sockets.has(socket.id)) {
|
||||
const nsp = this.sockets.get(socket.id)!.nsp.name;
|
||||
this.sockets.delete(socket.id);
|
||||
@@ -180,30 +216,32 @@ export class Client<
|
||||
* @param {Object} opts
|
||||
* @private
|
||||
*/
|
||||
_packet(packet: Packet, opts?: any): void {
|
||||
opts = opts || {};
|
||||
const self = this;
|
||||
|
||||
// this writes to the actual connection
|
||||
function writeToEngine(encodedPackets: any) {
|
||||
// TODO clarify this.
|
||||
if (opts.volatile && !self.conn.transport.writable) return;
|
||||
for (let i = 0; i < encodedPackets.length; i++) {
|
||||
self.conn.write(encodedPackets[i], { compress: opts.compress });
|
||||
}
|
||||
}
|
||||
|
||||
if ("open" === this.conn.readyState) {
|
||||
debug("writing packet %j", packet);
|
||||
if (!opts.preEncoded) {
|
||||
// not broadcasting, need to encode
|
||||
writeToEngine(this.encoder.encode(packet)); // encode, then write results to engine
|
||||
} else {
|
||||
// a broadcast pre-encodes a packet
|
||||
writeToEngine(packet);
|
||||
}
|
||||
} else {
|
||||
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
|
||||
if (this.conn.readyState !== "open") {
|
||||
debug("ignoring packet write %j", packet);
|
||||
return;
|
||||
}
|
||||
const encodedPackets = opts.preEncoded
|
||||
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
|
||||
: this.encoder.encode(packet as Packet);
|
||||
this.writeToEngine(encodedPackets, opts);
|
||||
}
|
||||
|
||||
private writeToEngine(
|
||||
encodedPackets: Array<string | Buffer>,
|
||||
opts: WriteOptions
|
||||
): void {
|
||||
if (opts.volatile && !this.conn.transport.writable) {
|
||||
debug(
|
||||
"volatile packet is discarded since the transport is not currently writable"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const packets = Array.isArray(encodedPackets)
|
||||
? encodedPackets
|
||||
: [encodedPackets];
|
||||
for (const encodedPacket of packets) {
|
||||
this.conn.write(encodedPacket, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +255,7 @@ export class Client<
|
||||
try {
|
||||
this.decoder.add(data);
|
||||
} catch (e) {
|
||||
debug("invalid packet format");
|
||||
this.onerror(e);
|
||||
}
|
||||
}
|
||||
@@ -227,22 +266,31 @@ export class Client<
|
||||
* @private
|
||||
*/
|
||||
private ondecoded(packet: Packet): void {
|
||||
if (PacketType.CONNECT === packet.type) {
|
||||
if (this.conn.protocol === 3) {
|
||||
const parsed = url.parse(packet.nsp, true);
|
||||
this.connect(parsed.pathname!, parsed.query);
|
||||
} else {
|
||||
this.connect(packet.nsp, packet.data);
|
||||
}
|
||||
let namespace: string;
|
||||
let authPayload: Record<string, unknown>;
|
||||
if (this.conn.protocol === 3) {
|
||||
const parsed = url.parse(packet.nsp, true);
|
||||
namespace = parsed.pathname!;
|
||||
authPayload = parsed.query;
|
||||
} else {
|
||||
const socket = this.nsps.get(packet.nsp);
|
||||
if (socket) {
|
||||
process.nextTick(function () {
|
||||
socket._onpacket(packet);
|
||||
});
|
||||
} else {
|
||||
debug("no socket for namespace %s", packet.nsp);
|
||||
}
|
||||
namespace = packet.nsp;
|
||||
authPayload = packet.data;
|
||||
}
|
||||
const socket = this.nsps.get(namespace);
|
||||
|
||||
if (!socket && packet.type === PacketType.CONNECT) {
|
||||
this.connect(namespace, authPayload);
|
||||
} else if (
|
||||
socket &&
|
||||
packet.type !== PacketType.CONNECT &&
|
||||
packet.type !== PacketType.CONNECT_ERROR
|
||||
) {
|
||||
process.nextTick(function () {
|
||||
socket._onpacket(packet);
|
||||
});
|
||||
} else {
|
||||
debug("invalid state (packet type: %s)", packet.type);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,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
|
||||
@@ -273,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();
|
||||
|
||||
|
||||
722
lib/index.ts
722
lib/index.ts
File diff suppressed because it is too large
Load Diff
650
lib/namespace.ts
650
lib/namespace.ts
@@ -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");
|
||||
|
||||
@@ -20,35 +25,126 @@ export interface ExtendedError extends Error {
|
||||
|
||||
export interface NamespaceReservedEventsMap<
|
||||
ListenEvents extends EventsMap,
|
||||
EmitEvents extends EventsMap
|
||||
EmitEvents extends EventsMap,
|
||||
ServerSideEvents extends EventsMap,
|
||||
SocketData
|
||||
> {
|
||||
connect: (socket: Socket<ListenEvents, EmitEvents>) => void;
|
||||
connection: (socket: Socket<ListenEvents, EmitEvents>) => void;
|
||||
connect: (
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
connection: (
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface ServerReservedEventsMap<
|
||||
ListenEvents extends EventsMap,
|
||||
EmitEvents extends EventsMap,
|
||||
ServerSideEvents extends EventsMap,
|
||||
SocketData
|
||||
> extends NamespaceReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
> {
|
||||
new_namespace: (
|
||||
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) => void;
|
||||
}
|
||||
|
||||
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
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends StrictEventEmitter<
|
||||
{},
|
||||
ServerSideEvents,
|
||||
EmitEvents,
|
||||
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
|
||||
NamespaceReservedEventsMap<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>
|
||||
> {
|
||||
public readonly name: string;
|
||||
public readonly sockets: Map<
|
||||
SocketId,
|
||||
Socket<ListenEvents, EmitEvents>
|
||||
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Map();
|
||||
|
||||
public adapter: Adapter;
|
||||
|
||||
/** @private */
|
||||
readonly server: Server<ListenEvents, EmitEvents>;
|
||||
readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
|
||||
/** @private */
|
||||
_fns: Array<
|
||||
(
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
next: (err?: ExtendedError) => void
|
||||
) => void
|
||||
> = [];
|
||||
@@ -62,7 +158,10 @@ export class Namespace<
|
||||
* @param server instance
|
||||
* @param name
|
||||
*/
|
||||
constructor(server: Server<ListenEvents, EmitEvents>, name: string) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
name: string
|
||||
) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.name = name;
|
||||
@@ -77,18 +176,26 @@ export class Namespace<
|
||||
* @private
|
||||
*/
|
||||
_initAdapter(): void {
|
||||
// @ts-ignore
|
||||
this.adapter = new (this.server.adapter()!)(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: (
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
next: (err?: ExtendedError) => void
|
||||
) => void
|
||||
): this {
|
||||
@@ -104,7 +211,7 @@ export class Namespace<
|
||||
* @private
|
||||
*/
|
||||
private run(
|
||||
socket: Socket<ListenEvents, EmitEvents>,
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
|
||||
fn: (err: ExtendedError | null) => void
|
||||
) {
|
||||
const fns = this._fns.slice(0);
|
||||
@@ -129,34 +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> {
|
||||
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> {
|
||||
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> {
|
||||
return new BroadcastOperator(this.adapter).except(room);
|
||||
public except(room: Room | Room[]) {
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
|
||||
room
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,46 +301,93 @@ export class Namespace<
|
||||
* @return {Socket}
|
||||
* @private
|
||||
*/
|
||||
_add(
|
||||
client: Client<ListenEvents, EmitEvents>,
|
||||
query,
|
||||
fn?: () => void
|
||||
): Socket<ListenEvents, EmitEvents> {
|
||||
async _add(
|
||||
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
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) {
|
||||
if (err) {
|
||||
if (client.conn.protocol === 3) {
|
||||
return socket._error(err.data || err.message);
|
||||
} else {
|
||||
return socket._error({
|
||||
message: err.message,
|
||||
data: err.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
} else {
|
||||
if ("open" !== client.conn.readyState) {
|
||||
debug("next called after client was closed - ignoring socket");
|
||||
socket._cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
debug("middleware error, sending CONNECT_ERROR packet to the client");
|
||||
socket._cleanup();
|
||||
if (client.conn.protocol === 3) {
|
||||
return socket._error(err.data || err.message);
|
||||
} else {
|
||||
return socket._error({
|
||||
message: err.message,
|
||||
data: err.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
) {
|
||||
const session = await this.adapter.restoreSession(sessionId, offset);
|
||||
if (session) {
|
||||
debug("connection state recovered for sid %s", session.sid);
|
||||
return new Socket(this, client, auth, session);
|
||||
} else {
|
||||
debug("unable to restore session state");
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,7 +395,9 @@ export class Namespace<
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
|
||||
_remove(
|
||||
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
): void {
|
||||
if (this.sockets.has(socket.id)) {
|
||||
this.sockets.delete(socket.id);
|
||||
} else {
|
||||
@@ -221,23 +406,77 @@ 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,
|
||||
...args: EventParams<EmitEvents, Ev>
|
||||
): boolean {
|
||||
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args);
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit(
|
||||
ev,
|
||||
...args
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
@@ -245,35 +484,134 @@ 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);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
|
||||
ev: Ev,
|
||||
...args: EventParams<
|
||||
DecorateAcknowledgementsWithTimeoutAndMultipleResponses<ServerSideEvents>,
|
||||
Ev
|
||||
>
|
||||
): boolean {
|
||||
if (RESERVED_EVENTS.has(ev)) {
|
||||
throw new Error(`"${String(ev)}" is a reserved event name`);
|
||||
}
|
||||
args.unshift(ev);
|
||||
this.adapter.serverSideEmit(args);
|
||||
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
|
||||
*
|
||||
* @param args - an array of arguments, which may include an acknowledgement callback at the end
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_onServerSideEmit(args: [string, ...any[]]) {
|
||||
super.emitUntyped.apply(this, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
return new BroadcastOperator(this.adapter).compress(compress);
|
||||
public compress(compress: boolean) {
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
|
||||
compress
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,59 +619,149 @@ export class Namespace<
|
||||
* receive messages (because of network slowness or other issues, or because they’re 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> {
|
||||
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
|
||||
*/
|
||||
public get local(): BroadcastOperator<EmitEvents> {
|
||||
return new BroadcastOperator(this.adapter).local;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the matching socket instances
|
||||
* @example
|
||||
* const myNamespace = io.of("/my-namespace");
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
|
||||
return new BroadcastOperator(this.adapter).fetchSockets();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances join the specified rooms
|
||||
* // the “foo” event will be broadcast to all connected clients on this node
|
||||
* myNamespace.local.emit("foo", "bar");
|
||||
*
|
||||
* @param room
|
||||
* @public
|
||||
* @return a new {@link BroadcastOperator} instance for chaining
|
||||
*/
|
||||
public socketsJoin(room: Room | Room[]): void {
|
||||
return new BroadcastOperator(this.adapter).socketsJoin(room);
|
||||
public get local() {
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances leave the specified rooms
|
||||
* Adds a timeout in milliseconds for the next operation.
|
||||
*
|
||||
* @param room
|
||||
* @public
|
||||
* @example
|
||||
* const myNamespace = io.of("/my-namespace");
|
||||
*
|
||||
* 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
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* @param timeout
|
||||
*/
|
||||
public socketsLeave(room: Room | Room[]): void {
|
||||
return new BroadcastOperator(this.adapter).socketsLeave(room);
|
||||
public timeout(timeout: number) {
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
|
||||
timeout
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the matching socket instances disconnect
|
||||
* Returns the matching socket instances.
|
||||
*
|
||||
* 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");
|
||||
*
|
||||
* // 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 fetchSockets() {
|
||||
return new BroadcastOperator<EmitEvents, SocketData>(
|
||||
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}.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Namespace } from "./namespace";
|
||||
import type { Server } from "./index";
|
||||
import type { Server, RemoteSocket } from "./index";
|
||||
import type {
|
||||
EventParams,
|
||||
EventNames,
|
||||
@@ -7,15 +7,24 @@ import type {
|
||||
DefaultEventsMap,
|
||||
} from "./typed-events";
|
||||
import type { BroadcastOptions } from "socket.io-adapter";
|
||||
import debugModule from "debug";
|
||||
|
||||
const debug = debugModule("socket.io:parent-namespace");
|
||||
|
||||
export class ParentNamespace<
|
||||
ListenEvents extends EventsMap = DefaultEventsMap,
|
||||
EmitEvents extends EventsMap = ListenEvents
|
||||
> extends Namespace<ListenEvents, EmitEvents> {
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
|
||||
private static count: number = 0;
|
||||
private children: Set<Namespace<ListenEvents, EmitEvents>> = new Set();
|
||||
private children: Set<
|
||||
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
> = new Set();
|
||||
|
||||
constructor(server: Server<ListenEvents, EmitEvents>) {
|
||||
constructor(
|
||||
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
|
||||
) {
|
||||
super(server, "/_" + ParentNamespace.count++);
|
||||
}
|
||||
|
||||
@@ -43,7 +52,10 @@ export class ParentNamespace<
|
||||
return true;
|
||||
}
|
||||
|
||||
createChild(name: string): Namespace<ListenEvents, EmitEvents> {
|
||||
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) =>
|
||||
@@ -53,7 +65,31 @@ 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);
|
||||
return namespace;
|
||||
}
|
||||
|
||||
fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
|
||||
// note: we could make the fetchSockets() method work for dynamic namespaces created with a regex (by sending the
|
||||
// regex to the other Socket.IO servers, and returning the sockets of each matching namespace for example), but
|
||||
// the behavior for namespaces created with a function is less clear
|
||||
// note²: we cannot loop over each children namespace, because with multiple Socket.IO servers, a given namespace
|
||||
// may exist on one node but not exist on another (since it is created upon client connection)
|
||||
throw new Error("fetchSockets() is not supported on parent namespaces");
|
||||
}
|
||||
}
|
||||
|
||||
693
lib/socket.ts
693
lib/socket.ts
@@ -1,21 +1,27 @@
|
||||
import { Packet, PacketType } from "socket.io-parser";
|
||||
import url = require("url");
|
||||
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";
|
||||
@@ -26,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;
|
||||
}
|
||||
|
||||
@@ -46,7 +75,7 @@ export interface EventEmitterReservedEventsMap {
|
||||
|
||||
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
|
||||
| ClientReservedEvents
|
||||
| keyof NamespaceReservedEventsMap<never, never>
|
||||
| keyof NamespaceReservedEventsMap<never, never, never, never>
|
||||
| keyof SocketReservedEventsMap
|
||||
| keyof EventEmitterReservedEventsMap
|
||||
>(<const>[
|
||||
@@ -108,32 +137,107 @@ export interface Handshake {
|
||||
auth: { [key: string]: any };
|
||||
}
|
||||
|
||||
/**
|
||||
* `[eventName, ...args]`
|
||||
*/
|
||||
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
|
||||
EmitEvents extends EventsMap = ListenEvents,
|
||||
ServerSideEvents extends EventsMap = DefaultEventsMap,
|
||||
SocketData = any
|
||||
> extends StrictEventEmitter<
|
||||
ListenEvents,
|
||||
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: any = {};
|
||||
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;
|
||||
|
||||
public connected: boolean;
|
||||
public disconnected: boolean;
|
||||
/**
|
||||
* The session ID, which must not be shared (unlike {@link id}).
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private readonly pid: PrivateSessionId;
|
||||
|
||||
private readonly server: Server<ListenEvents, EmitEvents>;
|
||||
// TODO: remove this unused reference
|
||||
private readonly server: Server<
|
||||
ListenEvents,
|
||||
EmitEvents,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
>;
|
||||
private readonly adapter: Adapter;
|
||||
private acks: Map<number, () => void> = new Map();
|
||||
private fns: Array<
|
||||
(event: Array<any>, next: (err?: Error) => void) => void
|
||||
> = [];
|
||||
private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
|
||||
private flags: BroadcastFlags = {};
|
||||
private _anyListeners?: Array<(...args: any[]) => void>;
|
||||
private _anyOutgoingListeners?: Array<(...args: any[]) => void>;
|
||||
|
||||
/**
|
||||
* Interface to a `Client` for a given `Namespace`.
|
||||
@@ -144,21 +248,37 @@ export class Socket<
|
||||
* @package
|
||||
*/
|
||||
constructor(
|
||||
readonly nsp: Namespace<ListenEvents, EmitEvents>,
|
||||
readonly client: Client<ListenEvents, EmitEvents>,
|
||||
auth: object
|
||||
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
|
||||
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.connected = true;
|
||||
this.disconnected = false;
|
||||
this.handshake = this.buildHandshake(auth);
|
||||
}
|
||||
|
||||
@@ -177,7 +297,8 @@ export class Socket<
|
||||
secure: !!this.request.connection.encrypted,
|
||||
issued: +new Date(),
|
||||
url: this.request.url!,
|
||||
query: url.parse(this.request.url!, true).query,
|
||||
// @ts-ignore
|
||||
query: this.request._query,
|
||||
auth,
|
||||
};
|
||||
}
|
||||
@@ -185,15 +306,27 @@ 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,
|
||||
...args: EventParams<EmitEvents, Ev>
|
||||
): boolean {
|
||||
if (RESERVED_EVENTS.has(ev)) {
|
||||
throw new Error(`"${ev}" is a reserved event name`);
|
||||
throw new Error(`"${String(ev)}" is a reserved event name`);
|
||||
}
|
||||
const data: any[] = [ev, ...args];
|
||||
const packet: any = {
|
||||
@@ -203,57 +336,170 @@ export class Socket<
|
||||
|
||||
// access last argument to see if it's an ACK callback
|
||||
if (typeof data[data.length - 1] === "function") {
|
||||
debug("emitting packet with ack id %d", this.nsp._ids);
|
||||
this.acks.set(this.nsp._ids, data.pop());
|
||||
packet.id = this.nsp._ids++;
|
||||
const id = this.nsp._ids++;
|
||||
debug("emitting packet with ack id %d", id);
|
||||
|
||||
this.registerAckCallback(id, data.pop());
|
||||
packet.id = id;
|
||||
}
|
||||
|
||||
const flags = Object.assign({}, this.flags);
|
||||
this.flags = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Targets a room when broadcasting.
|
||||
* Emits an event and waits for an acknowledgement
|
||||
*
|
||||
* @param room
|
||||
* @return self
|
||||
* @public
|
||||
* @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 to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||
return this.newBroadcastOperator().to(room);
|
||||
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
|
||||
*/
|
||||
private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
|
||||
const timeout = this.flags.timeout;
|
||||
if (timeout === undefined) {
|
||||
this.acks.set(id, ack);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
debug("event with ack id %d has timed out after %d ms", id, timeout);
|
||||
this.acks.delete(id);
|
||||
ack.call(this, new Error("operation has timed out"));
|
||||
}, timeout);
|
||||
|
||||
this.acks.set(id, (...args) => {
|
||||
clearTimeout(timer);
|
||||
ack.apply(this, [null, ...args]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
|
||||
public to(room: Room | Room[]) {
|
||||
return this.newBroadcastOperator().to(room);
|
||||
}
|
||||
|
||||
/**
|
||||
* Targets a room when broadcasting. Similar to `to()`, but might feel clearer in some cases:
|
||||
*
|
||||
* @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[]) {
|
||||
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> {
|
||||
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);
|
||||
@@ -261,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);
|
||||
@@ -290,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);
|
||||
@@ -306,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);
|
||||
@@ -335,11 +596,15 @@ export class Socket<
|
||||
*/
|
||||
_onconnect(): void {
|
||||
debug("socket connected - writing packet");
|
||||
this.connected = true;
|
||||
this.join(this.id);
|
||||
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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,9 +636,6 @@ export class Socket<
|
||||
case PacketType.DISCONNECT:
|
||||
this.ondisconnect();
|
||||
break;
|
||||
|
||||
case PacketType.CONNECT_ERROR:
|
||||
this._onerror(new Error(packet.data));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,23 +732,47 @@ 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.leaveAll();
|
||||
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.disconnected = true;
|
||||
this.emitReserved("disconnect", reason);
|
||||
this.emitReserved("disconnect", reason, description);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the socket leave all the rooms it was part of and prevents it from joining any other room
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_cleanup() {
|
||||
this.leaveAll();
|
||||
this.join = noop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces an `error` packet.
|
||||
*
|
||||
@@ -501,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;
|
||||
@@ -520,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;
|
||||
@@ -534,8 +831,12 @@ export class Socket<
|
||||
* receive messages (because of network slowness or other issues, or because they’re 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;
|
||||
@@ -546,30 +847,67 @@ 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> {
|
||||
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> {
|
||||
public get local() {
|
||||
return this.newBroadcastOperator().local;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
*
|
||||
* @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 timeout(
|
||||
timeout: number
|
||||
): Socket<
|
||||
ListenEvents,
|
||||
DecorateAcknowledgements<EmitEvents>,
|
||||
ServerSideEvents,
|
||||
SocketData
|
||||
> {
|
||||
this.flags.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch incoming event to socket listeners.
|
||||
*
|
||||
* @param {Array} event - event that will get emitted
|
||||
* @private
|
||||
*/
|
||||
private dispatch(event: [eventName: string, ...args: any[]]): void {
|
||||
private dispatch(event: Event): void {
|
||||
debug("dispatching an event %j", event);
|
||||
this.run(event, (err) => {
|
||||
process.nextTick(() => {
|
||||
@@ -588,13 +926,27 @@ 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: Array<any>, next: (err?: Error) => void) => void
|
||||
): this {
|
||||
public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
|
||||
this.fns.push(fn);
|
||||
return this;
|
||||
}
|
||||
@@ -606,10 +958,7 @@ export class Socket<
|
||||
* @param {Function} fn - last fn call in the middleware
|
||||
* @private
|
||||
*/
|
||||
private run(
|
||||
event: [eventName: string, ...args: any[]],
|
||||
fn: (err: Error | null) => void
|
||||
): void {
|
||||
private run(event: Event, fn: (err: Error | null) => void): void {
|
||||
const fns = this.fns.slice(0);
|
||||
if (!fns.length) return fn(null);
|
||||
|
||||
@@ -629,10 +978,15 @@ export class Socket<
|
||||
run(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the socket is currently disconnected
|
||||
*/
|
||||
public get disconnected() {
|
||||
return !this.connected;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reference to the request that originated the underlying Engine.IO Socket.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
public get request(): IncomingMessage {
|
||||
return this.client.request;
|
||||
@@ -641,25 +995,47 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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 || [];
|
||||
@@ -668,11 +1044,10 @@ 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.
|
||||
* Adds a listener that will be fired when any event is received. 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
|
||||
* @public
|
||||
*/
|
||||
public prependAny(listener: (...args: any[]) => void): this {
|
||||
this._anyListeners = this._anyListeners || [];
|
||||
@@ -681,10 +1056,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 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) {
|
||||
@@ -707,21 +1096,119 @@ 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 || [];
|
||||
}
|
||||
|
||||
private newBroadcastOperator(): BroadcastOperator<EmitEvents> {
|
||||
/**
|
||||
* Adds a listener that will be fired when any event is sent. The event name is passed as the first argument to
|
||||
* the callback.
|
||||
*
|
||||
* Note: acknowledgements sent to the client are not included.
|
||||
*
|
||||
* @example
|
||||
* io.on("connection", (socket) => {
|
||||
* socket.onAnyOutgoing((event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public onAnyOutgoing(listener: (...args: any[]) => void): this {
|
||||
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
|
||||
this._anyOutgoingListeners.push(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @example
|
||||
* io.on("connection", (socket) => {
|
||||
* socket.prependAnyOutgoing((event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* });
|
||||
* });
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
|
||||
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
|
||||
this._anyOutgoingListeners.unshift(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener that will be fired when any event is sent.
|
||||
*
|
||||
* @example
|
||||
* io.on("connection", (socket) => {
|
||||
* const catchAllListener = (event, ...args) => {
|
||||
* console.log(`sent event ${event}`);
|
||||
* }
|
||||
*
|
||||
* socket.onAnyOutgoing(catchAllListener);
|
||||
*
|
||||
* // remove a specific listener
|
||||
* socket.offAnyOutgoing(catchAllListener);
|
||||
*
|
||||
* // or remove all listeners
|
||||
* socket.offAnyOutgoing();
|
||||
* });
|
||||
*
|
||||
* @param listener - the catch-all listener
|
||||
*/
|
||||
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
|
||||
if (!this._anyOutgoingListeners) {
|
||||
return this;
|
||||
}
|
||||
if (listener) {
|
||||
const listeners = this._anyOutgoingListeners;
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
if (listener === listeners[i]) {
|
||||
listeners.splice(i, 1);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this._anyOutgoingListeners = [];
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 listenersAnyOutgoing() {
|
||||
return this._anyOutgoingListeners || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the listeners for each packet sent (emit or broadcast)
|
||||
*
|
||||
* @param packet
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private notifyOutgoingListeners(packet: Packet) {
|
||||
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
|
||||
const listeners = this._anyOutgoingListeners.slice();
|
||||
for (const listener of listeners) {
|
||||
listener.apply(this, packet.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export type ReservedOrUserListener<
|
||||
* Needed because of https://github.com/microsoft/TypeScript/issues/41778
|
||||
*/
|
||||
type FallbackToUntypedListener<T> = [T] extends [never]
|
||||
? (...args: any[]) => void
|
||||
? (...args: any[]) => void | Promise<void>
|
||||
: T;
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,8 @@ export abstract class StrictEventEmitter<
|
||||
ReservedEvents extends EventsMap = {}
|
||||
>
|
||||
extends EventEmitter
|
||||
implements TypedEventBroadcaster<EmitEvents> {
|
||||
implements TypedEventBroadcaster<EmitEvents>
|
||||
{
|
||||
/**
|
||||
* Adds the `listener` function as an event listener for `ev`.
|
||||
*
|
||||
@@ -177,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];
|
||||
};
|
||||
|
||||
164
lib/uws.ts
Normal file
164
lib/uws.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { Adapter, Room } from "socket.io-adapter";
|
||||
import type { WebSocket } from "uWebSockets.js";
|
||||
import type { Socket } from "./socket.js";
|
||||
import { createReadStream, statSync } from "fs";
|
||||
import debugModule from "debug";
|
||||
|
||||
const debug = debugModule("socket.io:adapter-uws");
|
||||
|
||||
const SEPARATOR = "\x1f"; // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
|
||||
|
||||
const { addAll, del, broadcast } = Adapter.prototype;
|
||||
|
||||
export function patchAdapter(app /* : TemplatedApp */) {
|
||||
Adapter.prototype.addAll = function (id, rooms) {
|
||||
const isNew = !this.sids.has(id);
|
||||
addAll.call(this, id, rooms);
|
||||
const socket: Socket = this.nsp.sockets.get(id);
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
if (socket.conn.transport.name === "websocket") {
|
||||
subscribe(this.nsp.name, socket, isNew, rooms);
|
||||
return;
|
||||
}
|
||||
if (isNew) {
|
||||
socket.conn.on("upgrade", () => {
|
||||
const rooms = this.sids.get(id);
|
||||
if (rooms) {
|
||||
subscribe(this.nsp.name, socket, isNew, rooms);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Adapter.prototype.del = function (id, room) {
|
||||
del.call(this, id, room);
|
||||
const socket: Socket = this.nsp.sockets.get(id);
|
||||
if (socket && socket.conn.transport.name === "websocket") {
|
||||
// @ts-ignore
|
||||
const sessionId = socket.conn.id;
|
||||
// @ts-ignore
|
||||
const websocket: WebSocket = socket.conn.transport.socket;
|
||||
const topic = `${this.nsp.name}${SEPARATOR}${room}`;
|
||||
debug("unsubscribe connection %s from topic %s", sessionId, topic);
|
||||
websocket.unsubscribe(topic);
|
||||
}
|
||||
};
|
||||
|
||||
Adapter.prototype.broadcast = function (packet, opts) {
|
||||
const useFastPublish = opts.rooms.size <= 1 && opts.except!.size === 0;
|
||||
if (!useFastPublish) {
|
||||
broadcast.call(this, packet, opts);
|
||||
return;
|
||||
}
|
||||
|
||||
const flags = opts.flags || {};
|
||||
const basePacketOpts = {
|
||||
preEncoded: true,
|
||||
volatile: flags.volatile,
|
||||
compress: flags.compress,
|
||||
};
|
||||
|
||||
packet.nsp = this.nsp.name;
|
||||
const encodedPackets = this.encoder.encode(packet);
|
||||
|
||||
const topic =
|
||||
opts.rooms.size === 0
|
||||
? this.nsp.name
|
||||
: `${this.nsp.name}${SEPARATOR}${opts.rooms.keys().next().value}`;
|
||||
debug("fast publish to %s", topic);
|
||||
|
||||
// fast publish for clients connected with WebSocket
|
||||
encodedPackets.forEach((encodedPacket) => {
|
||||
const isBinary = typeof encodedPacket !== "string";
|
||||
// "4" being the message type in the Engine.IO protocol, see https://github.com/socketio/engine.io-protocol
|
||||
app.publish(
|
||||
topic,
|
||||
isBinary ? encodedPacket : "4" + encodedPacket,
|
||||
isBinary
|
||||
);
|
||||
});
|
||||
|
||||
this.apply(opts, (socket) => {
|
||||
if (socket.conn.transport.name !== "websocket") {
|
||||
// classic publish for clients connected with HTTP long-polling
|
||||
socket.client.writeToEngine(encodedPackets, basePacketOpts);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function subscribe(
|
||||
namespaceName: string,
|
||||
socket: Socket,
|
||||
isNew: boolean,
|
||||
rooms: Set<Room>
|
||||
) {
|
||||
// @ts-ignore
|
||||
const sessionId = socket.conn.id;
|
||||
// @ts-ignore
|
||||
const websocket: WebSocket = socket.conn.transport.socket;
|
||||
if (isNew) {
|
||||
debug("subscribe connection %s to topic %s", sessionId, namespaceName);
|
||||
websocket.subscribe(namespaceName);
|
||||
}
|
||||
rooms.forEach((room) => {
|
||||
const topic = `${namespaceName}${SEPARATOR}${room}`; // '#' can be used as wildcard
|
||||
debug("subscribe connection %s to topic %s", sessionId, topic);
|
||||
websocket.subscribe(topic);
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreAdapter() {
|
||||
Adapter.prototype.addAll = addAll;
|
||||
Adapter.prototype.del = del;
|
||||
Adapter.prototype.broadcast = broadcast;
|
||||
}
|
||||
|
||||
const toArrayBuffer = (buffer: Buffer) => {
|
||||
const { buffer: arrayBuffer, byteOffset, byteLength } = buffer;
|
||||
return arrayBuffer.slice(byteOffset, byteOffset + byteLength);
|
||||
};
|
||||
|
||||
// imported from https://github.com/kolodziejczak-sz/uwebsocket-serve
|
||||
export function serveFile(res /* : HttpResponse */, filepath: string) {
|
||||
const { size } = statSync(filepath);
|
||||
const readStream = createReadStream(filepath);
|
||||
const destroyReadStream = () => !readStream.destroyed && readStream.destroy();
|
||||
|
||||
const onError = (error: Error) => {
|
||||
destroyReadStream();
|
||||
throw error;
|
||||
};
|
||||
|
||||
const onDataChunk = (chunk: Buffer) => {
|
||||
const arrayBufferChunk = toArrayBuffer(chunk);
|
||||
|
||||
const lastOffset = res.getWriteOffset();
|
||||
const [ok, done] = res.tryEnd(arrayBufferChunk, size);
|
||||
|
||||
if (!done && !ok) {
|
||||
readStream.pause();
|
||||
|
||||
res.onWritable((offset) => {
|
||||
const [ok, done] = res.tryEnd(
|
||||
arrayBufferChunk.slice(offset - lastOffset),
|
||||
size
|
||||
);
|
||||
|
||||
if (!done && ok) {
|
||||
readStream.resume();
|
||||
}
|
||||
|
||||
return ok;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
res.onAborted(destroyReadStream);
|
||||
readStream
|
||||
.on("data", onDataChunk)
|
||||
.on("error", onError)
|
||||
.on("end", destroyReadStream);
|
||||
}
|
||||
8211
package-lock.json
generated
8211
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
40
package.json
40
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "socket.io",
|
||||
"version": "4.0.2",
|
||||
"version": "4.6.0",
|
||||
"description": "node.js realtime framework server",
|
||||
"keywords": [
|
||||
"realtime",
|
||||
@@ -27,7 +27,8 @@
|
||||
"main": "./dist/index.js",
|
||||
"exports": {
|
||||
"import": "./wrapper.mjs",
|
||||
"require": "./dist/index.js"
|
||||
"require": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"license": "MIT",
|
||||
@@ -39,39 +40,34 @@
|
||||
"compile": "rimraf ./dist && tsc",
|
||||
"test": "npm run format:check && npm run compile && npm run test:types && npm run test:unit",
|
||||
"test:types": "tsd",
|
||||
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/socket.io.ts",
|
||||
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/index.ts",
|
||||
"format:check": "prettier --check \"lib/**/*.ts\" \"test/**/*.ts\"",
|
||||
"format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\"",
|
||||
"prepack": "npm run compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.0",
|
||||
"@types/cors": "^2.8.8",
|
||||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io": "~5.0.0",
|
||||
"socket.io-adapter": "~2.2.0",
|
||||
"socket.io-parser": "~4.0.3"
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.4.0",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mocha": "^8.0.4",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.14.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"expect.js": "0.3.1",
|
||||
"mocha": "^3.5.3",
|
||||
"mocha": "^10.0.0",
|
||||
"nyc": "^15.1.0",
|
||||
"prettier": "^2.2.0",
|
||||
"prettier": "^2.3.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"socket.io-client": "4.0.2",
|
||||
"socket.io-client": "4.6.0",
|
||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||
"superagent": "^6.1.0",
|
||||
"supertest": "^6.0.1",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsd": "^0.14.0",
|
||||
"typescript": "^4.1.2"
|
||||
"superagent": "^8.0.0",
|
||||
"supertest": "^6.1.6",
|
||||
"ts-node": "^10.2.1",
|
||||
"tsd": "^0.21.0",
|
||||
"typescript": "^4.4.2",
|
||||
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.0.0"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
|
||||
148
test/close.ts
Normal file
148
test/close.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { createServer } from "http";
|
||||
import { io as ioc } from "socket.io-client";
|
||||
import { join } from "path";
|
||||
import { exec } from "child_process";
|
||||
import { Server } from "..";
|
||||
import expect from "expect.js";
|
||||
import {
|
||||
createClient,
|
||||
eioHandshake,
|
||||
eioPoll,
|
||||
eioPush,
|
||||
getPort,
|
||||
} from "./support/util";
|
||||
|
||||
describe("close", () => {
|
||||
it("should be able to close sio sending a srv", (done) => {
|
||||
const httpServer = createServer().listen(0);
|
||||
const io = new Server(httpServer);
|
||||
const port = getPort(io);
|
||||
const net = require("net");
|
||||
const server = net.createServer();
|
||||
|
||||
const clientSocket = createClient(io, "/", { reconnection: false });
|
||||
|
||||
clientSocket.on("disconnect", () => {
|
||||
expect(io.sockets.sockets.size).to.equal(0);
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
clientSocket.on("connect", () => {
|
||||
expect(io.sockets.sockets.size).to.equal(1);
|
||||
io.close();
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
// PORT should be free
|
||||
server.close((error) => {
|
||||
expect(error).to.be(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to close sio sending a srv", (done) => {
|
||||
const io = new Server(0);
|
||||
const port = getPort(io);
|
||||
const net = require("net");
|
||||
const server = net.createServer();
|
||||
|
||||
const clientSocket = ioc("ws://0.0.0.0:" + port, {
|
||||
reconnection: false,
|
||||
});
|
||||
|
||||
clientSocket.on("disconnect", () => {
|
||||
expect(io.sockets.sockets.size).to.equal(0);
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
clientSocket.on("connect", () => {
|
||||
expect(io.sockets.sockets.size).to.equal(1);
|
||||
io.close();
|
||||
});
|
||||
|
||||
server.once("listening", () => {
|
||||
// PORT should be free
|
||||
server.close((error) => {
|
||||
expect(error).to.be(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("graceful close", () => {
|
||||
function fixture(filename) {
|
||||
return (
|
||||
'"' +
|
||||
process.execPath +
|
||||
'" "' +
|
||||
join(__dirname, "fixtures", filename) +
|
||||
'"'
|
||||
);
|
||||
}
|
||||
|
||||
it("should stop socket and timers", (done) => {
|
||||
exec(fixture("server-close.ts"), done);
|
||||
});
|
||||
});
|
||||
|
||||
describe("protocol violations", () => {
|
||||
it("should close the connection when receiving several CONNECT packets", async () => {
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
|
||||
httpServer.listen(0);
|
||||
|
||||
const sid = await eioHandshake(httpServer);
|
||||
// send a first CONNECT packet
|
||||
await eioPush(httpServer, sid, "40");
|
||||
// send another CONNECT packet
|
||||
await eioPush(httpServer, sid, "40");
|
||||
// session is cleanly closed (not discarded, see 'client.close()')
|
||||
// first, we receive the Socket.IO handshake response
|
||||
await eioPoll(httpServer, sid);
|
||||
// then a close packet
|
||||
const body = await eioPoll(httpServer, sid);
|
||||
expect(body).to.be("6\u001e1");
|
||||
|
||||
io.close();
|
||||
});
|
||||
|
||||
it("should close the connection when receiving an EVENT packet while not connected", async () => {
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
|
||||
httpServer.listen(0);
|
||||
|
||||
const sid = await eioHandshake(httpServer);
|
||||
// send an EVENT packet
|
||||
await eioPush(httpServer, sid, '42["some event"]');
|
||||
// session is cleanly closed, we receive a close packet
|
||||
const body = await eioPoll(httpServer, sid);
|
||||
expect(body).to.be("6\u001e1");
|
||||
|
||||
io.close();
|
||||
});
|
||||
|
||||
it("should close the connection when receiving an invalid packet", async () => {
|
||||
const httpServer = createServer();
|
||||
const io = new Server(httpServer);
|
||||
|
||||
httpServer.listen(0);
|
||||
|
||||
const sid = await eioHandshake(httpServer);
|
||||
// send a CONNECT packet
|
||||
await eioPush(httpServer, sid, "40");
|
||||
// send an invalid packet
|
||||
await eioPush(httpServer, sid, "4abc");
|
||||
// session is cleanly closed (not discarded, see 'client.close()')
|
||||
// first, we receive the Socket.IO handshake response
|
||||
await eioPoll(httpServer, sid);
|
||||
// then a close packet
|
||||
const body = await eioPoll(httpServer, sid);
|
||||
expect(body).to.be("6\u001e1");
|
||||
|
||||
io.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
247
test/connection-state-recovery.ts
Normal file
247
test/connection-state-recovery.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
2
test/fixtures/server-close.ts
vendored
2
test/fixtures/server-close.ts
vendored
@@ -3,7 +3,7 @@ const ioc = require("socket.io-client");
|
||||
const io = require("../..")(server);
|
||||
|
||||
const srv = server.listen(() => {
|
||||
const socket = ioc("ws://localhost:" + server.address().port);
|
||||
const socket = ioc.connect("ws://localhost:" + server.address().port);
|
||||
socket.on("connect", () => {
|
||||
io.close();
|
||||
socket.close();
|
||||
|
||||
87
test/handshake.ts
Normal file
87
test/handshake.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Server } from "..";
|
||||
import expect from "expect.js";
|
||||
import { getPort, success } from "./support/util";
|
||||
|
||||
describe("handshake", () => {
|
||||
const request = require("superagent");
|
||||
|
||||
it("should send the Access-Control-Allow-xxx headers on OPTIONS request", (done) => {
|
||||
const io = new Server(0, {
|
||||
cors: {
|
||||
origin: "http://localhost:54023",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["content-type"],
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
request
|
||||
.options(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.set("Origin", "http://localhost:54023")
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.be(204);
|
||||
|
||||
expect(res.headers["access-control-allow-origin"]).to.be(
|
||||
"http://localhost:54023"
|
||||
);
|
||||
expect(res.headers["access-control-allow-methods"]).to.be("GET,POST");
|
||||
expect(res.headers["access-control-allow-headers"]).to.be(
|
||||
"content-type"
|
||||
);
|
||||
expect(res.headers["access-control-allow-credentials"]).to.be("true");
|
||||
success(done, io);
|
||||
});
|
||||
});
|
||||
|
||||
it("should send the Access-Control-Allow-xxx headers on GET request", (done) => {
|
||||
const io = new Server(0, {
|
||||
cors: {
|
||||
origin: "http://localhost:54024",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["content-type"],
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
request
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.set("Origin", "http://localhost:54024")
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.be(200);
|
||||
|
||||
expect(res.headers["access-control-allow-origin"]).to.be(
|
||||
"http://localhost:54024"
|
||||
);
|
||||
expect(res.headers["access-control-allow-credentials"]).to.be("true");
|
||||
success(done, io);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow request if custom function in opts.allowRequest returns true", (done) => {
|
||||
const io = new Server(0, {
|
||||
allowRequest: (req, callback) => callback(null, true),
|
||||
});
|
||||
|
||||
request
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.be(200);
|
||||
success(done, io);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disallow request if custom function in opts.allowRequest returns false", (done) => {
|
||||
const io = new Server(0, {
|
||||
allowRequest: (req, callback) => callback(null, false),
|
||||
});
|
||||
request
|
||||
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
|
||||
.set("origin", "http://foo.example")
|
||||
.query({ transport: "polling", EIO: 4 })
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.be(403);
|
||||
success(done, io);
|
||||
});
|
||||
});
|
||||
});
|
||||
24
test/index.ts
Normal file
24
test/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
"use strict";
|
||||
|
||||
import expect from "expect.js";
|
||||
|
||||
describe("socket.io", () => {
|
||||
it("should be the same version as client", () => {
|
||||
const version = require("../package").version;
|
||||
expect(version).to.be(require("socket.io-client/package.json").version);
|
||||
});
|
||||
|
||||
require("./server-attachment");
|
||||
require("./handshake");
|
||||
require("./close");
|
||||
require("./namespaces");
|
||||
require("./socket");
|
||||
require("./messaging-many");
|
||||
require("./middleware");
|
||||
require("./socket-middleware");
|
||||
require("./v2-compatibility");
|
||||
require("./socket-timeout");
|
||||
require("./uws");
|
||||
require("./utility-methods");
|
||||
require("./connection-state-recovery");
|
||||
});
|
||||
587
test/messaging-many.ts
Normal file
587
test/messaging-many.ts
Normal file
@@ -0,0 +1,587 @@
|
||||
import { Server } from "..";
|
||||
import expect from "expect.js";
|
||||
import {
|
||||
createClient,
|
||||
createPartialDone,
|
||||
success,
|
||||
successFn,
|
||||
waitFor,
|
||||
} from "./support/util";
|
||||
|
||||
describe("messaging many", () => {
|
||||
it("emits to a namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/test");
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2, socket3)
|
||||
);
|
||||
|
||||
socket1.on("a", (a) => {
|
||||
expect(a).to.be("b");
|
||||
partialDone();
|
||||
});
|
||||
socket2.on("a", (a) => {
|
||||
expect(a).to.be("b");
|
||||
partialDone();
|
||||
});
|
||||
socket3.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
|
||||
let sockets = 3;
|
||||
io.on("connection", () => {
|
||||
--sockets || emit();
|
||||
});
|
||||
io.of("/test", () => {
|
||||
--sockets || emit();
|
||||
});
|
||||
|
||||
function emit() {
|
||||
io.emit("a", "b");
|
||||
}
|
||||
});
|
||||
|
||||
it("emits binary data to a namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/test");
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2, socket3)
|
||||
);
|
||||
|
||||
socket1.on("bin", (a) => {
|
||||
expect(Buffer.isBuffer(a)).to.be(true);
|
||||
partialDone();
|
||||
});
|
||||
socket2.on("bin", (a) => {
|
||||
expect(Buffer.isBuffer(a)).to.be(true);
|
||||
partialDone();
|
||||
});
|
||||
socket3.on("bin", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
|
||||
let sockets = 3;
|
||||
io.on("connection", () => {
|
||||
--sockets || emit();
|
||||
});
|
||||
io.of("/test", () => {
|
||||
--sockets || emit();
|
||||
});
|
||||
|
||||
function emit() {
|
||||
io.emit("bin", Buffer.alloc(10));
|
||||
}
|
||||
});
|
||||
|
||||
it("emits to the rest", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/test");
|
||||
|
||||
socket1.on("a", (a) => {
|
||||
expect(a).to.be("b");
|
||||
socket1.emit("finish");
|
||||
});
|
||||
socket2.emit("broadcast");
|
||||
socket2.on("a", () => {
|
||||
done(new Error("done"));
|
||||
});
|
||||
socket3.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("broadcast", () => {
|
||||
socket.broadcast.emit("a", "b");
|
||||
});
|
||||
socket.on("finish", () => {
|
||||
success(done, io, socket1, socket2, socket3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("emits to rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket1.on("a", () => {
|
||||
success(done, io, socket1, socket2);
|
||||
});
|
||||
socket1.emit("join", "woot");
|
||||
socket1.emit("emit", "woot");
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("join", (room, fn) => {
|
||||
socket.join(room);
|
||||
fn && fn();
|
||||
});
|
||||
|
||||
socket.on("emit", (room) => {
|
||||
io.in(room).emit("a");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("emits to rooms avoiding dupes", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2)
|
||||
);
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket1.on("a", partialDone);
|
||||
socket2.on("b", partialDone);
|
||||
|
||||
socket1.emit("join", "woot");
|
||||
socket1.emit("join", "test");
|
||||
socket2.emit("join", "third", () => {
|
||||
socket2.emit("emit");
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("join", (room, fn) => {
|
||||
socket.join(room);
|
||||
fn && fn();
|
||||
});
|
||||
|
||||
socket.on("emit", () => {
|
||||
io.in("woot").in("test").emit("a");
|
||||
io.in("third").emit("b");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts to rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2, socket3)
|
||||
);
|
||||
|
||||
socket1.emit("join", "woot");
|
||||
socket2.emit("join", "test");
|
||||
socket3.emit("join", "test", () => {
|
||||
socket3.emit("broadcast");
|
||||
});
|
||||
|
||||
socket1.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket2.on("a", () => {
|
||||
partialDone();
|
||||
});
|
||||
socket3.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket3.on("b", () => {
|
||||
partialDone();
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("join", (room, fn) => {
|
||||
socket.join(room);
|
||||
fn && fn();
|
||||
});
|
||||
|
||||
socket.on("broadcast", () => {
|
||||
socket.broadcast.to("test").emit("a");
|
||||
socket.emit("b");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("broadcasts binary data to rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2, socket3)
|
||||
);
|
||||
|
||||
socket1.emit("join", "woot");
|
||||
socket2.emit("join", "test");
|
||||
socket3.emit("join", "test", () => {
|
||||
socket3.emit("broadcast");
|
||||
});
|
||||
|
||||
socket1.on("bin", (data) => {
|
||||
throw new Error("got bin in socket1");
|
||||
});
|
||||
socket2.on("bin", (data) => {
|
||||
expect(Buffer.isBuffer(data)).to.be(true);
|
||||
partialDone();
|
||||
});
|
||||
socket2.on("bin2", (data) => {
|
||||
throw new Error("socket2 got bin2");
|
||||
});
|
||||
socket3.on("bin", (data) => {
|
||||
throw new Error("socket3 got bin");
|
||||
});
|
||||
socket3.on("bin2", (data) => {
|
||||
expect(Buffer.isBuffer(data)).to.be(true);
|
||||
partialDone();
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("join", (room, fn) => {
|
||||
socket.join(room);
|
||||
fn && fn();
|
||||
});
|
||||
socket.on("broadcast", () => {
|
||||
socket.broadcast.to("test").emit("bin", Buffer.alloc(5));
|
||||
socket.emit("bin2", Buffer.alloc(5));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps track of rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
|
||||
io.on("connection", (s) => {
|
||||
s.join("a");
|
||||
expect(s.rooms).to.contain(s.id, "a");
|
||||
s.join("b");
|
||||
expect(s.rooms).to.contain(s.id, "a", "b");
|
||||
s.join("c");
|
||||
expect(s.rooms).to.contain(s.id, "a", "b", "c");
|
||||
s.leave("b");
|
||||
expect(s.rooms).to.contain(s.id, "a", "c");
|
||||
(s as any).leaveAll();
|
||||
expect(s.rooms.size).to.eql(0);
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("deletes empty rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
|
||||
io.on("connection", (s) => {
|
||||
s.join("a");
|
||||
expect(s.nsp.adapter.rooms).to.contain("a");
|
||||
s.leave("a");
|
||||
expect(s.nsp.adapter.rooms).to.not.contain("a");
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should properly cleanup left rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
|
||||
io.on("connection", (s) => {
|
||||
s.join("a");
|
||||
expect(s.rooms).to.contain(s.id, "a");
|
||||
s.join("b");
|
||||
expect(s.rooms).to.contain(s.id, "a", "b");
|
||||
s.leave("unknown");
|
||||
expect(s.rooms).to.contain(s.id, "a", "b");
|
||||
(s as any).leaveAll();
|
||||
expect(s.rooms.size).to.eql(0);
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("allows to join several rooms at once", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
|
||||
io.on("connection", (s) => {
|
||||
s.join(["a", "b", "c"]);
|
||||
expect(s.rooms).to.contain(s.id, "a", "b", "c");
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should exclude specific sockets when broadcasting", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket3.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket1.on("a", successFn(done, io, socket1, socket2, socket3));
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("exclude", (id) => {
|
||||
socket.broadcast.except(id).emit("a");
|
||||
});
|
||||
});
|
||||
|
||||
socket2.on("connect", () => {
|
||||
socket3.emit("exclude", socket2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should exclude a specific room when broadcasting", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/", { multiplex: false });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket3.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket1.on("a", successFn(done, io, socket1, socket2, socket3));
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("join", (room, cb) => {
|
||||
socket.join(room);
|
||||
cb();
|
||||
});
|
||||
socket.on("broadcast", () => {
|
||||
socket.broadcast.except("room1").emit("a");
|
||||
});
|
||||
});
|
||||
|
||||
socket2.emit("join", "room1", () => {
|
||||
socket3.emit("broadcast");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return an immutable broadcast operator", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
const operator = socket.local
|
||||
.compress(false)
|
||||
.to(["room1", "room2"])
|
||||
.except("room3");
|
||||
operator.compress(true).emit("hello");
|
||||
operator.volatile.emit("hello");
|
||||
operator.to("room4").emit("hello");
|
||||
operator.except("room5").emit("hello");
|
||||
socket.emit("hello");
|
||||
socket.to("room6").emit("hello");
|
||||
// @ts-ignore
|
||||
expect(operator.rooms).to.contain("room1", "room2");
|
||||
// @ts-ignore
|
||||
expect(operator.rooms).to.not.contain("room4", "room5", "room6");
|
||||
// @ts-ignore
|
||||
expect(operator.exceptRooms).to.contain("room3");
|
||||
// @ts-ignore
|
||||
expect(operator.flags).to.eql({ local: true, compress: false });
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should broadcast and expect multiple acknowledgements", (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(() => {
|
||||
io.timeout(2000).emit("some event", (err, responses) => {
|
||||
expect(err).to.be(null);
|
||||
expect(responses).to.have.length(3);
|
||||
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", (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(() => {
|
||||
io.timeout(200).emit("some event", (err, responses) => {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(responses).to.have.length(2);
|
||||
expect(responses).to.contain(1, 2);
|
||||
|
||||
success(done, io, socket1, socket2, socket3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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 });
|
||||
const socket2 = createClient(io, "/", { multiplex: false });
|
||||
const socket3 = createClient(io, "/", { multiplex: false });
|
||||
|
||||
socket1.on("some event", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
|
||||
socket2.on("some event", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
|
||||
socket3.on("some event", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
|
||||
io.to("room123")
|
||||
.timeout(200)
|
||||
.emit("some event", (err, responses) => {
|
||||
expect(err).to.be(null);
|
||||
expect(responses).to.have.length(0);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
210
test/middleware.ts
Normal file
210
test/middleware.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import { Server, Socket } from "..";
|
||||
import expect from "expect.js";
|
||||
import {
|
||||
success,
|
||||
createClient,
|
||||
successFn,
|
||||
createPartialDone,
|
||||
} from "./support/util";
|
||||
|
||||
describe("middleware", () => {
|
||||
it("should call functions", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
let run = 0;
|
||||
io.use((socket, next) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
run++;
|
||||
next();
|
||||
});
|
||||
io.use((socket, next) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
run++;
|
||||
next();
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", () => {
|
||||
expect(run).to.be(2);
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass errors", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
io.use((socket, next) => {
|
||||
next(new Error("Authentication error"));
|
||||
});
|
||||
io.use((socket, next) => {
|
||||
done(new Error("nope"));
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", () => {
|
||||
done(new Error("nope"));
|
||||
});
|
||||
socket.on("connect_error", (err) => {
|
||||
expect(err.message).to.be("Authentication error");
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass an object", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
io.use((socket, next) => {
|
||||
const err = new Error("Authentication error");
|
||||
// @ts-ignore
|
||||
err.data = { a: "b", c: 3 };
|
||||
next(err);
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", () => {
|
||||
done(new Error("nope"));
|
||||
});
|
||||
socket.on("connect_error", (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(err.message).to.eql("Authentication error");
|
||||
// @ts-ignore
|
||||
expect(err.data).to.eql({ a: "b", c: 3 });
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should only call connection after fns", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
io.use((socket: any, next) => {
|
||||
socket.name = "guillermo";
|
||||
next();
|
||||
});
|
||||
|
||||
const clientSocket = createClient(io);
|
||||
io.on("connection", (socket) => {
|
||||
expect((socket as any).name).to.be("guillermo");
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should only call connection after (lengthy) fns", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
let authenticated = false;
|
||||
|
||||
io.use((socket, next) => {
|
||||
setTimeout(() => {
|
||||
authenticated = true;
|
||||
next();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", () => {
|
||||
expect(authenticated).to.be(true);
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be ignored if socket gets closed", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
let socket;
|
||||
io.use((s, next) => {
|
||||
socket.io.engine.close();
|
||||
s.client.conn.on("close", () => {
|
||||
process.nextTick(next);
|
||||
setTimeout(() => {
|
||||
success(done, io, socket);
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
|
||||
socket = createClient(io);
|
||||
io.on("connection", (socket) => {
|
||||
done(new Error("should not fire"));
|
||||
});
|
||||
});
|
||||
|
||||
it("should call functions in expected order", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
const result: number[] = [];
|
||||
|
||||
io.use(() => {
|
||||
done(new Error("should not fire"));
|
||||
});
|
||||
io.of("/chat").use((socket, next) => {
|
||||
result.push(1);
|
||||
setTimeout(next, 50);
|
||||
});
|
||||
io.of("/chat").use((socket, next) => {
|
||||
result.push(2);
|
||||
setTimeout(next, 50);
|
||||
});
|
||||
io.of("/chat").use((socket, next) => {
|
||||
result.push(3);
|
||||
setTimeout(next, 50);
|
||||
});
|
||||
|
||||
const chat = createClient(io, "/chat");
|
||||
chat.on("connect", () => {
|
||||
expect(result).to.eql([1, 2, 3]);
|
||||
|
||||
success(done, io, chat);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable the merge of handshake packets", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
io.use((socket, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", successFn(done, io, socket));
|
||||
});
|
||||
|
||||
it("should work with a custom namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket1 = createClient(io, "/");
|
||||
const socket2 = createClient(io, "/chat");
|
||||
|
||||
const partialDone = createPartialDone(
|
||||
2,
|
||||
successFn(done, io, socket1, socket2)
|
||||
);
|
||||
|
||||
io.of("/chat").use((socket, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
socket1.on("connect", partialDone);
|
||||
socket2.on("connect", partialDone);
|
||||
});
|
||||
|
||||
it("should only set `connected` to true after the middleware execution", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io, "/");
|
||||
|
||||
io.use((socket, next) => {
|
||||
expect(socket.connected).to.be(false);
|
||||
expect(socket.disconnected).to.be(true);
|
||||
next();
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
expect(socket.connected).to.be(true);
|
||||
expect(socket.disconnected).to.be(false);
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
});
|
||||
656
test/namespaces.ts
Normal file
656
test/namespaces.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
import type { SocketId } from "socket.io-adapter";
|
||||
import { Server, Namespace, Socket } from "..";
|
||||
import expect from "expect.js";
|
||||
import {
|
||||
success,
|
||||
createClient,
|
||||
successFn,
|
||||
createPartialDone,
|
||||
} from "./support/util";
|
||||
|
||||
describe("namespaces", () => {
|
||||
it("should be accessible through .sockets", () => {
|
||||
const io = new Server();
|
||||
expect(io.sockets).to.be.a(Namespace);
|
||||
});
|
||||
|
||||
it("should be aliased", () => {
|
||||
const io = new Server();
|
||||
expect(io.use).to.be.a("function");
|
||||
expect(io.to).to.be.a("function");
|
||||
expect(io["in"]).to.be.a("function");
|
||||
expect(io.emit).to.be.a("function");
|
||||
expect(io.send).to.be.a("function");
|
||||
expect(io.write).to.be.a("function");
|
||||
expect(io.allSockets).to.be.a("function");
|
||||
expect(io.compress).to.be.a("function");
|
||||
});
|
||||
|
||||
it("should return an immutable broadcast operator", () => {
|
||||
const io = new Server();
|
||||
const operator = io.local.to(["room1", "room2"]).except("room3");
|
||||
operator.compress(true).emit("hello");
|
||||
operator.volatile.emit("hello");
|
||||
operator.to("room4").emit("hello");
|
||||
operator.except("room5").emit("hello");
|
||||
io.to("room6").emit("hello");
|
||||
// @ts-ignore
|
||||
expect(operator.rooms).to.contain("room1", "room2");
|
||||
// @ts-ignore
|
||||
expect(operator.exceptRooms).to.contain("room3");
|
||||
// @ts-ignore
|
||||
expect(operator.flags).to.eql({ local: true });
|
||||
});
|
||||
|
||||
it("should automatically connect", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
socket.on("connect", successFn(done, io, socket));
|
||||
});
|
||||
|
||||
it("should fire a `connection` event", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io);
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fire a `connect` event", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io);
|
||||
|
||||
io.on("connect", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with many sockets", (done) => {
|
||||
const io = new Server(0);
|
||||
io.of("/chat");
|
||||
io.of("/news");
|
||||
const chat = createClient(io, "/chat");
|
||||
const news = createClient(io, "/news");
|
||||
|
||||
let total = 2;
|
||||
chat.on("connect", () => {
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
news.on("connect", () => {
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to equivalently start with "" or "/" on server', (done) => {
|
||||
const io = new Server(0);
|
||||
const c1 = createClient(io, "/");
|
||||
const c2 = createClient(io, "/abc");
|
||||
|
||||
let total = 2;
|
||||
io.of("").on("connection", () => {
|
||||
--total || success(done, io, c1, c2);
|
||||
});
|
||||
io.of("abc").on("connection", () => {
|
||||
--total || success(done, io, c1, c2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be equivalent for "" and "/" on client', (done) => {
|
||||
const io = new Server(0);
|
||||
const c1 = createClient(io, "");
|
||||
|
||||
io.of("/").on("connection", successFn(done, io, c1));
|
||||
});
|
||||
|
||||
it("should work with `of` and many sockets", (done) => {
|
||||
const io = new Server(0);
|
||||
const chat = createClient(io, "/chat");
|
||||
const news = createClient(io, "/news");
|
||||
|
||||
let total = 2;
|
||||
io.of("/news").on("connection", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
io.of("/news").on("connection", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with `of` second param", (done) => {
|
||||
const io = new Server(0);
|
||||
const chat = createClient(io, "/chat");
|
||||
const news = createClient(io, "/news");
|
||||
|
||||
let total = 2;
|
||||
io.of("/news", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
io.of("/news", (socket) => {
|
||||
expect(socket).to.be.a(Socket);
|
||||
--total || success(done, io, chat, news);
|
||||
});
|
||||
});
|
||||
|
||||
it("should disconnect upon transport disconnection", (done) => {
|
||||
const io = new Server(0);
|
||||
const chat = createClient(io, "/chat");
|
||||
const news = createClient(io, "/news");
|
||||
|
||||
let total = 2;
|
||||
let totald = 2;
|
||||
let s;
|
||||
io.of("/news", (socket) => {
|
||||
socket.on("disconnect", (reason) => {
|
||||
--totald || success(done, io, chat, news);
|
||||
});
|
||||
--total || close();
|
||||
});
|
||||
io.of("/chat", (socket) => {
|
||||
s = socket;
|
||||
socket.on("disconnect", (reason) => {
|
||||
--totald || success(done, io, chat, news);
|
||||
});
|
||||
--total || close();
|
||||
});
|
||||
function close() {
|
||||
s.disconnect(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should fire a `disconnecting` event just before leaving all rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io);
|
||||
|
||||
io.on("connection", (s) => {
|
||||
s.join("a");
|
||||
// FIXME not sure why process.nextTick() is needed here
|
||||
process.nextTick(() => s.disconnect());
|
||||
|
||||
let total = 2;
|
||||
s.on("disconnecting", (reason) => {
|
||||
expect(s.rooms).to.contain(s.id, "a");
|
||||
total--;
|
||||
});
|
||||
|
||||
s.on("disconnect", (reason) => {
|
||||
expect(s.rooms.size).to.eql(0);
|
||||
--total || success(done, io, socket);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should return error connecting to non-existent namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io, "/doesnotexist");
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
expect(err.message).to.be("Invalid namespace");
|
||||
success(done, io);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not reuse same-namespace connections", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket1 = createClient(io);
|
||||
const clientSocket2 = createClient(io);
|
||||
|
||||
let connections = 0;
|
||||
io.on("connection", () => {
|
||||
connections++;
|
||||
if (connections === 2) {
|
||||
success(done, io, clientSocket1, clientSocket2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should find all clients in a namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
const chatSids: string[] = [];
|
||||
let otherSid: SocketId | null = null;
|
||||
|
||||
const c1 = createClient(io, "/chat");
|
||||
const c2 = createClient(io, "/chat", { forceNew: true });
|
||||
const c3 = createClient(io, "/other", { forceNew: true });
|
||||
|
||||
let total = 3;
|
||||
io.of("/chat").on("connection", (socket) => {
|
||||
chatSids.push(socket.id);
|
||||
--total || getSockets();
|
||||
});
|
||||
io.of("/other").on("connection", (socket) => {
|
||||
otherSid = socket.id;
|
||||
--total || getSockets();
|
||||
});
|
||||
|
||||
async function getSockets() {
|
||||
const sids = await io.of("/chat").allSockets();
|
||||
|
||||
expect(sids).to.contain(chatSids[0], chatSids[1]);
|
||||
expect(sids).to.not.contain(otherSid);
|
||||
success(done, io, c1, c2, c3);
|
||||
}
|
||||
});
|
||||
|
||||
it("should find all clients in a namespace room", (done) => {
|
||||
const io = new Server(0);
|
||||
let chatFooSid: SocketId | null = null;
|
||||
let chatBarSid: SocketId | null = null;
|
||||
let otherSid: SocketId | null = null;
|
||||
|
||||
const c1 = createClient(io, "/chat");
|
||||
const c2 = createClient(io, "/chat", { forceNew: true });
|
||||
const c3 = createClient(io, "/other", { forceNew: true });
|
||||
|
||||
let chatIndex = 0;
|
||||
let total = 3;
|
||||
io.of("/chat").on("connection", (socket) => {
|
||||
if (chatIndex++) {
|
||||
socket.join("foo");
|
||||
chatFooSid = socket.id;
|
||||
--total || getSockets();
|
||||
} else {
|
||||
socket.join("bar");
|
||||
chatBarSid = socket.id;
|
||||
--total || getSockets();
|
||||
}
|
||||
});
|
||||
io.of("/other").on("connection", (socket) => {
|
||||
socket.join("foo");
|
||||
otherSid = socket.id;
|
||||
--total || getSockets();
|
||||
});
|
||||
|
||||
async function getSockets() {
|
||||
const sids = await io.of("/chat").in("foo").allSockets();
|
||||
|
||||
expect(sids).to.contain(chatFooSid);
|
||||
expect(sids).to.not.contain(chatBarSid);
|
||||
expect(sids).to.not.contain(otherSid);
|
||||
success(done, io, c1, c2, c3);
|
||||
}
|
||||
});
|
||||
|
||||
it("should find all clients across namespace rooms", (done) => {
|
||||
const io = new Server(0);
|
||||
let chatFooSid: SocketId | null = null;
|
||||
let chatBarSid: SocketId | null = null;
|
||||
let otherSid: SocketId | null = null;
|
||||
|
||||
const c1 = createClient(io, "/chat");
|
||||
const c2 = createClient(io, "/chat", { forceNew: true });
|
||||
const c3 = createClient(io, "/other", { forceNew: true });
|
||||
|
||||
let chatIndex = 0;
|
||||
let total = 3;
|
||||
io.of("/chat").on("connection", (socket) => {
|
||||
if (chatIndex++) {
|
||||
socket.join("foo");
|
||||
chatFooSid = socket.id;
|
||||
--total || getSockets();
|
||||
} else {
|
||||
socket.join("bar");
|
||||
chatBarSid = socket.id;
|
||||
--total || getSockets();
|
||||
}
|
||||
});
|
||||
io.of("/other").on("connection", (socket) => {
|
||||
socket.join("foo");
|
||||
otherSid = socket.id;
|
||||
--total || getSockets();
|
||||
});
|
||||
|
||||
async function getSockets() {
|
||||
const sids = await io.of("/chat").allSockets();
|
||||
expect(sids).to.contain(chatFooSid, chatBarSid);
|
||||
expect(sids).to.not.contain(otherSid);
|
||||
success(done, io, c1, c2, c3);
|
||||
}
|
||||
});
|
||||
|
||||
it("should not emit volatile event after regular event", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
let counter = 0;
|
||||
io.of("/chat").on("connection", (s) => {
|
||||
// Wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
io.of("/chat").emit("ev", "data");
|
||||
io.of("/chat").volatile.emit("ev", "data");
|
||||
}, 50);
|
||||
});
|
||||
|
||||
const socket = createClient(io, "/chat");
|
||||
socket.on("ev", () => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(counter).to.be(1);
|
||||
success(done, io, socket);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it("should emit volatile event", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
let counter = 0;
|
||||
io.of("/chat").on("connection", (s) => {
|
||||
// Wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
io.of("/chat").volatile.emit("ev", "data");
|
||||
}, 100);
|
||||
});
|
||||
|
||||
const socket = createClient(io, "/chat");
|
||||
socket.on("ev", () => {
|
||||
counter++;
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(counter).to.be(1);
|
||||
success(done, io, socket);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
it("should enable compression by default", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io, "/chat");
|
||||
|
||||
io.of("/chat").on("connection", (s) => {
|
||||
s.conn.once("packetCreate", (packet) => {
|
||||
expect(packet.options.compress).to.be(true);
|
||||
success(done, io, socket);
|
||||
});
|
||||
io.of("/chat").emit("woot", "hi");
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable compression", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io, "/chat");
|
||||
|
||||
io.of("/chat").on("connection", (s) => {
|
||||
s.conn.once("packetCreate", (packet) => {
|
||||
expect(packet.options.compress).to.be(false);
|
||||
success(done, io, socket);
|
||||
});
|
||||
io.of("/chat").compress(false).emit("woot", "hi");
|
||||
});
|
||||
});
|
||||
|
||||
it("should throw on reserved event", () => {
|
||||
const io = new Server();
|
||||
|
||||
expect(() => io.emit("connect")).to.throwException(
|
||||
/"connect" is a reserved event name/
|
||||
);
|
||||
});
|
||||
|
||||
it("should close a client without namespace", (done) => {
|
||||
const io = new Server(0, {
|
||||
connectTimeout: 10,
|
||||
});
|
||||
|
||||
const socket = createClient(io);
|
||||
|
||||
// @ts-ignore
|
||||
socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
|
||||
|
||||
socket.on("disconnect", successFn(done, io, socket));
|
||||
});
|
||||
|
||||
it("should exclude a specific socket when emitting", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
const socket1 = createClient(io, "/");
|
||||
const socket2 = createClient(io, "/");
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
socket1.on("a", successFn(done, io, socket1, socket2));
|
||||
|
||||
socket2.on("connect", () => {
|
||||
io.except(socket2.id).emit("a");
|
||||
});
|
||||
});
|
||||
|
||||
it("should exclude a specific socket when emitting (in a namespace)", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
const nsp = io.of("/nsp");
|
||||
|
||||
const socket1 = createClient(io, "/nsp");
|
||||
const socket2 = createClient(io, "/nsp");
|
||||
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
socket1.on("a", successFn(done, io, socket1, socket2));
|
||||
|
||||
socket2.on("connect", () => {
|
||||
nsp.except(socket2.id).emit("a");
|
||||
});
|
||||
});
|
||||
|
||||
it("should exclude a specific room when emitting", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
const nsp = io.of("/nsp");
|
||||
|
||||
const socket1 = createClient(io, "/nsp");
|
||||
const socket2 = createClient(io, "/nsp");
|
||||
|
||||
socket1.on("a", successFn(done, io, socket1, socket2));
|
||||
socket2.on("a", () => {
|
||||
done(new Error("not"));
|
||||
});
|
||||
|
||||
nsp.on("connection", (socket) => {
|
||||
socket.on("broadcast", () => {
|
||||
socket.join("room1");
|
||||
nsp.except("room1").emit("a");
|
||||
});
|
||||
});
|
||||
|
||||
socket2.emit("broadcast");
|
||||
});
|
||||
|
||||
it("should emit an 'new_namespace' event", (done) => {
|
||||
const io = new Server();
|
||||
|
||||
io.on("new_namespace", (namespace) => {
|
||||
expect(namespace.name).to.eql("/nsp");
|
||||
done();
|
||||
});
|
||||
|
||||
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);
|
||||
const socket = createClient(io, "/dynamic-101");
|
||||
const partialDone = createPartialDone(4, successFn(done, io, socket));
|
||||
|
||||
let dynamicNsp = io
|
||||
.of(/^\/dynamic-\d+$/)
|
||||
.on("connect", (socket) => {
|
||||
expect(socket.nsp.name).to.be("/dynamic-101");
|
||||
dynamicNsp.emit("hello", 1, "2", { 3: "4" });
|
||||
partialDone();
|
||||
})
|
||||
.use((socket, next) => {
|
||||
next();
|
||||
partialDone();
|
||||
});
|
||||
socket.on("connect_error", (err) => {
|
||||
expect().fail();
|
||||
});
|
||||
socket.on("connect", () => {
|
||||
partialDone();
|
||||
});
|
||||
socket.on("hello", (a, b, c) => {
|
||||
expect(a).to.eql(1);
|
||||
expect(b).to.eql("2");
|
||||
expect(c).to.eql({ 3: "4" });
|
||||
partialDone();
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow connections to dynamic namespaces with a function", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io, "/dynamic-101");
|
||||
|
||||
io.of((name, query, next) => next(null, "/dynamic-101" === name));
|
||||
socket.on("connect", successFn(done, io, socket));
|
||||
});
|
||||
|
||||
it("should disallow connections when no dynamic namespace matches", (done) => {
|
||||
const io = new Server(0);
|
||||
const socket = createClient(io, "/abc");
|
||||
io.of(/^\/dynamic-\d+$/);
|
||||
io.of((name, query, next) => next(null, "/dynamic-101" === name));
|
||||
|
||||
socket.on("connect_error", (err) => {
|
||||
expect(err.message).to.be("Invalid namespace");
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit an 'new_namespace' event for a dynamic namespace", (done) => {
|
||||
const io = new Server(0);
|
||||
io.of(/^\/dynamic-\d+$/);
|
||||
const socket = createClient(io, "/dynamic-101");
|
||||
|
||||
io.on("new_namespace", (namespace) => {
|
||||
expect(namespace.name).to.be("/dynamic-101");
|
||||
|
||||
success(done, io, socket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
|
||||
const io = new Server(0);
|
||||
const counters = {
|
||||
connected: 0,
|
||||
created: 0,
|
||||
events: 0,
|
||||
};
|
||||
const buffer: Function[] = [];
|
||||
io.on("new_namespace", (namespace) => {
|
||||
counters.created++;
|
||||
});
|
||||
|
||||
const handler = () => {
|
||||
if (++counters.events === 2) {
|
||||
expect(counters.created).to.equal(1);
|
||||
success(done, io, one, two);
|
||||
}
|
||||
};
|
||||
|
||||
io.of((name, query, next) => {
|
||||
buffer.push(next);
|
||||
if (buffer.length === 2) {
|
||||
buffer.forEach((next) => next(null, true));
|
||||
}
|
||||
}).on("connection", (socket) => {
|
||||
if (++counters.connected === 2) {
|
||||
io.of("/dynamic-101").emit("message");
|
||||
}
|
||||
});
|
||||
|
||||
let one = createClient(io, "/dynamic-101");
|
||||
let two = createClient(io, "/dynamic-101");
|
||||
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+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
174
test/server-attachment.ts
Normal file
174
test/server-attachment.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import { Server } from "..";
|
||||
import { createServer } from "http";
|
||||
import request from "supertest";
|
||||
import expect from "expect.js";
|
||||
import { getPort, successFn } from "./support/util";
|
||||
|
||||
describe("server attachment", () => {
|
||||
describe("http.Server", () => {
|
||||
const clientVersion = require("socket.io-client/package.json").version;
|
||||
|
||||
const testSource = (filename) => (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/" + filename)
|
||||
.buffer(true)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
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/);
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
const testSourceMap = (filename) => (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/" + filename)
|
||||
.buffer(true)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
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);
|
||||
done();
|
||||
});
|
||||
};
|
||||
|
||||
it("should serve client", testSource("socket.io.js"));
|
||||
it(
|
||||
"should serve client with query string",
|
||||
testSource("socket.io.js?buster=" + Date.now())
|
||||
);
|
||||
it("should serve source map", testSourceMap("socket.io.js.map"));
|
||||
it("should serve client (min)", testSource("socket.io.min.js"));
|
||||
|
||||
it("should serve source map (min)", testSourceMap("socket.io.min.js.map"));
|
||||
|
||||
it("should serve client (gzip)", (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.set("accept-encoding", "gzip,br,deflate")
|
||||
.buffer(true)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
expect(res.headers["content-encoding"]).to.be("gzip");
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"should serve bundle with msgpack parser",
|
||||
testSource("socket.io.msgpack.min.js")
|
||||
);
|
||||
|
||||
it(
|
||||
"should serve source map for bundle with msgpack parser",
|
||||
testSourceMap("socket.io.msgpack.min.js.map")
|
||||
);
|
||||
|
||||
it("should serve the ESM bundle", testSource("socket.io.esm.min.js"));
|
||||
|
||||
it(
|
||||
"should serve the source map for the ESM bundle",
|
||||
testSourceMap("socket.io.esm.min.js.map")
|
||||
);
|
||||
|
||||
it("should handle 304", (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.set("If-None-Match", '"' + clientVersion + '"')
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
expect(res.statusCode).to.be(304);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle 304", (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.set("If-None-Match", 'W/"' + clientVersion + '"')
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
expect(res.statusCode).to.be(304);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not serve static files", (done) => {
|
||||
const srv = createServer();
|
||||
new Server(srv, { serveClient: false });
|
||||
request(srv).get("/socket.io/socket.io.js").expect(400, done);
|
||||
});
|
||||
|
||||
it("should work with #attach", (done) => {
|
||||
const srv = createServer((req, res) => {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
const sockets = new Server();
|
||||
sockets.attach(srv);
|
||||
request(srv)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should work with #attach (and merge options)", () => {
|
||||
const srv = createServer((req, res) => {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
const server = new Server({
|
||||
pingTimeout: 6000,
|
||||
});
|
||||
server.attach(srv, {
|
||||
pingInterval: 24000,
|
||||
});
|
||||
// @ts-ignore
|
||||
expect(server.eio.opts.pingTimeout).to.eql(6000);
|
||||
// @ts-ignore
|
||||
expect(server.eio.opts.pingInterval).to.eql(24000);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("port", () => {
|
||||
it("should be bound", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
request(`http://localhost:${getPort(io)}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, successFn(done, io));
|
||||
});
|
||||
|
||||
it("with listen", (done) => {
|
||||
const io = new Server().listen(0);
|
||||
|
||||
request(`http://localhost:${getPort(io)}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.expect(200, successFn(done, io));
|
||||
});
|
||||
});
|
||||
});
|
||||
60
test/socket-middleware.ts
Normal file
60
test/socket-middleware.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Server } from "..";
|
||||
import expect from "expect.js";
|
||||
import { success, createClient } from "./support/util";
|
||||
|
||||
describe("socket middleware", () => {
|
||||
it("should call functions", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io, "/", { multiplex: false });
|
||||
|
||||
clientSocket.emit("join", "woot");
|
||||
|
||||
let run = 0;
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.use((event, next) => {
|
||||
expect(event).to.eql(["join", "woot"]);
|
||||
event.unshift("wrap");
|
||||
run++;
|
||||
next();
|
||||
});
|
||||
socket.use((event, next) => {
|
||||
expect(event).to.eql(["wrap", "join", "woot"]);
|
||||
run++;
|
||||
next();
|
||||
});
|
||||
socket.on("wrap", (data1, data2) => {
|
||||
expect(data1).to.be("join");
|
||||
expect(data2).to.be("woot");
|
||||
expect(run).to.be(2);
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should pass errors", (done) => {
|
||||
const io = new Server(0);
|
||||
const clientSocket = createClient(io, "/", { multiplex: false });
|
||||
|
||||
clientSocket.emit("join", "woot");
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.use((event, next) => {
|
||||
next(new Error("Authentication error"));
|
||||
});
|
||||
socket.use((event, next) => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
socket.on("join", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
socket.on("error", (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
expect(err.message).to.eql("Authentication error");
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
87
test/socket-timeout.ts
Normal file
87
test/socket-timeout.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Server } from "..";
|
||||
import { createClient, success } from "./support/util";
|
||||
import expect from "expect.js";
|
||||
|
||||
describe("timeout", () => {
|
||||
it("should timeout if the client does not acknowledge the event", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(50).emit("unknown", (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
success(done, io, client);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should timeout if the client does not acknowledge the event in time", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
client.on("echo", (arg, cb) => {
|
||||
cb(arg);
|
||||
});
|
||||
|
||||
let count = 0;
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(0).emit("echo", 42, (err) => {
|
||||
expect(err).to.be.an(Error);
|
||||
count++;
|
||||
});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(count).to.eql(1);
|
||||
success(done, io, client);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
it("should not timeout if the client does acknowledge the event", (done) => {
|
||||
const io = new Server(0);
|
||||
const client = createClient(io, "/");
|
||||
|
||||
client.on("echo", (arg, cb) => {
|
||||
cb(arg);
|
||||
});
|
||||
|
||||
io.on("connection", (socket) => {
|
||||
socket.timeout(50).emit("echo", 42, (err, value) => {
|
||||
expect(err).to.be(null);
|
||||
expect(value).to.be(42);
|
||||
success(done, io, client);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,10 @@
|
||||
"use strict";
|
||||
import { Server, Socket } from "..";
|
||||
import { Namespace, Server, Socket } from "..";
|
||||
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.
|
||||
|
||||
@@ -16,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) => {
|
||||
@@ -90,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", () => {
|
||||
@@ -117,7 +141,9 @@ describe("server", () => {
|
||||
|
||||
it("does not accept arguments of wrong types", (done) => {
|
||||
const srv = createServer();
|
||||
const sio = new Server<BidirectionalEvents>(srv);
|
||||
const sio = new Server<BidirectionalEvents, BidirectionalEvents, {}>(
|
||||
srv
|
||||
);
|
||||
expectError(sio.on("random", (a, b, c) => {}));
|
||||
srv.listen(() => {
|
||||
expectError(sio.on("wrong name", (s) => {}));
|
||||
@@ -163,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", () => {
|
||||
@@ -181,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -205,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();
|
||||
});
|
||||
});
|
||||
@@ -216,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"));
|
||||
@@ -228,5 +324,123 @@ 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 for the serverSideEmit method", () => {
|
||||
interface ClientToServerEvents {
|
||||
helloFromClient: (message: string) => void;
|
||||
}
|
||||
|
||||
interface ServerToClientEvents {
|
||||
helloFromServer: (message: string, x: number) => void;
|
||||
}
|
||||
|
||||
interface InterServerEvents {
|
||||
helloFromServerToServer: (message: string, x: number) => void;
|
||||
ackFromServerToServer: (foo: string, cb: (bar: number) => void) => void;
|
||||
}
|
||||
|
||||
describe("on", () => {
|
||||
it("infers correct types for listener parameters", () => {
|
||||
const srv = createServer();
|
||||
const sio = new Server<
|
||||
ClientToServerEvents,
|
||||
ServerToClientEvents,
|
||||
InterServerEvents
|
||||
>(srv);
|
||||
|
||||
expectType<
|
||||
Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents>
|
||||
>(sio);
|
||||
srv.listen(async () => {
|
||||
sio.serverSideEmit("helloFromServerToServer", "hello", 10);
|
||||
sio
|
||||
.of("/test")
|
||||
.serverSideEmit("helloFromServerToServer", "hello", 10);
|
||||
|
||||
sio.on("helloFromServerToServer", (message, x) => {
|
||||
expectType<string>(message);
|
||||
expectType<number>(x);
|
||||
});
|
||||
sio.of("/test").on("helloFromServerToServer", (message, x) => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("adapter", () => {
|
||||
it("accepts arguments of the correct types", () => {
|
||||
const io = new Server({
|
||||
adapter: (nsp) => new Adapter(nsp),
|
||||
});
|
||||
io.adapter(Adapter);
|
||||
|
||||
class MyCustomAdapter extends Adapter {
|
||||
constructor(nsp, readonly opts) {
|
||||
super(nsp);
|
||||
}
|
||||
}
|
||||
io.adapter((nsp) => new MyCustomAdapter(nsp, { test: "123" }));
|
||||
});
|
||||
|
||||
it("does not accept arguments of wrong types", () => {
|
||||
const io = new Server();
|
||||
expectError(io.adapter((nsp) => "nope"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
2715
test/socket.io.ts
2715
test/socket.io.ts
File diff suppressed because it is too large
Load Diff
1108
test/socket.ts
Normal file
1108
test/socket.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,19 @@
|
||||
import type { Server } from "../..";
|
||||
import {
|
||||
io as ioc,
|
||||
ManagerOptions,
|
||||
Socket as ClientSocket,
|
||||
SocketOptions,
|
||||
} from "socket.io-client";
|
||||
import request from "supertest";
|
||||
|
||||
const expect = require("expect.js");
|
||||
const i = expect.stringify;
|
||||
|
||||
// add support for Set/Map
|
||||
const contain = expect.Assertion.prototype.contain;
|
||||
expect.Assertion.prototype.contain = function (...args) {
|
||||
if (typeof this.obj === "object") {
|
||||
if (this.obj instanceof Set || this.obj instanceof Map) {
|
||||
args.forEach((obj) => {
|
||||
this.assert(
|
||||
this.obj.has(obj),
|
||||
@@ -20,3 +29,91 @@ expect.Assertion.prototype.contain = function (...args) {
|
||||
}
|
||||
return contain.apply(this, args);
|
||||
};
|
||||
|
||||
export function createClient(
|
||||
io: Server,
|
||||
nsp: string = "/",
|
||||
opts?: Partial<ManagerOptions & SocketOptions>
|
||||
): ClientSocket {
|
||||
// @ts-ignore
|
||||
const port = io.httpServer.address().port;
|
||||
return ioc(`http://localhost:${port}${nsp}`, opts);
|
||||
}
|
||||
|
||||
export function success(
|
||||
done: Function,
|
||||
io: Server,
|
||||
...clients: ClientSocket[]
|
||||
) {
|
||||
io.close();
|
||||
clients.forEach((client) => client.disconnect());
|
||||
done();
|
||||
}
|
||||
|
||||
export function successFn(
|
||||
done: () => void,
|
||||
sio: Server,
|
||||
...clientSockets: ClientSocket[]
|
||||
) {
|
||||
return () => success(done, sio, ...clientSockets);
|
||||
}
|
||||
|
||||
export function getPort(io: Server): number {
|
||||
// @ts-ignore
|
||||
return io.httpServer.address().port;
|
||||
}
|
||||
|
||||
export function createPartialDone(count: number, done: (err?: Error) => void) {
|
||||
let i = 0;
|
||||
return () => {
|
||||
if (++i === count) {
|
||||
done();
|
||||
} else if (i > count) {
|
||||
done(new Error(`partialDone() called too many times: ${i} > ${count}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,26 +6,10 @@ import expect from "expect.js";
|
||||
import type { AddressInfo } from "net";
|
||||
|
||||
import "./support/util";
|
||||
import { createPartialDone } from "./support/util";
|
||||
|
||||
const SOCKETS_COUNT = 3;
|
||||
|
||||
const createPartialDone = (
|
||||
count: number,
|
||||
done: () => void,
|
||||
callback?: () => void
|
||||
) => {
|
||||
let i = 0;
|
||||
return () => {
|
||||
i++;
|
||||
if (i === count) {
|
||||
done();
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
class DummyAdapter extends Adapter {
|
||||
fetchSockets(opts: BroadcastOptions): Promise<any[]> {
|
||||
return Promise.resolve([
|
||||
@@ -49,7 +33,7 @@ class DummyAdapter extends Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
describe("socket.io", () => {
|
||||
describe("utility methods", () => {
|
||||
let io: Server, clientSockets: ClientSocket[], serverSockets: Socket[];
|
||||
beforeEach((done) => {
|
||||
const srv = createServer();
|
||||
@@ -59,7 +43,12 @@ describe("socket.io", () => {
|
||||
|
||||
clientSockets = [];
|
||||
for (let i = 0; i < SOCKETS_COUNT; i++) {
|
||||
clientSockets.push(ioc(`http://localhost:${port}`));
|
||||
clientSockets.push(
|
||||
ioc(`http://localhost:${port}`, {
|
||||
// FIXME needed so that clients are properly closed
|
||||
transports: ["websocket"],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
serverSockets = [];
|
||||
@@ -77,100 +66,99 @@ describe("socket.io", () => {
|
||||
clientSockets.forEach((socket) => socket.disconnect());
|
||||
});
|
||||
|
||||
describe("utility methods", () => {
|
||||
describe("fetchSockets", () => {
|
||||
it("returns all socket instances", async () => {
|
||||
const sockets = await io.fetchSockets();
|
||||
expect(sockets.length).to.eql(3);
|
||||
});
|
||||
describe("fetchSockets", () => {
|
||||
it("returns all socket instances", async () => {
|
||||
const sockets = await io.fetchSockets();
|
||||
expect(sockets.length).to.eql(3);
|
||||
});
|
||||
|
||||
it("returns all socket instances in the given room", async () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
const sockets = await io.in("room1").fetchSockets();
|
||||
expect(sockets.length).to.eql(2);
|
||||
});
|
||||
it("returns all socket instances in the given room", async () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
const sockets = await io.in("room1").fetchSockets();
|
||||
expect(sockets.length).to.eql(2);
|
||||
});
|
||||
|
||||
it("works with a custom adapter", async () => {
|
||||
io.adapter(DummyAdapter);
|
||||
const sockets = await io.fetchSockets();
|
||||
expect(sockets.length).to.eql(1);
|
||||
const remoteSocket = sockets[0];
|
||||
expect(remoteSocket.id).to.eql("42");
|
||||
expect(remoteSocket.rooms).to.contain("42", "room1");
|
||||
expect(remoteSocket.data).to.eql({ username: "john" });
|
||||
it("works with a custom adapter", async () => {
|
||||
io.adapter(DummyAdapter);
|
||||
const sockets = await io.fetchSockets();
|
||||
expect(sockets.length).to.eql(1);
|
||||
const remoteSocket = sockets[0];
|
||||
expect(remoteSocket.id).to.eql("42");
|
||||
expect(remoteSocket.rooms).to.contain("42", "room1");
|
||||
expect(remoteSocket.data).to.eql({ username: "john" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("socketsJoin", () => {
|
||||
it("makes all socket instances join the given room", () => {
|
||||
io.socketsJoin("room1");
|
||||
serverSockets.forEach((socket) => {
|
||||
expect(socket.rooms).to.contain("room1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("socketsJoin", () => {
|
||||
it("makes all socket instances join the given room", () => {
|
||||
io.socketsJoin("room1");
|
||||
serverSockets.forEach((socket) => {
|
||||
expect(socket.rooms).to.contain("room1");
|
||||
});
|
||||
});
|
||||
it("makes all socket instances in a room join the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room1").socketsJoin("room3");
|
||||
expect(serverSockets[0].rooms).to.contain("room3");
|
||||
expect(serverSockets[1].rooms).to.contain("room3");
|
||||
expect(serverSockets[2].rooms).to.not.contain("room3");
|
||||
});
|
||||
});
|
||||
|
||||
it("makes all socket instances in a room join the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room1").socketsJoin("room3");
|
||||
expect(serverSockets[0].rooms).to.contain("room3");
|
||||
expect(serverSockets[1].rooms).to.contain("room3");
|
||||
expect(serverSockets[2].rooms).to.not.contain("room3");
|
||||
});
|
||||
describe("socketsLeave", () => {
|
||||
it("makes all socket instances leave the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.socketsLeave("room1");
|
||||
expect(serverSockets[0].rooms).to.contain("room2");
|
||||
expect(serverSockets[0].rooms).to.not.contain("room1");
|
||||
expect(serverSockets[1].rooms).to.not.contain("room1");
|
||||
});
|
||||
|
||||
describe("socketsLeave", () => {
|
||||
it("makes all socket instances leave the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.socketsLeave("room1");
|
||||
expect(serverSockets[0].rooms).to.contain("room2");
|
||||
expect(serverSockets[0].rooms).to.not.contain("room1");
|
||||
expect(serverSockets[1].rooms).to.not.contain("room1");
|
||||
});
|
||||
it("makes all socket instances in a room leave the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room2").socketsLeave("room1");
|
||||
expect(serverSockets[0].rooms).to.contain("room2");
|
||||
expect(serverSockets[0].rooms).to.not.contain("room1");
|
||||
expect(serverSockets[1].rooms).to.contain("room1");
|
||||
});
|
||||
});
|
||||
|
||||
it("makes all socket instances in a room leave the given room", () => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room2").socketsLeave("room1");
|
||||
expect(serverSockets[0].rooms).to.contain("room2");
|
||||
expect(serverSockets[0].rooms).to.not.contain("room1");
|
||||
expect(serverSockets[1].rooms).to.contain("room1");
|
||||
});
|
||||
describe("disconnectSockets", () => {
|
||||
it("makes all socket instances disconnect", (done) => {
|
||||
io.disconnectSockets(true);
|
||||
|
||||
const partialDone = createPartialDone(3, done);
|
||||
|
||||
clientSockets[0].on("disconnect", partialDone);
|
||||
clientSockets[1].on("disconnect", partialDone);
|
||||
clientSockets[2].on("disconnect", partialDone);
|
||||
});
|
||||
|
||||
describe("disconnectSockets", () => {
|
||||
it("makes all socket instances disconnect", (done) => {
|
||||
io.disconnectSockets(true);
|
||||
it("makes all socket instances in a room disconnect", (done) => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room2").disconnectSockets(true);
|
||||
|
||||
const partialDone = createPartialDone(3, done);
|
||||
|
||||
clientSockets[0].on("disconnect", partialDone);
|
||||
clientSockets[1].on("disconnect", partialDone);
|
||||
clientSockets[2].on("disconnect", partialDone);
|
||||
const partialDone = createPartialDone(2, () => {
|
||||
clientSockets[1].off("disconnect");
|
||||
done();
|
||||
});
|
||||
|
||||
it("makes all socket instances in a room disconnect", (done) => {
|
||||
serverSockets[0].join(["room1", "room2"]);
|
||||
serverSockets[1].join("room1");
|
||||
serverSockets[2].join("room2");
|
||||
io.in("room2").disconnectSockets(true);
|
||||
|
||||
const partialDone = createPartialDone(2, done, () => {
|
||||
clientSockets[1].off("disconnect");
|
||||
});
|
||||
|
||||
clientSockets[0].on("disconnect", partialDone);
|
||||
clientSockets[1].on("disconnect", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
clientSockets[2].on("disconnect", partialDone);
|
||||
clientSockets[0].on("disconnect", partialDone);
|
||||
clientSockets[1].on("disconnect", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
clientSockets[2].on("disconnect", partialDone);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
213
test/uws.ts
Normal file
213
test/uws.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
App,
|
||||
us_socket_local_port,
|
||||
us_listen_socket_close,
|
||||
} from "uWebSockets.js";
|
||||
import { Server } from "..";
|
||||
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
|
||||
import request from "supertest";
|
||||
import expect from "expect.js";
|
||||
|
||||
const createPartialDone = (done: (err?: Error) => void, count: number) => {
|
||||
let i = 0;
|
||||
return () => {
|
||||
if (++i === count) {
|
||||
done();
|
||||
} else if (i > count) {
|
||||
done(new Error(`partialDone() called too many times: ${i} > ${count}`));
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const shouldNotHappen = (done) => () => done(new Error("should not happen"));
|
||||
|
||||
describe("socket.io with uWebSocket.js-based engine", () => {
|
||||
let io: Server,
|
||||
uwsSocket: any,
|
||||
port: number,
|
||||
client: ClientSocket,
|
||||
clientWSOnly: ClientSocket,
|
||||
clientPollingOnly: ClientSocket,
|
||||
clientCustomNamespace: ClientSocket;
|
||||
|
||||
beforeEach((done) => {
|
||||
const app = App();
|
||||
io = new Server();
|
||||
io.attachApp(app);
|
||||
|
||||
io.of("/custom");
|
||||
|
||||
app.listen(0, (listenSocket) => {
|
||||
uwsSocket = listenSocket;
|
||||
port = us_socket_local_port(listenSocket);
|
||||
|
||||
client = ioc(`http://localhost:${port}`);
|
||||
clientWSOnly = ioc(`http://localhost:${port}`, {
|
||||
transports: ["websocket"],
|
||||
});
|
||||
clientPollingOnly = ioc(`http://localhost:${port}`, {
|
||||
transports: ["polling"],
|
||||
});
|
||||
clientCustomNamespace = ioc(`http://localhost:${port}/custom`);
|
||||
});
|
||||
|
||||
const partialDone = createPartialDone(done, 4);
|
||||
client.on("connect", partialDone);
|
||||
clientWSOnly.once("connect", partialDone);
|
||||
clientPollingOnly.on("connect", partialDone);
|
||||
clientCustomNamespace.on("connect", partialDone);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
io.close();
|
||||
us_listen_socket_close(uwsSocket);
|
||||
|
||||
client.disconnect();
|
||||
clientWSOnly.disconnect();
|
||||
clientPollingOnly.disconnect();
|
||||
clientCustomNamespace.disconnect();
|
||||
});
|
||||
|
||||
it("should broadcast", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in a namespace", (done) => {
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", shouldNotHappen(done));
|
||||
clientPollingOnly.on("hello", shouldNotHappen(done));
|
||||
clientCustomNamespace.on("hello", done);
|
||||
|
||||
io.of("/custom").emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in a dynamic namespace", (done) => {
|
||||
const dynamicNamespace = io.of(/\/dynamic-\d+/);
|
||||
const dynamicClient = clientWSOnly.io.socket("/dynamic-101");
|
||||
|
||||
dynamicClient.on("connect", () => {
|
||||
dynamicNamespace.emit("hello");
|
||||
});
|
||||
|
||||
dynamicClient.on("hello", () => {
|
||||
dynamicClient.disconnect();
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should broadcast binary content", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.emit("hello", Buffer.from([1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should broadcast volatile packet with binary content", (done) => {
|
||||
const partialDone = createPartialDone(done, 3);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
// wait to make sure there are no packets being sent for opening the connection
|
||||
setTimeout(() => {
|
||||
io.volatile.emit("hello", Buffer.from([1, 2, 3]));
|
||||
}, 20);
|
||||
});
|
||||
|
||||
it("should broadcast in a room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
|
||||
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in multiple rooms", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", shouldNotHappen(done));
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
|
||||
|
||||
io.to(["room1", "room2"]).emit("hello");
|
||||
});
|
||||
|
||||
it("should broadcast in all but a given room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", partialDone);
|
||||
clientPollingOnly.on("hello", shouldNotHappen(done));
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
|
||||
|
||||
io.except("room2").emit("hello");
|
||||
});
|
||||
|
||||
it("should work even after leaving room", (done) => {
|
||||
const partialDone = createPartialDone(done, 2);
|
||||
|
||||
client.on("hello", partialDone);
|
||||
clientWSOnly.on("hello", shouldNotHappen(done));
|
||||
clientPollingOnly.on("hello", partialDone);
|
||||
clientCustomNamespace.on("hello", shouldNotHappen(done));
|
||||
|
||||
io.of("/").sockets.get(client.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
|
||||
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
|
||||
io.of("/").sockets.get(clientWSOnly.id)!.leave("room1");
|
||||
|
||||
io.to("room1").emit("hello");
|
||||
});
|
||||
|
||||
it("should not crash when socket is disconnected before the upgrade", (done) => {
|
||||
client.on("disconnect", () => done());
|
||||
|
||||
io.of("/").sockets.get(client.id)!.disconnect();
|
||||
});
|
||||
|
||||
it("should serve static files", (done) => {
|
||||
const clientVersion = require("socket.io-client/package.json").version;
|
||||
|
||||
request(`http://localhost:${port}`)
|
||||
.get("/socket.io/socket.io.js")
|
||||
.buffer(true)
|
||||
.end((err, res) => {
|
||||
if (err) return done(err);
|
||||
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/);
|
||||
expect(res.status).to.be(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
64
test/v2-compatibility.ts
Normal file
64
test/v2-compatibility.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { Server, Socket } from "..";
|
||||
import expect from "expect.js";
|
||||
import { success, getPort, waitFor } from "./support/util";
|
||||
import * as io_v2 from "socket.io-client-v2";
|
||||
|
||||
describe("v2 compatibility", () => {
|
||||
it("should connect if `allowEIO3` is true", (done) => {
|
||||
const io = new Server(0, {
|
||||
allowEIO3: true,
|
||||
});
|
||||
|
||||
const clientSocket = io_v2.connect(`http://localhost:${getPort(io)}`, {
|
||||
multiplex: false,
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
waitFor(io, "connection"),
|
||||
waitFor(clientSocket, "connect"),
|
||||
]).then(([socket]) => {
|
||||
expect((socket as Socket).id).to.eql(clientSocket.id);
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to connect to a namespace with a query", (done) => {
|
||||
const io = new Server(0, {
|
||||
allowEIO3: true,
|
||||
});
|
||||
|
||||
const clientSocket = io_v2.connect(
|
||||
`http://localhost:${getPort(io)}/the-namespace`,
|
||||
{
|
||||
multiplex: false,
|
||||
}
|
||||
);
|
||||
clientSocket.query = { test: "123" };
|
||||
|
||||
Promise.all([
|
||||
waitFor(io.of("/the-namespace"), "connection"),
|
||||
waitFor(clientSocket, "connect"),
|
||||
]).then(([socket]) => {
|
||||
expect((socket as Socket).handshake.auth).to.eql({ test: "123" });
|
||||
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not connect if `allowEIO3` is false (default)", (done) => {
|
||||
const io = new Server(0);
|
||||
|
||||
const clientSocket = io_v2.connect(`http://localhost:${getPort(io)}`, {
|
||||
multiplex: false,
|
||||
});
|
||||
|
||||
clientSocket.on("connect", () => {
|
||||
done(new Error("should not happen"));
|
||||
});
|
||||
|
||||
clientSocket.on("connect_error", () => {
|
||||
success(done, io, clientSocket);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user