Compare commits

...

51 Commits

Author SHA1 Message Date
Maxime Kjaer
f3ea96eb3a test: build examples in the CI (#3856) 2023-10-10 20:00:27 +02:00
abriejenny
c2858e911e docs(examples): update docker-compose.yml (#4594)
Updated docker compose to be in line with current practices.

Reference: https://docs.docker.com/compose/
2023-01-12 09:56:11 +01:00
Szegedi Ádám
2f96438952 chore: bump engine.io version to fix CVE-2022-21676 (#4262)
Related: https://github.com/socketio/engine.io/security/advisories/GHSA-273r-mgr4-v34f
2022-01-25 22:18:18 +01:00
Chris Swithinbank
02c87a8561 fix(typings): ensure compatibility with TypeScript 3.x (#4259)
Labeled tuple elements were added in TypeScript 4.0.

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

Related: 44e20ba5bf
2022-01-25 01:25:05 +01:00
Damien Arrachequesne
37b6d8fff0 chore: update default label for bug reports 2022-01-10 08:55:56 +01:00
Damien Arrachequesne
af54565b2d docs: remove broken badges
Related: https://github.com/socketio/socket.io/issues/4242
2022-01-10 08:03:53 +01:00
Damien Arrachequesne
aa5312a4b6 chore: revert to lockfile v1
Updating to v2 fails in the CI on Node.js 12 & 14 with the following
error:

> npm ERR! Error while executing:
> npm ERR! /usr/bin/git ls-remote -h -t ssh://git@github.com/uNetworking/uWebSockets.js.git
> npm ERR!
> npm ERR! Warning: Permanently added the RSA host key for IP address '140.82.113.3' to the list of known hosts.
> npm ERR! git@github.com: Permission denied (publickey).
> npm ERR! fatal: Could not read from remote repository.
> npm ERR!
> npm ERR! Please make sure you have the correct access rights
> npm ERR! and the repository exists.
> npm ERR!
> npm ERR! exited with error code: 128

So we will revert the change for now.
2022-01-06 08:01:00 +01:00
Damien Arrachequesne
c82a4bdf1f chore(release): 4.4.1
Diff: https://github.com/socketio/socket.io/compare/4.4.0...4.4.1
2022-01-06 07:32:03 +01:00
Orkhan Alikhanov
770ee5949f fix(types): make RemoteSocket.data type safe (#4234)
Related:

- https://github.com/socketio/socket.io/issues/4229
- fe8730ca0f
2022-01-06 07:14:55 +01:00
Damien Arrachequesne
3bf5d92735 refactor: add note about fetchSockets() for parent namespaces
Related: https://github.com/socketio/socket.io/issues/4235
2022-01-05 08:50:40 +01:00
Shayan Yousefi
fc82e44f73 refactor(typings): export Event type (#4215)
So that it can be used by the end users:

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

io.on("connection", (socket) => {
  socket.use(myMiddleware);
});
```
2022-01-05 08:08:18 +01:00
Damien Arrachequesne
c840bad43a test: fix flaky tests 2022-01-05 08:00:55 +01:00
Orkhan Alikhanov
f2b8de7191 fix(typings): pass SocketData type to custom namespaces (#4233)
The `SocketData` type was only available on the main namespace.

Related: https://github.com/socketio/socket.io/issues/4229
See also: fe8730ca0f
2022-01-04 09:09:42 +01:00
Gray Zhang
51784d0305 chore: add types to exports field to be compatible with nodenext module resolution (#4228)
See [1] for detail, in `nodenext` module resolution it requires a
`types` field in `exports` with full filename including extension.

[1]: https://github.com/microsoft/TypeScript/issues/46770#issuecomment-966612103
2021-12-28 10:27:08 +01:00
Damien Arrachequesne
c196689545 docs: fix basic crud example
Related: https://github.com/socketio/socket.io/issues/4213
2021-12-16 23:00:20 +01:00
Mikhail Dudin
7a70f63499 docs: fix reconnection handling in the chat demo app (#4189) 2021-12-01 00:03:43 +01:00
anderslatif
e5897dd7dc docs: add usage with ES modules (#4195) 2021-12-01 00:02:13 +01:00
Damien Arrachequesne
2071a66c5a docs: simplify nginx cluster example
- remove useless Dockerfile
- clean format
- migrate to @socket.io/redis-adapter
2021-11-24 18:15:26 +01:00
Damien Arrachequesne
0f11c4745f chore(release): 4.4.0
Diff: https://github.com/socketio/socket.io/compare/4.3.2...4.4.0
2021-11-18 14:10:19 +01:00
Damien Arrachequesne
b839a3b400 fix: prevent double ack when emitting with a timeout
The ack was not properly removed upon timeout, and could be called
twice.

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

```js
socket.timeout(5000).emit("my-event", (err) => {
  if (err) {
    // the client did not acknowledge the event in the given delay
  }
});
```
2021-11-16 20:07:53 +01:00
Damien Arrachequesne
b7213e71e4 test: fix flaky test
`srv.close()` only closes the underlying HTTP server, but this does not
terminate the existing WebSocket connections.

Reference: https://nodejs.org/api/http.html#serverclosecallback
2021-11-16 15:58:55 +01:00
Damien Arrachequesne
2da82103d2 test: add test for volatile packet with binary
See also: 88eee5948a
2021-11-16 15:57:32 +01:00
Damien Arrachequesne
02b0f73e2c fix: only set 'connected' to true after middleware execution
The Socket instance is only considered connected when the "connection"
event is emitted, and not during the middleware(s) execution.

```js
io.use((socket, next) => {
  console.log(socket.connected); // prints "false"
  next();
});

io.on("connection", (socket) => {
  console.log(socket.connected); // prints "true"
});
```

Related: https://github.com/socketio/socket.io/issues/4129
2021-11-12 07:31:52 +01:00
Damien Arrachequesne
c0d8c5ab23 feat: add an implementation based on uWebSockets.js
Usage:

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

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

server.attachApp(app);

app.listen(3000);
```

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

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

Related:

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

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

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

io.on("connection", (socket) => {
  socket.data.name = "john";
  socket.data.age = 42;
});
```
2021-11-08 15:21:48 +01:00
Damien Arrachequesne
ed8483da4d chore(release): 4.3.2
Diff: https://github.com/socketio/socket.io/compare/4.3.1...4.3.2
2021-11-08 06:39:20 +01:00
Sebastiaan Marynissen
9d86397243 fix: fix race condition in dynamic namespaces (#4137)
Using an async operation with `io.use()` could lead to the creation of
several instances of a same namespace, each of them overriding the
previous one.

Example:

```js
io.use(async (nsp, auth, next) => {
  await anOperationThatTakesSomeTime();
  next();
});
```

Related: https://github.com/socketio/socket.io/pull/4136
2021-10-24 07:46:29 +02:00
Naseem
44e20ba5bf refactor: add event type for use() (#4138) 2021-10-24 07:19:43 +02:00
Damien Arrachequesne
ccc5ec39a8 chore(release): 4.3.1
Diff: https://github.com/socketio/socket.io/compare/4.3.0...4.3.1
2021-10-17 00:02:16 +02:00
Josh Field
0ef2a4d02c fix: fix server attachment (#4127)
The check excluded an HTTPS server from being properly attached.

Related: https://github.com/socketio/socket.io/issues/4124
2021-10-16 23:58:55 +02:00
Damien Arrachequesne
95810aa62d chore(release): 4.3.0
Diff: https://github.com/socketio/socket.io/compare/4.2.0...4.3.0
2021-10-14 14:59:13 +02:00
Damien Arrachequesne
60edecb3bd feat: serve ESM bundle
Related:

- 0661564dc2
- https://github.com/socketio/socket.io-client/issues/1198
2021-10-13 18:17:12 +02:00
Damien Arrachequesne
eb5fdbd03e chore: bump engine.io to version 6.0.0
Release notes: https://github.com/socketio/engine.io/releases/tag/6.0.0
Diff: https://github.com/socketio/engine.io/compare/5.2.0...6.0.0
2021-10-12 00:05:10 +02:00
roh-kan
4974e9077c docs: update .NET client library link (#4115) 2021-10-08 14:18:03 +02:00
douira
033c5d399a fix(typings): add name field to cookie option (#4099)
Reference: 18a6eb89fb/lib/server.js (L355)
2021-09-20 09:13:38 +02:00
Damien Arrachequesne
7a74b66872 test: remove hardcoded ports
Related: https://github.com/socketio/socket.io/issues/3447
2021-09-09 08:57:11 +02:00
Damien Arrachequesne
dc81fcf461 fix: send volatile packets with binary attachments
The binary attachments of volatile packets were discarded (only the
header packet was sent) due to a bug introduced by [1].

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

[1]: dc381b72c6
2021-09-09 08:55:51 +02:00
Damien Arrachequesne
c100b7b61c chore(release): 4.2.0
Diff: https://github.com/socketio/socket.io/compare/4.1.3...4.2.0
2021-08-30 09:21:00 +02:00
Damien Arrachequesne
f03eeca39a chore: bump dependencies 2021-08-30 08:27:46 +02:00
Damien Arrachequesne
d8cc8aef7e docs: update the link of the Repl.it badge
The link will now point towards a sample project, instead of the root
repository.

Related: https://github.com/socketio/socket.io/issues/3934
2021-08-30 08:03:55 +02:00
Damien Arrachequesne
ccfd8caba6 fix(typings): allow async listener in typed events
So that:

```ts
socket.on("my-event", async () => {
  // ...
});
```

is valid under the @typescript-eslint/no-misused-promises rule.

Related: https://github.com/socketio/socket.io-client/issues/1486
2021-08-30 08:01:29 +02:00
Tim Düsterhus
24fee27ba3 feat: ignore the query string when serving client JavaScript (#4024)
Related: https://github.com/socketio/socket.io/issues/4023
2021-08-30 07:59:47 +02:00
brownman
310f8557a7 docs(examples): add missing module (#4018)
Fixes the following error:

> test/todo-management/todo.tests.ts:275:3 - error TS2582: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha`.

Co-authored-by: brownman <brownman@users.noreply.github.com>
2021-07-15 21:48:20 +02:00
Damien Arrachequesne
dbd2a07cda chore(release): 4.1.3
Diff: https://github.com/socketio/socket.io/compare/4.1.2...4.1.3
2021-07-10 12:13:15 +02:00
Damien Arrachequesne
94e27cd072 fix: fix io.except() method
Previously, calling `io.except("theroom").emit(...)` did not exclude
the sockets in the given room.

This method was forgotten in [1].

[1]: ac9e8ca6c7
2021-07-10 11:48:46 +02:00
Damien Arrachequesne
a4dffc6527 fix: remove x-sourcemap header
This header is useless, as the client bundle already contains a
sourceMappingURL field.

Besides, Firefox prints the following warning:

> <url> is being assigned a //# sourceMappingURL, but already has one

Related: https://github.com/socketio/socket.io/issues/3958
2021-07-04 00:51:41 +02:00
Damien Arrachequesne
7c44893d78 chore: bump dependencies 2021-07-04 00:37:35 +02:00
Daniele TDC
b833f918c8 ci: update to node 16 (#3990)
See also: https://github.com/nodejs/Release#release-schedule
2021-06-28 09:09:44 +02:00
Daniele TDC
24d8d1f67f ci: update setup-node step (#3986) 2021-06-24 14:53:46 +02:00
Damien Arrachequesne
6f2a50b932 docs(examples): update example to webpack 5 2021-06-15 22:35:06 +02:00
41 changed files with 5657 additions and 7906 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: 'bug' labels: 'to triage'
assignees: '' assignees: ''
--- ---

View File

@@ -12,15 +12,43 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [10.x, 12.x, 14.x, 15.x] node-version: [12, 14, 16]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1 uses: actions/setup-node@v2
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
- run: npm ci - run: npm ci
- run: npm test - run: npm test
env: env:
CI: true CI: true
build-examples:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
example:
- custom-parsers
- typescript
- webpack-build
- webpack-build-server
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: 20
- name: Build ${{ matrix.example }}
run: |
cd examples/${{ matrix.example }}
npm install
npm run build

View File

@@ -1,3 +1,81 @@
## [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) ## [4.1.2](https://github.com/socketio/socket.io/compare/4.1.1...4.1.2) (2021-05-17)

View File

@@ -1,9 +1,7 @@
# socket.io # socket.io
[![Run on Repl.it](https://repl.it/badge/github/socketio/socket.io)](https://repl.it/github/socketio/socket.io) [![Run on Repl.it](https://repl.it/badge/github/socketio/socket.io)](https://replit.com/@socketio/socketio-minimal-example)
[![Backers on Open Collective](https://opencollective.com/socketio/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/socketio/sponsors/badge.svg)](#sponsors) [![Backers on Open Collective](https://opencollective.com/socketio/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/socketio/sponsors/badge.svg)](#sponsors)
[![Build Status](https://github.com/socketio/socket.io/workflows/CI/badge.svg)](https://github.com/socketio/socket.io/actions) [![Build Status](https://github.com/socketio/socket.io/workflows/CI/badge.svg)](https://github.com/socketio/socket.io/actions)
[![Dependency Status](https://david-dm.org/socketio/socket.io.svg)](https://david-dm.org/socketio/socket.io)
[![devDependency Status](https://david-dm.org/socketio/socket.io/dev-status.svg)](https://david-dm.org/socketio/socket.io#info=devDependencies)
[![NPM version](https://badge.fury.io/js/socket.io.svg)](https://www.npmjs.com/package/socket.io) [![NPM version](https://badge.fury.io/js/socket.io.svg)](https://www.npmjs.com/package/socket.io)
![Downloads](https://img.shields.io/npm/dm/socket.io.svg?style=flat) ![Downloads](https://img.shields.io/npm/dm/socket.io.svg?style=flat)
[![](https://slackin-socketio.now.sh/badge.svg)](https://slackin-socketio.now.sh) [![](https://slackin-socketio.now.sh/badge.svg)](https://slackin-socketio.now.sh)
@@ -22,7 +20,7 @@ Some implementations in other languages are also available:
- [Swift](https://github.com/socketio/socket.io-client-swift) - [Swift](https://github.com/socketio/socket.io-client-swift)
- [Dart](https://github.com/rikulo/socket.io-client-dart) - [Dart](https://github.com/rikulo/socket.io-client-dart)
- [Python](https://github.com/miguelgrinberg/python-socketio) - [Python](https://github.com/miguelgrinberg/python-socketio)
- [.Net](https://github.com/Quobject/SocketIoClientDotNet) - [.NET](https://github.com/doghappy/socket.io-client-csharp)
Its main features are: Its main features are:
@@ -115,6 +113,14 @@ io.on('connection', client => { ... });
io.listen(3000); io.listen(3000);
``` ```
### Module syntax
```js
import { Server } from "socket.io";
const io = new Server(server);
io.listen(3000);
```
### In conjunction with Express ### In conjunction with Express
Starting with **3.0**, express applications have become request handler Starting with **3.0**, express applications have become request handler

7
client-dist/socket.io.esm.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -7,8 +7,8 @@ export enum Errors {
const errorValues: string[] = Object.values(Errors); const errorValues: string[] = Object.values(Errors);
export function sanitizeErrorMessage(message: string) { export function sanitizeErrorMessage(message: any) {
if (errorValues.includes(message)) { if (typeof message === "string" && errorValues.includes(message)) {
return message; return message;
} else { } else {
return "an unknown error has occurred"; return "an unknown error has occurred";

View File

@@ -24,6 +24,7 @@
"uuid": "^8.3.2" "uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^8.2.3",
"@types/chai": "^4.2.16", "@types/chai": "^4.2.16",
"@types/uuid": "^8.3.0", "@types/uuid": "^8.3.0",
"chai": "^4.3.4", "chai": "^4.3.4",

View File

@@ -6,7 +6,7 @@ A simple chat demo for Socket.IO
## How to use ## How to use
``` ```
$ npm ci $ npm i
$ npm start $ npm start
``` ```

View File

@@ -264,14 +264,14 @@ $(function() {
log('you have been disconnected'); log('you have been disconnected');
}); });
socket.on('reconnect', () => { socket.io.on('reconnect', () => {
log('you have been reconnected'); log('you have been reconnected');
if (username) { if (username) {
socket.emit('add user', username); socket.emit('add user', username);
} }
}); });
socket.on('reconnect_error', () => { socket.io.on('reconnect_error', () => {
log('attempt to reconnect has failed'); log('attempt to reconnect has failed');
}); });

View File

@@ -1,56 +1,43 @@
services:
nginx:
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
ports:
- "3000:80"
nginx: server-john:
build: ./nginx build: ./server
links: expose:
- server-john
- server-paul
- server-george
- server-ringo
ports:
- "3000:80"
server-john:
build: ./server
links:
- redis
expose:
- "3000" - "3000"
environment: environment:
- NAME=John - NAME=John
server-paul: server-paul:
build: ./server build: ./server
links: expose:
- redis - "3000"
expose: environment:
- "3000" - NAME=Paul
environment:
- NAME=Paul
server-george: server-george:
build: ./server build: ./server
links: expose:
- redis - "3000"
expose: environment:
- "3000" - NAME=George
environment:
- NAME=George
server-ringo: server-ringo:
build: ./server build: ./server
links: expose:
- redis - "3000"
expose: environment:
- "3000" - NAME=Ringo
environment:
- NAME=Ringo
client: client:
build: ./client build: ./client
links:
- nginx
redis: redis:
image: redis:alpine image: redis:alpine
expose: expose:
- "6379" - "6379"

View File

@@ -1,3 +0,0 @@
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf

View File

@@ -1,15 +1,18 @@
// Setup basic express server const express = require('express');
var express = require('express'); const app = express();
var app = express(); const server = require('http').createServer(app);
var server = require('http').createServer(app); const io = require('socket.io')(server);
var io = require('socket.io')(server); const { createAdapter } = require('@socket.io/redis-adapter');
var redis = require('socket.io-redis'); const { createClient } = require('redis');
var port = process.env.PORT || 3000; const port = process.env.PORT || 3000;
var serverName = process.env.NAME || 'Unknown'; 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('Server listening at port %d', port);
console.log('Hello, I\'m %s, how can I help?', serverName); console.log('Hello, I\'m %s, how can I help?', serverName);
}); });
@@ -19,15 +22,15 @@ app.use(express.static(__dirname + '/public'));
// Chatroom // Chatroom
var numUsers = 0; let numUsers = 0;
io.on('connection', function (socket) { io.on('connection', socket => {
socket.emit('my-name-is', serverName); socket.emit('my-name-is', serverName);
var addedUser = false; let addedUser = false;
// when the client emits 'new message', this listens and executes // 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' // we tell the client to execute 'new message'
socket.broadcast.emit('new message', { socket.broadcast.emit('new message', {
username: socket.username, username: socket.username,
@@ -36,7 +39,7 @@ io.on('connection', function (socket) {
}); });
// when the client emits 'add user', this listens and executes // when the client emits 'add user', this listens and executes
socket.on('add user', function (username) { socket.on('add user', username => {
if (addedUser) return; if (addedUser) return;
// we store the username in the socket session for this client // 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 // when the client emits 'typing', we broadcast it to others
socket.on('typing', function () { socket.on('typing', () => {
socket.broadcast.emit('typing', { socket.broadcast.emit('typing', {
username: socket.username username: socket.username
}); });
}); });
// when the client emits 'stop typing', we broadcast it to others // 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', { socket.broadcast.emit('stop typing', {
username: socket.username username: socket.username
}); });
}); });
// when the user disconnects.. perform this // when the user disconnects.. perform this
socket.on('disconnect', function () { socket.on('disconnect', () => {
if (addedUser) { if (addedUser) {
--numUsers; --numUsers;

View File

@@ -7,9 +7,10 @@
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@socket.io/redis-adapter": "^7.0.1",
"express": "4.13.4", "express": "4.13.4",
"socket.io": "^4.0.0", "redis": "^3.1.2",
"socket.io-redis": "^6.0.1" "socket.io": "^4.0.0"
}, },
"scripts": { "scripts": {
"start": "node index.js" "start": "node index.js"

View 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);

View File

@@ -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));
}

View File

@@ -4,13 +4,15 @@
"description": "A sample Webpack build (for the server)", "description": "A sample Webpack build (for the server)",
"scripts": { "scripts": {
"start": "node dist/server.js", "start": "node dist/server.js",
"build": "webpack --config ./support/webpack.config.js" "build": "webpack"
}, },
"author": "Damien Arrachequesne", "author": "Damien Arrachequesne",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"bufferutil": "^4.0.3",
"socket.io": "^4.0.0", "socket.io": "^4.0.0",
"webpack": "~4.43.0", "utf-8-validate": "^5.0.5",
"webpack-cli": "~3.3.11" "webpack": "^5.39.0",
"webpack-cli": "^4.7.2"
} }
} }

View File

@@ -1,10 +0,0 @@
module.exports = {
entry: './lib/index.js',
target: 'node',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'server.js'
},
mode: 'production'
};

View 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",
},
],
},
};

View File

@@ -9,8 +9,9 @@ import type {
TypedEventBroadcaster, TypedEventBroadcaster,
} from "./typed-events"; } from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap> export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> { implements TypedEventBroadcaster<EmitEvents>
{
constructor( constructor(
private readonly adapter: Adapter, private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(), private readonly rooms: Set<Room> = new Set<Room>(),
@@ -25,7 +26,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
const rooms = new Set(this.rooms); const rooms = new Set(this.rooms);
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r)); room.forEach((r) => rooms.add(r));
@@ -47,7 +48,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return this.to(room); return this.to(room);
} }
@@ -58,7 +59,9 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
const exceptRooms = new Set(this.exceptRooms); const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) { if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r)); room.forEach((r) => exceptRooms.add(r));
@@ -80,7 +83,9 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public compress(compress: boolean): BroadcastOperator<EmitEvents> { public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
const flags = Object.assign({}, this.flags, { compress }); const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator( return new BroadcastOperator(
this.adapter, this.adapter,
@@ -98,7 +103,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
const flags = Object.assign({}, this.flags, { volatile: true }); const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator( return new BroadcastOperator(
this.adapter, this.adapter,
@@ -114,7 +119,7 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* @return a new BroadcastOperator instance * @return a new BroadcastOperator instance
* @public * @public
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local(): BroadcastOperator<EmitEvents, SocketData> {
const flags = Object.assign({}, this.flags, { local: true }); const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator( return new BroadcastOperator(
this.adapter, this.adapter,
@@ -176,7 +181,9 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* *
* @public * @public
*/ */
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> { public fetchSockets<SocketData = any>(): Promise<
RemoteSocket<EmitEvents, SocketData>[]
> {
return this.adapter return this.adapter
.fetchSockets({ .fetchSockets({
rooms: this.rooms, rooms: this.rooms,
@@ -186,9 +193,12 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
return sockets.map((socket) => { return sockets.map((socket) => {
if (socket instanceof Socket) { if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties // 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 { } else {
return new RemoteSocket(this.adapter, socket as SocketDetails); return new RemoteSocket(
this.adapter,
socket as SocketDetails<SocketData>
);
} }
}); });
}); });
@@ -246,26 +256,27 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/** /**
* Format of the data when the Socket instance exists on another Socket.IO server * Format of the data when the Socket instance exists on another Socket.IO server
*/ */
interface SocketDetails { interface SocketDetails<SocketData> {
id: SocketId; id: SocketId;
handshake: Handshake; handshake: Handshake;
rooms: Room[]; rooms: Room[];
data: any; data: SocketData;
} }
/** /**
* Expose of subset of the attributes and methods of the Socket class * Expose of subset of the attributes and methods of the Socket class
*/ */
export class RemoteSocket<EmitEvents extends EventsMap> export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents> { implements TypedEventBroadcaster<EmitEvents>
{
public readonly id: SocketId; public readonly id: SocketId;
public readonly handshake: Handshake; public readonly handshake: Handshake;
public readonly rooms: Set<Room>; 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.id = details.id;
this.handshake = details.handshake; this.handshake = details.handshake;
this.rooms = new Set(details.rooms); this.rooms = new Set(details.rooms);

View File

@@ -7,6 +7,7 @@ import type { Namespace } from "./namespace";
import type { EventsMap } from "./typed-events"; import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket"; import type { Socket } from "./socket";
import type { SocketId } from "socket.io-adapter"; import type { SocketId } from "socket.io-adapter";
import type { Socket as RawSocket } from "engine.io";
const debug = debugModule("socket.io:client"); const debug = debugModule("socket.io:client");
@@ -20,21 +21,27 @@ interface WriteOptions {
export class Client< export class Client<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
EmitEvents extends EventsMap, EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap ServerSideEvents extends EventsMap,
SocketData = any
> { > {
public readonly conn; public readonly conn: RawSocket;
private readonly id: string; private readonly id: string;
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>; private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
private readonly encoder: Encoder; private readonly encoder: Encoder;
private readonly decoder: Decoder; private readonly decoder: Decoder;
private sockets: Map< private sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private nsps: Map< private nsps: Map<
string, string,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private connectTimeout?: NodeJS.Timeout; private connectTimeout?: NodeJS.Timeout;
@@ -46,7 +53,7 @@ export class Client<
* @package * @package
*/ */
constructor( constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any conn: any
) { ) {
this.server = server; this.server = server;
@@ -111,11 +118,10 @@ export class Client<
auth, auth,
( (
dynamicNspName: dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents> | Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false | false
) => { ) => {
if (dynamicNspName) { if (dynamicNspName) {
debug("dynamic namespace %s was created", dynamicNspName);
this.doConnect(name, auth); this.doConnect(name, auth);
} else { } else {
debug("creation of namespace %s was denied", name); debug("creation of namespace %s was denied", name);
@@ -171,7 +177,9 @@ export class Client<
* *
* @private * @private
*/ */
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void { _remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name; const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id); this.sockets.delete(socket.id);
@@ -209,13 +217,11 @@ export class Client<
const encodedPackets = opts.preEncoded const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine() ? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet); : this.encoder.encode(packet as Packet);
for (const encodedPacket of encodedPackets) { this.writeToEngine(encodedPackets, opts);
this.writeToEngine(encodedPacket, opts);
}
} }
private writeToEngine( private writeToEngine(
encodedPacket: String | Buffer, encodedPackets: Array<String | Buffer>,
opts: WriteOptions opts: WriteOptions
): void { ): void {
if (opts.volatile && !this.conn.transport.writable) { if (opts.volatile && !this.conn.transport.writable) {
@@ -224,7 +230,12 @@ export class Client<
); );
return; return;
} }
this.conn.write(encodedPacket, opts); const packets = Array.isArray(encodedPackets)
? encodedPackets
: [encodedPackets];
for (const encodedPacket of packets) {
this.conn.write(encodedPacket, opts);
}
} }
/** /**

View File

@@ -4,7 +4,13 @@ import { createDeflate, createGzip, createBrotliCompress } from "zlib";
import accepts = require("accepts"); import accepts = require("accepts");
import { pipeline } from "stream"; import { pipeline } from "stream";
import path = require("path"); import path = require("path");
import engine = require("engine.io"); import {
attach,
Server as Engine,
ServerOptions as EngineOptions,
AttachOptions,
uServer,
} from "engine.io";
import { Client } from "./client"; import { Client } from "./client";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace"; import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace";
@@ -14,8 +20,6 @@ import * as parser from "socket.io-parser";
import type { Encoder } from "socket.io-parser"; import type { Encoder } from "socket.io-parser";
import debugModule from "debug"; import debugModule from "debug";
import { Socket } from "./socket"; import { Socket } from "./socket";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"; import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
import { import {
EventsMap, EventsMap,
@@ -24,13 +28,13 @@ import {
StrictEventEmitter, StrictEventEmitter,
EventNames, EventNames,
} from "./typed-events"; } from "./typed-events";
import { patchAdapter, restoreAdapter, serveFile } from "./uws.js";
const debug = debugModule("socket.io:server"); const debug = debugModule("socket.io:server");
const clientVersion = require("../package.json").version; const clientVersion = require("../package.json").version;
const dotMapRegex = /\.map/; const dotMapRegex = /\.map/;
type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = ( type ParentNspNameMatchFn = (
name: string, name: string,
auth: { [key: string]: any }, auth: { [key: string]: any },
@@ -39,107 +43,7 @@ type ParentNspNameMatchFn = (
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter); type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter);
interface EngineOptions { interface ServerOptions extends EngineOptions, AttachOptions {
/**
* how many ms without a pong packet to consider the connection closed
* @default 20000
*/
pingTimeout: number;
/**
* how many ms before sending a new ping packet
* @default 25000
*/
pingInterval: number;
/**
* how many ms before an uncompleted transport upgrade is cancelled
* @default 10000
*/
upgradeTimeout: number;
/**
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
* @default 1e5 (100 KB)
*/
maxHttpBufferSize: number;
/**
* A function that receives a given handshake or upgrade request as its first parameter,
* and can decide whether to continue or not. The second argument is a function that needs
* to be called with the decided information: fn(err, success), where success is a boolean
* value where false means that the request is rejected, and err is an error code.
*/
allowRequest: (
req: http.IncomingMessage,
fn: (err: string | null | undefined, success: boolean) => void
) => void;
/**
* the low-level transports that are enabled
* @default ["polling", "websocket"]
*/
transports: Transport[];
/**
* whether to allow transport upgrades
* @default true
*/
allowUpgrades: boolean;
/**
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
* @default false
*/
perMessageDeflate: boolean | object;
/**
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
* @default true
*/
httpCompression: boolean | object;
/**
* what WebSocket server implementation to use. Specified module must
* conform to the ws interface (see ws module api docs).
* An alternative c++ addon is also available by installing eiows module.
*
* @default `require("ws").Server`
*/
wsEngine: Function;
/**
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
*/
initialPacket: any;
/**
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
* might be used for sticky-session. Defaults to not sending any cookie.
* @default false
*/
cookie: CookieSerializeOptions | boolean;
/**
* the options that will be forwarded to the cors module
*/
cors: CorsOptions;
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3: boolean;
}
interface AttachOptions {
/**
* name of the path to capture
* @default "/engine.io"
*/
path: string;
/**
* destroy unhandled upgrade requests
* @default true
*/
destroyUpgrade: boolean;
/**
* milliseconds after which unhandled requests are ended
* @default 1000
*/
destroyUpgradeTimeout: number;
}
interface EngineAttachOptions extends EngineOptions, AttachOptions {}
interface ServerOptions extends EngineAttachOptions {
/** /**
* name of the path to capture * name of the path to capture
* @default "/socket.io" * @default "/socket.io"
@@ -170,16 +74,23 @@ interface ServerOptions extends EngineAttachOptions {
export class Server< export class Server<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter< > extends StrictEventEmitter<
ServerSideEvents, ServerSideEvents,
EmitEvents, EmitEvents,
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents> ServerReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> { > {
public readonly sockets: Namespace< public readonly sockets: Namespace<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents ServerSideEvents,
SocketData
>; >;
/** /**
* A reference to the underlying Engine.IO server. * A reference to the underlying Engine.IO server.
@@ -203,16 +114,16 @@ export class Server<
*/ */
_nsps: Map< _nsps: Map<
string, string,
Namespace<ListenEvents, EmitEvents, ServerSideEvents> Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private parentNsps: Map< private parentNsps: Map<
ParentNspNameMatchFn, ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents> ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
private _adapter?: AdapterConstructor; private _adapter?: AdapterConstructor;
private _serveClient: boolean; private _serveClient: boolean;
private opts: Partial<EngineOptions>; private opts: Partial<EngineOptions>;
private eio; private eio: Engine;
private _path: string; private _path: string;
private clientPathRegex: RegExp; private clientPathRegex: RegExp;
@@ -256,7 +167,7 @@ export class Server<
this.adapter(opts.adapter || Adapter); this.adapter(opts.adapter || Adapter);
this.sockets = this.of("/"); this.sockets = this.of("/");
this.opts = opts; this.opts = opts;
if (srv) this.attach(srv as http.Server); if (srv || typeof srv == "number") this.attach(srv as http.Server | number);
} }
/** /**
@@ -288,7 +199,9 @@ export class Server<
name: string, name: string,
auth: { [key: string]: any }, auth: { [key: string]: any },
fn: ( fn: (
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false nsp:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false
) => void ) => void
): void { ): void {
if (this.parentNsps.size === 0) return fn(false); if (this.parentNsps.size === 0) return fn(false);
@@ -302,15 +215,18 @@ export class Server<
} }
nextFn.value(name, auth, (err, allow) => { nextFn.value(name, auth, (err, allow) => {
if (err || !allow) { if (err || !allow) {
run(); return run();
} else {
const namespace = this.parentNsps
.get(nextFn.value)!
.createChild(name);
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace);
fn(namespace);
} }
if (this._nsps.has(name)) {
// the namespace was created in the meantime
debug("dynamic namespace %s already exists", name);
return fn(this._nsps.get(name) as Namespace);
}
const namespace = this.parentNsps.get(nextFn.value)!.createChild(name);
debug("dynamic namespace %s was created", name);
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace);
fn(namespace);
}); });
}; };
@@ -336,7 +252,7 @@ export class Server<
this.clientPathRegex = new RegExp( this.clientPathRegex = new RegExp(
"^" + "^" +
escapedPath + escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$" "/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)"
); );
return this; return this;
} }
@@ -434,6 +350,69 @@ export class Server<
return this; return this;
} }
public attachApp(app /*: TemplatedApp */, opts: Partial<ServerOptions> = {}) {
// merge the options passed to the Socket.IO server
Object.assign(opts, this.opts);
// set engine.io path to `/socket.io`
opts.path = opts.path || this._path;
// initialize engine
debug("creating uWebSockets.js-based engine with opts %j", opts);
const engine = new uServer(opts);
engine.attach(app, opts);
// bind to engine events
this.bind(engine);
if (this._serveClient) {
// attach static file serving
app.get(`${this._path}/*`, (res, req) => {
if (!this.clientPathRegex.test(req.getUrl())) {
req.setYield(true);
return;
}
const filename = req
.getUrl()
.replace(this._path, "")
.replace(/\?.*$/, "")
.replace(/^\//, "");
const isMap = dotMapRegex.test(filename);
const type = isMap ? "map" : "source";
// Per the standard, ETags must be quoted:
// https://tools.ietf.org/html/rfc7232#section-2.3
const expectedEtag = '"' + clientVersion + '"';
const weakEtag = "W/" + expectedEtag;
const etag = req.getHeader("if-none-match");
if (etag) {
if (expectedEtag === etag || weakEtag === etag) {
debug("serve client %s 304", type);
res.writeStatus("304 Not Modified");
res.end();
return;
}
}
debug("serve client %s", type);
res.writeHeader("cache-control", "public, max-age=0");
res.writeHeader(
"content-type",
"application/" + (isMap ? "json" : "javascript")
);
res.writeHeader("etag", expectedEtag);
const filepath = path.join(__dirname, "../client-dist/", filename);
serveFile(res, filepath);
});
}
patchAdapter(app);
}
/** /**
* Initialize engine * Initialize engine
* *
@@ -443,11 +422,11 @@ export class Server<
*/ */
private initEngine( private initEngine(
srv: http.Server, srv: http.Server,
opts: Partial<EngineAttachOptions> opts: EngineOptions & AttachOptions
): void { ): void {
// initialize engine // initialize engine
debug("creating engine.io instance with opts %j", opts); debug("creating engine.io instance with opts %j", opts);
this.eio = engine.attach(srv, opts); this.eio = attach(srv, opts);
// attach static file serving // attach static file serving
if (this._serveClient) this.attachServe(srv); if (this._serveClient) this.attachServe(srv);
@@ -471,7 +450,7 @@ export class Server<
const evs = srv.listeners("request").slice(0); const evs = srv.listeners("request").slice(0);
srv.removeAllListeners("request"); srv.removeAllListeners("request");
srv.on("request", (req, res) => { srv.on("request", (req, res) => {
if (this.clientPathRegex.test(req.url)) { if (this.clientPathRegex.test(req.url!)) {
this.serve(req, res); this.serve(req, res);
} else { } else {
for (let i = 0; i < evs.length; i++) { for (let i = 0; i < evs.length; i++) {
@@ -489,7 +468,7 @@ export class Server<
* @private * @private
*/ */
private serve(req: http.IncomingMessage, res: http.ServerResponse): void { private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
const filename = req.url!.replace(this._path, ""); const filename = req.url!.replace(this._path, "").replace(/\?.*$/, "");
const isMap = dotMapRegex.test(filename); const isMap = dotMapRegex.test(filename);
const type = isMap ? "map" : "source"; const type = isMap ? "map" : "source";
@@ -517,9 +496,6 @@ export class Server<
); );
res.setHeader("ETag", expectedEtag); res.setHeader("ETag", expectedEtag);
if (!isMap) {
res.setHeader("X-SourceMap", filename.substring(1) + ".map");
}
Server.sendFile(filename, req, res); Server.sendFile(filename, req, res);
} }
@@ -604,8 +580,10 @@ export class Server<
*/ */
public of( public of(
name: string | RegExp | ParentNspNameMatchFn, name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void fn?: (
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> { socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
if (typeof name === "function" || name instanceof RegExp) { if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this); const parentNsp = new ParentNamespace(this);
debug("initializing parent namespace %s", parentNsp.name); debug("initializing parent namespace %s", parentNsp.name);
@@ -653,6 +631,9 @@ export class Server<
this.engine.close(); this.engine.close();
// restore the Adapter prototype
restoreAdapter();
if (this.httpServer) { if (this.httpServer) {
this.httpServer.close(fn); this.httpServer.close(fn);
} else { } else {
@@ -668,7 +649,7 @@ export class Server<
*/ */
public use( public use(
fn: ( fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
): this { ): this {
@@ -683,7 +664,7 @@ export class Server<
* @return self * @return self
* @public * @public
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.to(room); return this.sockets.to(room);
} }
@@ -694,7 +675,7 @@ export class Server<
* @return self * @return self
* @public * @public
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.in(room); return this.sockets.in(room);
} }
@@ -707,9 +688,8 @@ export class Server<
*/ */
public except( public except(
name: Room | Room[] name: Room | Room[]
): Server<ListenEvents, EmitEvents, ServerSideEvents> { ): BroadcastOperator<EmitEvents, SocketData> {
this.sockets.except(name); return this.sockets.except(name);
return this;
} }
/** /**
@@ -764,7 +744,9 @@ export class Server<
* @return self * @return self
* @public * @public
*/ */
public compress(compress: boolean): BroadcastOperator<EmitEvents> { public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.compress(compress); return this.sockets.compress(compress);
} }
@@ -776,7 +758,7 @@ export class Server<
* @return self * @return self
* @public * @public
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.volatile; return this.sockets.volatile;
} }
@@ -786,7 +768,7 @@ export class Server<
* @return self * @return self
* @public * @public
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local(): BroadcastOperator<EmitEvents, SocketData> {
return this.sockets.local; return this.sockets.local;
} }
@@ -795,7 +777,7 @@ export class Server<
* *
* @public * @public
*/ */
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> { public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.sockets.fetchSockets(); return this.sockets.fetchSockets();
} }
@@ -852,3 +834,4 @@ module.exports.Namespace = Namespace;
module.exports.Socket = Socket; module.exports.Socket = Socket;
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket }; export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };
export { Event } from "./socket";

View File

@@ -21,56 +21,72 @@ export interface ExtendedError extends Error {
export interface NamespaceReservedEventsMap< export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap, ListenEvents extends EventsMap,
EmitEvents extends EventsMap, EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap ServerSideEvents extends EventsMap,
SocketData
> { > {
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void; connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void;
connection: ( connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents> socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void; ) => void;
} }
export interface ServerReservedEventsMap< export interface ServerReservedEventsMap<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents ServerSideEvents,
SocketData
> extends NamespaceReservedEventsMap< > extends NamespaceReservedEventsMap<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
ServerSideEvents ServerSideEvents,
SocketData
> { > {
new_namespace: ( new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents> namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void; ) => void;
} }
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set< export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never> keyof ServerReservedEventsMap<never, never, never, never>
>(<const>["connect", "connection", "new_namespace"]); >(<const>["connect", "connection", "new_namespace"]);
export class Namespace< export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter< > extends StrictEventEmitter<
ServerSideEvents, ServerSideEvents,
EmitEvents, EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents> NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> { > {
public readonly name: string; public readonly name: string;
public readonly sockets: Map< public readonly sockets: Map<
SocketId, SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents> Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map(); > = new Map();
public adapter: Adapter; public adapter: Adapter;
/** @private */ /** @private */
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>; readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
/** @private */ /** @private */
_fns: Array< _fns: Array<
( (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
> = []; > = [];
@@ -85,7 +101,7 @@ export class Namespace<
* @param name * @param name
*/ */
constructor( constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>, server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string name: string
) { ) {
super(); super();
@@ -114,7 +130,7 @@ export class Namespace<
*/ */
public use( public use(
fn: ( fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void next: (err?: ExtendedError) => void
) => void ) => void
): this { ): this {
@@ -130,7 +146,7 @@ export class Namespace<
* @private * @private
*/ */
private run( private run(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>, socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void fn: (err: ExtendedError | null) => void
) { ) {
const fns = this._fns.slice(0); const fns = this._fns.slice(0);
@@ -159,7 +175,7 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).to(room); return new BroadcastOperator(this.adapter).to(room);
} }
@@ -170,7 +186,7 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).in(room); return new BroadcastOperator(this.adapter).in(room);
} }
@@ -181,7 +197,9 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).except(room); return new BroadcastOperator(this.adapter).except(room);
} }
@@ -195,7 +213,7 @@ export class Namespace<
client: Client<ListenEvents, EmitEvents, ServerSideEvents>, client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query, query,
fn?: () => void fn?: () => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents> { ): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
debug("adding socket to nsp %s", this.name); debug("adding socket to nsp %s", this.name);
const socket = new Socket(this, client, query); const socket = new Socket(this, client, query);
this.run(socket, (err) => { this.run(socket, (err) => {
@@ -238,7 +256,9 @@ export class Namespace<
* *
* @private * @private
*/ */
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void { _remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) { if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id); this.sockets.delete(socket.id);
} else { } else {
@@ -256,7 +276,10 @@ export class Namespace<
ev: Ev, ev: Ev,
...args: EventParams<EmitEvents, Ev> ...args: EventParams<EmitEvents, Ev>
): boolean { ): boolean {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args); return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit(
ev,
...args
);
} }
/** /**
@@ -328,7 +351,9 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public compress(compress: boolean): BroadcastOperator<EmitEvents> { public compress(
compress: boolean
): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).compress(compress); return new BroadcastOperator(this.adapter).compress(compress);
} }
@@ -340,7 +365,7 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public get volatile(): BroadcastOperator<EmitEvents> { public get volatile(): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).volatile; return new BroadcastOperator(this.adapter).volatile;
} }
@@ -350,7 +375,7 @@ export class Namespace<
* @return self * @return self
* @public * @public
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local(): BroadcastOperator<EmitEvents, SocketData> {
return new BroadcastOperator(this.adapter).local; return new BroadcastOperator(this.adapter).local;
} }
@@ -359,7 +384,7 @@ export class Namespace<
* *
* @public * @public
*/ */
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> { public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return new BroadcastOperator(this.adapter).fetchSockets(); return new BroadcastOperator(this.adapter).fetchSockets();
} }

View File

@@ -1,5 +1,5 @@
import { Namespace } from "./namespace"; import { Namespace } from "./namespace";
import type { Server } from "./index"; import type { Server, RemoteSocket } from "./index";
import type { import type {
EventParams, EventParams,
EventNames, EventNames,
@@ -11,14 +11,17 @@ import type { BroadcastOptions } from "socket.io-adapter";
export class ParentNamespace< export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents> { SocketData = any
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
private static count: number = 0; private static count: number = 0;
private children: Set< private children: Set<
Namespace<ListenEvents, EmitEvents, ServerSideEvents> Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Set(); > = new Set();
constructor(server: Server<ListenEvents, EmitEvents, ServerSideEvents>) { constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) {
super(server, "/_" + ParentNamespace.count++); super(server, "/_" + ParentNamespace.count++);
} }
@@ -48,7 +51,7 @@ export class ParentNamespace<
createChild( createChild(
name: string name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> { ): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name); const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0); namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) => this.listeners("connect").forEach((listener) =>
@@ -61,4 +64,13 @@ export class ParentNamespace<
this.server._nsps.set(name, namespace); this.server._nsps.set(name, namespace);
return 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");
}
} }

View File

@@ -1,5 +1,4 @@
import { Packet, PacketType } from "socket.io-parser"; import { Packet, PacketType } from "socket.io-parser";
import url = require("url");
import debugModule from "debug"; import debugModule from "debug";
import type { Server } from "./index"; import type { Server } from "./index";
import { import {
@@ -46,7 +45,7 @@ export interface EventEmitterReservedEventsMap {
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set< export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents | ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never, never> | keyof NamespaceReservedEventsMap<never, never, never, never>
| keyof SocketReservedEventsMap | keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap | keyof EventEmitterReservedEventsMap
>(<const>[ >(<const>[
@@ -108,10 +107,16 @@ export interface Handshake {
auth: { [key: string]: any }; auth: { [key: string]: any };
} }
/**
* `[eventName, ...args]`
*/
export type Event = [string, ...any[]];
export class Socket< export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap, ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents, EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter< > extends StrictEventEmitter<
ListenEvents, ListenEvents,
EmitEvents, EmitEvents,
@@ -122,18 +127,20 @@ export class Socket<
/** /**
* 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 fetchSockets method
*/ */
public data: any = {}; public data: Partial<SocketData> = {};
public connected: boolean; public connected: boolean = false;
public disconnected: boolean;
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>; private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
private readonly adapter: Adapter; private readonly adapter: Adapter;
private acks: Map<number, () => void> = new Map(); private acks: Map<number, () => void> = new Map();
private fns: Array< private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
(event: Array<any>, next: (err?: Error) => void) => void private flags: BroadcastFlags & { timeout?: number } = {};
> = [];
private flags: BroadcastFlags = {};
private _anyListeners?: Array<(...args: any[]) => void>; private _anyListeners?: Array<(...args: any[]) => void>;
/** /**
@@ -158,8 +165,6 @@ export class Socket<
} else { } else {
this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information this.id = base64id.generateId(); // don't reuse the Engine.IO id because it's sensitive information
} }
this.connected = true;
this.disconnected = false;
this.handshake = this.buildHandshake(auth); this.handshake = this.buildHandshake(auth);
} }
@@ -178,7 +183,8 @@ export class Socket<
secure: !!this.request.connection.encrypted, secure: !!this.request.connection.encrypted,
issued: +new Date(), issued: +new Date(),
url: this.request.url!, url: this.request.url!,
query: url.parse(this.request.url!, true).query, // @ts-ignore
query: this.request._query,
auth, auth,
}; };
} }
@@ -204,9 +210,11 @@ export class Socket<
// access last argument to see if it's an ACK callback // access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") { if (typeof data[data.length - 1] === "function") {
debug("emitting packet with ack id %d", this.nsp._ids); const id = this.nsp._ids++;
this.acks.set(this.nsp._ids, data.pop()); debug("emitting packet with ack id %d", id);
packet.id = this.nsp._ids++;
this.registerAckCallback(id, data.pop());
packet.id = id;
} }
const flags = Object.assign({}, this.flags); const flags = Object.assign({}, this.flags);
@@ -217,6 +225,28 @@ export class Socket<
return true; return true;
} }
/**
* @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. * Targets a room when broadcasting.
* *
@@ -224,7 +254,7 @@ export class Socket<
* @return self * @return self
* @public * @public
*/ */
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> { public to(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return this.newBroadcastOperator().to(room); return this.newBroadcastOperator().to(room);
} }
@@ -235,7 +265,7 @@ export class Socket<
* @return self * @return self
* @public * @public
*/ */
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> { public in(room: Room | Room[]): BroadcastOperator<EmitEvents, SocketData> {
return this.newBroadcastOperator().in(room); return this.newBroadcastOperator().in(room);
} }
@@ -246,7 +276,9 @@ export class Socket<
* @return self * @return self
* @public * @public
*/ */
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> { public except(
room: Room | Room[]
): BroadcastOperator<EmitEvents, SocketData> {
return this.newBroadcastOperator().except(room); return this.newBroadcastOperator().except(room);
} }
@@ -336,6 +368,7 @@ export class Socket<
*/ */
_onconnect(): void { _onconnect(): void {
debug("socket connected - writing packet"); debug("socket connected - writing packet");
this.connected = true;
this.join(this.id); this.join(this.id);
if (this.conn.protocol === 3) { if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT }); this.packet({ type: PacketType.CONNECT });
@@ -483,7 +516,6 @@ export class Socket<
this.nsp._remove(this); this.nsp._remove(this);
this.client._remove(this); this.client._remove(this);
this.connected = false; this.connected = false;
this.disconnected = true;
this.emitReserved("disconnect", reason); this.emitReserved("disconnect", reason);
return; return;
} }
@@ -550,7 +582,7 @@ export class Socket<
* @return {Socket} self * @return {Socket} self
* @public * @public
*/ */
public get broadcast(): BroadcastOperator<EmitEvents> { public get broadcast(): BroadcastOperator<EmitEvents, SocketData> {
return this.newBroadcastOperator(); return this.newBroadcastOperator();
} }
@@ -560,17 +592,37 @@ export class Socket<
* @return {Socket} self * @return {Socket} self
* @public * @public
*/ */
public get local(): BroadcastOperator<EmitEvents> { public get local(): BroadcastOperator<EmitEvents, SocketData> {
return this.newBroadcastOperator().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:
*
* ```
* socket.timeout(5000).emit("my-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* ```
*
* @returns self
* @public
*/
public timeout(timeout: number): this {
this.flags.timeout = timeout;
return this;
}
/** /**
* Dispatch incoming event to socket listeners. * Dispatch incoming event to socket listeners.
* *
* @param {Array} event - event that will get emitted * @param {Array} event - event that will get emitted
* @private * @private
*/ */
private dispatch(event: [eventName: string, ...args: any[]]): void { private dispatch(event: Event): void {
debug("dispatching an event %j", event); debug("dispatching an event %j", event);
this.run(event, (err) => { this.run(event, (err) => {
process.nextTick(() => { process.nextTick(() => {
@@ -593,9 +645,7 @@ export class Socket<
* @return {Socket} self * @return {Socket} self
* @public * @public
*/ */
public use( public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
this.fns.push(fn); this.fns.push(fn);
return this; return this;
} }
@@ -607,10 +657,7 @@ export class Socket<
* @param {Function} fn - last fn call in the middleware * @param {Function} fn - last fn call in the middleware
* @private * @private
*/ */
private run( private run(event: Event, fn: (err: Error | null) => void): void {
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
const fns = this.fns.slice(0); const fns = this.fns.slice(0);
if (!fns.length) return fn(null); if (!fns.length) return fn(null);
@@ -630,6 +677,13 @@ export class Socket<
run(0); 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. * A reference to the request that originated the underlying Engine.IO Socket.
* *
@@ -715,7 +769,7 @@ export class Socket<
return this._anyListeners || []; return this._anyListeners || [];
} }
private newBroadcastOperator(): BroadcastOperator<EmitEvents> { private newBroadcastOperator(): BroadcastOperator<EmitEvents, SocketData> {
const flags = Object.assign({}, this.flags); const flags = Object.assign({}, this.flags);
this.flags = {}; this.flags = {};
return new BroadcastOperator( return new BroadcastOperator(

View File

@@ -58,7 +58,7 @@ export type ReservedOrUserListener<
* Needed because of https://github.com/microsoft/TypeScript/issues/41778 * Needed because of https://github.com/microsoft/TypeScript/issues/41778
*/ */
type FallbackToUntypedListener<T> = [T] extends [never] type FallbackToUntypedListener<T> = [T] extends [never]
? (...args: any[]) => void ? (...args: any[]) => void | Promise<void>
: T; : T;
/** /**
@@ -91,7 +91,8 @@ export abstract class StrictEventEmitter<
ReservedEvents extends EventsMap = {} ReservedEvents extends EventsMap = {}
> >
extends EventEmitter extends EventEmitter
implements TypedEventBroadcaster<EmitEvents> { implements TypedEventBroadcaster<EmitEvents>
{
/** /**
* Adds the `listener` function as an event listener for `ev`. * Adds the `listener` function as an event listener for `ev`.
* *

162
lib/uws.ts Normal file
View File

@@ -0,0 +1,162 @@
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);
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);
}

1999
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "socket.io", "name": "socket.io",
"version": "4.1.2", "version": "4.4.1",
"description": "node.js realtime framework server", "description": "node.js realtime framework server",
"keywords": [ "keywords": [
"realtime", "realtime",
@@ -27,7 +27,8 @@
"main": "./dist/index.js", "main": "./dist/index.js",
"exports": { "exports": {
"import": "./wrapper.mjs", "import": "./wrapper.mjs",
"require": "./dist/index.js" "require": "./dist/index.js",
"types": "./dist/index.d.ts"
}, },
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"license": "MIT", "license": "MIT",
@@ -45,33 +46,28 @@
"prepack": "npm run compile" "prepack": "npm run compile"
}, },
"dependencies": { "dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4", "accepts": "~1.3.4",
"base64id": "~2.0.0", "base64id": "~2.0.0",
"debug": "~4.3.1", "debug": "~4.3.2",
"engine.io": "~5.1.0", "engine.io": "~6.1.2",
"socket.io-adapter": "~2.3.0", "socket.io-adapter": "~2.3.3",
"socket.io-parser": "~4.0.3" "socket.io-parser": "~4.0.4"
}, },
"devDependencies": { "devDependencies": {
"@types/mocha": "^8.0.4", "@types/mocha": "^9.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^7.14.0",
"eslint-config-prettier": "^6.11.0",
"expect.js": "0.3.1", "expect.js": "0.3.1",
"mocha": "^3.5.3", "mocha": "^3.5.3",
"nyc": "^15.1.0", "nyc": "^15.1.0",
"prettier": "^2.2.0", "prettier": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"socket.io-client": "4.1.2", "socket.io-client": "4.4.1",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0", "socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^6.1.0", "superagent": "^6.1.0",
"supertest": "^6.0.1", "supertest": "^6.1.6",
"ts-node": "^9.0.0", "ts-node": "^10.2.1",
"tsd": "^0.14.0", "tsd": "^0.17.0",
"typescript": "^4.1.2" "typescript": "^4.4.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.0.0"
}, },
"contributors": [ "contributors": [
{ {

View File

@@ -3,7 +3,7 @@ const ioc = require("socket.io-client");
const io = require("../..")(server); const io = require("../..")(server);
const srv = server.listen(() => { const srv = server.listen(() => {
const socket = ioc("ws://localhost:" + server.address().port); const socket = ioc.connect("ws://localhost:" + server.address().port);
socket.on("connect", () => { socket.on("connect", () => {
io.close(); io.close();
socket.close(); socket.close();

57
test/socket-timeout.ts Normal file
View File

@@ -0,0 +1,57 @@
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);
});
});
});
});

View File

@@ -14,6 +14,9 @@ import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import "./support/util"; import "./support/util";
import "./utility-methods"; import "./utility-methods";
import "./uws";
type callback = (err: Error | null, success: boolean) => void;
// Creates a socket.io client for the given server // Creates a socket.io client for the given server
function client(srv, nsp?: string | object, opts?: object): ClientSocket { function client(srv, nsp?: string | object, opts?: object): ClientSocket {
@@ -39,6 +42,11 @@ const waitFor = (emitter, event) => {
}); });
}; };
const getPort = (io: Server): number => {
// @ts-ignore
return io.httpServer.address().port;
};
describe("socket.io", () => { describe("socket.io", () => {
it("should be the same version as client", () => { it("should be the same version as client", () => {
const version = require("../package").version; const version = require("../package").version;
@@ -59,7 +67,7 @@ describe("socket.io", () => {
if (err) return done(err); if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/javascript"); expect(res.headers["content-type"]).to.be("application/javascript");
expect(res.headers.etag).to.be('"' + clientVersion + '"'); expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.headers["x-sourcemap"]).to.be(filename + ".map"); expect(res.headers["x-sourcemap"]).to.be(undefined);
expect(res.text).to.match(/engine\.io/); expect(res.text).to.match(/engine\.io/);
expect(res.status).to.be(200); expect(res.status).to.be(200);
done(); done();
@@ -83,6 +91,10 @@ describe("socket.io", () => {
}; };
it("should serve client", testSource("socket.io.js")); 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 source map", testSourceMap("socket.io.js.map"));
it("should serve client (min)", testSource("socket.io.min.js")); it("should serve client (min)", testSource("socket.io.min.js"));
@@ -116,6 +128,13 @@ describe("socket.io", () => {
testSourceMap("socket.io.msgpack.min.js.map") 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) => { it("should handle 304", (done) => {
const srv = createServer(); const srv = createServer();
new Server(srv); new Server(srv);
@@ -185,29 +204,17 @@ describe("socket.io", () => {
describe("port", () => { describe("port", () => {
it("should be bound", (done) => { it("should be bound", (done) => {
const sockets = new Server(54010); const io = new Server(0);
request("http://localhost:54010")
.get("/socket.io/socket.io.js")
.expect(200, done);
});
it("should be bound as a string", (done) => { request(`http://localhost:${getPort(io)}`)
const sockets = new Server(54020);
request("http://localhost:54020")
.get("/socket.io/socket.io.js") .get("/socket.io/socket.io.js")
.expect(200, done); .expect(200, done);
}); });
it("with listen", (done) => { it("with listen", (done) => {
const sockets = new Server().listen(54011); const io = new Server().listen(0);
request("http://localhost:54011")
.get("/socket.io/socket.io.js")
.expect(200, done);
});
it("as a string", (done) => { request(`http://localhost:${getPort(io)}`)
const sockets = new Server().listen(54012);
request("http://localhost:54012")
.get("/socket.io/socket.io.js") .get("/socket.io/socket.io.js")
.expect(200, done); .expect(200, done);
}); });
@@ -218,7 +225,7 @@ describe("socket.io", () => {
const request = require("superagent"); const request = require("superagent");
it("should send the Access-Control-Allow-xxx headers on OPTIONS request", (done) => { it("should send the Access-Control-Allow-xxx headers on OPTIONS request", (done) => {
const sockets = new Server(54013, { const io = new Server(0, {
cors: { cors: {
origin: "http://localhost:54023", origin: "http://localhost:54023",
methods: ["GET", "POST"], methods: ["GET", "POST"],
@@ -227,7 +234,7 @@ describe("socket.io", () => {
}, },
}); });
request request
.options("http://localhost:54013/socket.io/default/") .options(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 }) .query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54023") .set("Origin", "http://localhost:54023")
.end((err, res) => { .end((err, res) => {
@@ -246,7 +253,7 @@ describe("socket.io", () => {
}); });
it("should send the Access-Control-Allow-xxx headers on GET request", (done) => { it("should send the Access-Control-Allow-xxx headers on GET request", (done) => {
const sockets = new Server(54014, { const io = new Server(0, {
cors: { cors: {
origin: "http://localhost:54024", origin: "http://localhost:54024",
methods: ["GET", "POST"], methods: ["GET", "POST"],
@@ -255,7 +262,7 @@ describe("socket.io", () => {
}, },
}); });
request request
.get("http://localhost:54014/socket.io/default/") .get(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 }) .query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54024") .set("Origin", "http://localhost:54024")
.end((err, res) => { .end((err, res) => {
@@ -270,12 +277,12 @@ describe("socket.io", () => {
}); });
it("should allow request if custom function in opts.allowRequest returns true", (done) => { it("should allow request if custom function in opts.allowRequest returns true", (done) => {
const sockets = new Server(createServer().listen(54022), { const io = new Server(0, {
allowRequest: (req, callback) => callback(null, true), allowRequest: (req, callback) => callback(null, true),
}); });
request request
.get("http://localhost:54022/socket.io/default/") .get(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 }) .query({ transport: "polling", EIO: 4 })
.end((err, res) => { .end((err, res) => {
expect(res.status).to.be(200); expect(res.status).to.be(200);
@@ -284,11 +291,11 @@ describe("socket.io", () => {
}); });
it("should disallow request if custom function in opts.allowRequest returns false", (done) => { it("should disallow request if custom function in opts.allowRequest returns false", (done) => {
const sockets = new Server(createServer().listen(54023), { const io = new Server(0, {
allowRequest: (req, callback) => callback(null, false), allowRequest: (req, callback) => callback(null, false),
}); });
request request
.get("http://localhost:54023/socket.io/default/") .get(`http://localhost:${getPort(io)}/socket.io/default/`)
.set("origin", "http://foo.example") .set("origin", "http://foo.example")
.query({ transport: "polling", EIO: 4 }) .query({ transport: "polling", EIO: 4 })
.end((err, res) => { .end((err, res) => {
@@ -300,22 +307,22 @@ describe("socket.io", () => {
describe("close", () => { describe("close", () => {
it("should be able to close sio sending a srv", (done) => { it("should be able to close sio sending a srv", (done) => {
const PORT = 54018; const httpServer = createServer().listen(0);
const srv = createServer().listen(PORT); const io = new Server(httpServer);
const sio = new Server(srv); const port = getPort(io);
const net = require("net"); const net = require("net");
const server = net.createServer(); const server = net.createServer();
const clientSocket = client(srv, { reconnection: false }); const clientSocket = client(httpServer, { reconnection: false });
clientSocket.on("disconnect", () => { clientSocket.on("disconnect", () => {
expect(sio.sockets.sockets.size).to.equal(0); expect(io.sockets.sockets.size).to.equal(0);
server.listen(PORT); server.listen(port);
}); });
clientSocket.on("connect", () => { clientSocket.on("connect", () => {
expect(sio.sockets.sockets.size).to.equal(1); expect(io.sockets.sockets.size).to.equal(1);
sio.close(); io.close();
}); });
server.once("listening", () => { server.once("listening", () => {
@@ -327,30 +334,31 @@ describe("socket.io", () => {
}); });
}); });
it("should be able to close sio sending a port", () => { it("should be able to close sio sending a srv", (done) => {
const PORT = 54019; const io = new Server(0);
const sio = new Server(PORT); const port = getPort(io);
const net = require("net"); const net = require("net");
const server = net.createServer(); const server = net.createServer();
const clientSocket = ioc("ws://0.0.0.0:" + PORT, { const clientSocket = ioc("ws://0.0.0.0:" + port, {
reconnection: false, reconnection: false,
}); });
clientSocket.on("disconnect", () => { clientSocket.on("disconnect", () => {
expect(Object.keys(sio._nsps["/"].sockets).length).to.equal(0); expect(io.sockets.sockets.size).to.equal(0);
server.listen(PORT); server.listen(port);
}); });
clientSocket.on("connect", () => { clientSocket.on("connect", () => {
expect(Object.keys(sio._nsps["/"].sockets).length).to.equal(1); expect(io.sockets.sockets.size).to.equal(1);
sio.close(); io.close();
}); });
server.once("listening", () => { server.once("listening", () => {
// PORT should be free // PORT should be free
server.close((error) => { server.close((error) => {
expect(error).to.be(undefined); expect(error).to.be(undefined);
done();
}); });
}); });
}); });
@@ -802,6 +810,7 @@ describe("socket.io", () => {
srv.listen(() => { srv.listen(() => {
const socket = client(srv); const socket = client(srv);
// @ts-ignore
socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
socket.on("disconnect", () => { socket.on("disconnect", () => {
@@ -812,30 +821,28 @@ describe("socket.io", () => {
}); });
}); });
it("should close a client without namespace (2)", (done) => { it("should exclude a specific socket when emitting", (done) => {
const srv = createServer(); const srv = createServer();
const sio = new Server(srv, { const io = new Server(srv);
connectTimeout: 100,
});
sio.use((_, next) => {
next(new Error("nope"));
});
srv.listen(() => { srv.listen(() => {
const socket = client(srv); const socket1 = client(srv, "/");
const socket2 = client(srv, "/");
const success = () => { socket2.on("a", () => {
socket.close(); done(new Error("should not happen"));
sio.close(); });
socket1.on("a", () => {
done(); done();
}; });
socket.on("disconnect", success); socket2.on("connect", () => {
io.except(socket2.id).emit("a");
});
}); });
}); });
it("should exclude a specific socket when emitting", (done) => { it("should exclude a specific socket when emitting (in a namespace)", (done) => {
const srv = createServer(); const srv = createServer();
const sio = new Server(srv); const sio = new Server(srv);
@@ -971,6 +978,46 @@ describe("socket.io", () => {
const socket = client(srv, "/dynamic-101"); const socket = client(srv, "/dynamic-101");
}); });
}); });
it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
const srv = createServer();
const sio = new Server(srv);
const counters = {
connected: 0,
created: 0,
events: 0,
};
const buffer: callback[] = [];
sio.on("new_namespace", (namespace) => {
counters.created++;
});
srv.listen(() => {
const handler = () => {
if (++counters.events === 2) {
expect(counters.created).to.equal(1);
done();
}
};
sio
.of((name, query, next) => {
buffer.push(next);
if (buffer.length === 2) {
buffer.forEach((next) => next(null, true));
}
})
.on("connection", (socket) => {
if (++counters.connected === 2) {
sio.of("/dynamic-101").emit("message");
}
});
let one = client(srv, "/dynamic-101");
let two = client(srv, "/dynamic-101");
one.on("message", handler);
two.on("message", handler);
});
});
}); });
}); });
@@ -1003,7 +1050,7 @@ describe("socket.io", () => {
reconnectionDelay: 100, reconnectionDelay: 100,
}); });
clientSocket.on("connect", () => { clientSocket.on("connect", () => {
srv.close(); sio.close();
}); });
clientSocket.io.on("reconnect_failed", () => { clientSocket.io.on("reconnect_failed", () => {
@@ -1357,6 +1404,58 @@ describe("socket.io", () => {
}, 200); }, 200);
}); });
it("should emit only one consecutive volatile event with binary (ws)", (done) => {
const srv = createServer();
const sio = new Server(srv, { transports: ["websocket"] });
let counter = 0;
srv.listen(() => {
sio.on("connection", (s) => {
// Wait to make sure there are no packets being sent for opening the connection
setTimeout(() => {
s.volatile.emit("ev", Buffer.from([1, 2, 3]));
s.volatile.emit("ev", Buffer.from([4, 5, 6]));
}, 20);
});
const socket = client(srv, { transports: ["websocket"] });
socket.on("ev", () => {
counter++;
});
});
setTimeout(() => {
expect(counter).to.be(1);
done();
}, 200);
});
it("should broadcast only one consecutive volatile event with binary (ws)", (done) => {
const srv = createServer();
const sio = new Server(srv, { transports: ["websocket"] });
let counter = 0;
srv.listen(() => {
sio.on("connection", (s) => {
// Wait to make sure there are no packets being sent for opening the connection
setTimeout(() => {
sio.volatile.emit("ev", Buffer.from([1, 2, 3]));
sio.volatile.emit("ev", Buffer.from([4, 5, 6]));
}, 20);
});
const socket = client(srv, { transports: ["websocket"] });
socket.on("ev", () => {
counter++;
});
});
setTimeout(() => {
expect(counter).to.be(1);
done();
}, 200);
});
it("should emit regular events after trying a failed volatile event (polling)", (done) => { it("should emit regular events after trying a failed volatile event (polling)", (done) => {
const srv = createServer(); const srv = createServer();
const sio = new Server(srv, { transports: ["polling"] }); const sio = new Server(srv, { transports: ["polling"] });
@@ -1731,7 +1830,7 @@ describe("socket.io", () => {
reconnectionDelay: 100, reconnectionDelay: 100,
}); });
clientSocket.once("connect", () => { clientSocket.once("connect", () => {
srv.close(() => { sio.close(() => {
clientSocket.io.on("reconnect", () => { clientSocket.io.on("reconnect", () => {
clientSocket.emit("ev", "payload"); clientSocket.emit("ev", "payload");
}); });
@@ -1781,6 +1880,7 @@ describe("socket.io", () => {
console.log( console.log(
"\u001b[96mNote: warning expected and normal in test.\u001b[39m" "\u001b[96mNote: warning expected and normal in test.\u001b[39m"
); );
// @ts-ignore
socket.io.engine.write("5woooot"); socket.io.engine.write("5woooot");
setTimeout(() => { setTimeout(() => {
done(); done();
@@ -1800,6 +1900,7 @@ describe("socket.io", () => {
console.log( console.log(
"\u001b[96mNote: warning expected and normal in test.\u001b[39m" "\u001b[96mNote: warning expected and normal in test.\u001b[39m"
); );
// @ts-ignore
socket.io.engine.write('44["handle me please"]'); socket.io.engine.write('44["handle me please"]');
setTimeout(() => { setTimeout(() => {
done(); done();
@@ -1820,6 +1921,7 @@ describe("socket.io", () => {
done(); done();
}); });
s.conn.on("upgrade", () => { s.conn.on("upgrade", () => {
// @ts-ignore
socket.io.engine.write("5woooot"); socket.io.engine.write("5woooot");
}); });
}); });
@@ -1837,6 +1939,7 @@ describe("socket.io", () => {
done(); done();
}); });
s.conn.on("upgrade", () => { s.conn.on("upgrade", () => {
// @ts-ignore
socket.io.engine.write("5"); socket.io.engine.write("5");
}); });
}); });
@@ -2416,28 +2519,6 @@ describe("socket.io", () => {
}); });
}); });
}); });
it("should pre encode a broadcast packet", (done) => {
const srv = createServer();
const sio = new Server(srv);
srv.listen(() => {
const clientSocket = client(srv, { multiplex: false });
sio.on("connection", (socket) => {
socket.conn.on("packetCreate", (packet) => {
expect(packet.data).to.eql('2["hello","world"]');
expect(packet.options.wsPreEncoded).to.eql('42["hello","world"]');
clientSocket.close();
sio.close();
done();
});
sio.emit("hello", "world");
});
});
});
}); });
describe("middleware", () => { describe("middleware", () => {
@@ -2626,6 +2707,25 @@ describe("socket.io", () => {
if (++count === 2) done(); if (++count === 2) done();
}); });
}); });
it("should only set `connected` to true after the middleware execution", (done) => {
const httpServer = createServer();
const io = new Server(httpServer);
const clientSocket = client(httpServer, "/");
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(io, clientSocket, done);
});
});
}); });
describe("socket middleware", () => { describe("socket middleware", () => {
@@ -2765,4 +2865,6 @@ describe("socket.io", () => {
}); });
}); });
}); });
require("./socket-timeout");
}); });

View File

@@ -1,3 +1,11 @@
import type { Server } from "../..";
import {
io as ioc,
ManagerOptions,
Socket as ClientSocket,
SocketOptions,
} from "socket.io-client";
const expect = require("expect.js"); const expect = require("expect.js");
const i = expect.stringify; const i = expect.stringify;
@@ -20,3 +28,19 @@ expect.Assertion.prototype.contain = function (...args) {
} }
return contain.apply(this, args); return contain.apply(this, args);
}; };
export function createClient(
io: Server,
nsp: string,
opts?: 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, client: ClientSocket) {
io.close();
client.disconnect();
done();
}

197
test/uws.ts Normal file
View File

@@ -0,0 +1,197 @@
import { App, us_socket_local_port } 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,
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) => {
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.on("connect", partialDone);
clientPollingOnly.on("connect", partialDone);
clientCustomNamespace.on("connect", partialDone);
});
afterEach(() => {
io.close();
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 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");
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();
});
});
});