Compare commits

...

94 Commits
4.0.2 ... 4.5.4

Author SHA1 Message Date
Damien Arrachequesne
3b7ced7af7 chore(release): 4.5.4
Diff: https://github.com/socketio/socket.io/compare/4.5.3...4.5.4
2022-11-22 22:45:13 +01:00
Damien Arrachequesne
c00bb9564c chore: bump engine.io to version 6.2.1
In order to fix CVE-2022-41940.

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

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

- https://github.com/socketio/socket.io/issues/4434
- https://github.com/socketio/socket.io/issues/4494
2022-10-14 11:24:36 +02:00
Damien Arrachequesne
19b225b0c8 docs(examples): update dependencies of the basic CRUD example
Related: https://github.com/socketio/socket.io/issues/4409
2022-10-14 10:39:40 +02:00
Damien Arrachequesne
8fae95dd18 docs: add jsdoc for each public method 2022-10-14 10:30:08 +02:00
Damien Arrachequesne
e6f6b906db docs: add deprecation notice for the allSockets() method 2022-10-13 15:02:23 +02:00
Damien Arrachequesne
596eb88af7 ci: upgrade to actions/checkout@3 and actions/setup-node@3
Reference: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/
2022-10-13 14:50:09 +02:00
Damien Arrachequesne
e357daf585 fix(typings): apply types to "io.timeout(...).emit()" calls
Typed events were not applied when calling "io.timeout(...).emit()".

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

Reference: https://socket.io/docs/v4/typescript/
2022-10-13 14:50:09 +02:00
Damien Arrachequesne
10fa4a2690 refactor: add list of possible disconnection reasons
Note: some disconnection reasons could be merged in the next major
release, i.e. the Deno impl does not have "forced server close" and
"server shutting down"

Related: https://github.com/socketio/socket.io/issues/4387
2022-09-13 08:25:13 +02:00
Damien Arrachequesne
8be95b3bd3 chore(release): 4.5.2
Diff: https://github.com/socketio/socket.io/compare/4.5.1...4.5.2
2022-09-02 23:46:14 +01:00
Damien Arrachequesne
ba497ee3eb fix(uws): prevent the server from crashing after upgrade
This should fix a rare case where the Engine.IO connection was upgraded
to WebSocket while the Socket.IO socket was disconnected, which would
result in the following exception:

> TypeError: Cannot read properties of undefined (reading 'forEach')
>    at subscribe (/node_modules/socket.io/dist/uws.js:87:11)
>    at Socket.<anonymous> (/node_modules/socket.io/dist/uws.js:28:17)
>    at Socket.emit (node:events:402:35)
>    at WebSocket.onPacket (/node_modules/engine.io/build/socket.js:214:22)
>    at WebSocket.emit (node:events:390:28)
>    at WebSocket.onPacket (/node_modules/engine.io/build/transport.js:92:14)
>    at WebSocket.onData (/node_modules/engine.io/build/transport.js:101:14)
>    at message (/node_modules/engine.io/build/userver.js:56:30)

Related: https://github.com/socketio/socket.io/issues/4443
2022-09-02 23:42:26 +01:00
Alex
28038715cb ci: add explicit permissions to workflow (#4466)
Reference: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
2022-09-02 23:10:19 +01:00
Daniel Rosenwasser
134226e96c refactor: add missing constraints (#4431)
See also: https://github.com/microsoft/TypeScript/issues/49489
2022-09-01 21:13:55 +01:00
Damien Arrachequesne
9890b036cf chore: bump dependencies
Production:

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

Development:

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

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

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

Related:

- https://github.com/socketio/socket.io/issues/4067
- https://github.com/socketio/socket.io/issues/4380
2022-05-25 23:18:42 +02:00
Damien Arrachequesne
5ab8289c0a chore(release): 4.5.1
Diff: https://github.com/socketio/socket.io/compare/4.5.0...4.5.1
2022-05-17 23:38:07 +02:00
Damien Arrachequesne
30430f0985 fix: forward the local flag to the adapter when using fetchSockets()
Related:

- https://github.com/socketio/socket.io/issues/4359
- https://github.com/socketio/socket.io-redis-adapter/issues/454
2022-05-03 15:41:24 +02:00
h110m
9b43c9167c fix(typings): add HTTPS server to accepted types (#4351) 2022-05-03 08:15:22 +02:00
Damien Arrachequesne
8ecfcba5c1 chore(release): 4.5.0
Diff: https://github.com/socketio/socket.io/compare/4.4.1...4.5.0
2022-04-24 00:45:57 +02:00
Damien Arrachequesne
572133a58d docs(examples): update example with webpack 2022-04-22 23:10:01 +02:00
Damien Arrachequesne
6e1bb62982 chore: bump engine.io to version 6.2.0
Release notes: https://github.com/socketio/engine.io/releases/tag/6.2.0
Diff: https://github.com/socketio/engine.io/compare/6.1.3...6.2.0
2022-04-22 22:43:31 +02:00
Damien Arrachequesne
06e6838b18 docs(examples): add server bundling example with rollup
Related: https://github.com/socketio/socket.io/issues/4329
2022-04-22 22:40:15 +02:00
WD
1f03a44d1f docs(examples): update create-react-app example (#4347) 2022-04-20 22:47:34 +02:00
Damien Arrachequesne
be3d7f0f1f docs(examples): add TODO example with Postgres and Node.js cluster 2022-04-07 12:35:00 +02:00
Damien Arrachequesne
d12aab2d69 docs(examples): add example with express-session
Related: https://github.com/socketio/socket.io/issues/3933
2022-04-02 11:08:26 +02:00
Damien Arrachequesne
9f758689f6 docs(examples): pin the version of karma-jasmine-html-reporter
Related:

- https://github.com/socketio/socket.io/issues/4325
- https://github.com/dfederm/karma-jasmine-html-reporter/issues/54
2022-04-01 14:42:34 +02:00
Damien Arrachequesne
0b35dc77c0 refactor: make the protocol implementation stricter
This commit handles several edge cases that were silently ignored
before:

- receiving several CONNECT packets during a session
- receiving any packet without CONNECT packet first
2022-03-31 12:24:31 +02:00
Damien Arrachequesne
531104d332 feat: add support for catch-all listeners for outgoing packets
This is similar to `onAny()`, but for outgoing packets.

Syntax:

```js
socket.onAnyOutgoing((event, ...args) => {
  console.log(event);
});
```
2022-03-31 10:35:09 +02:00
Damien Arrachequesne
8b204570a9 feat: broadcast and expect multiple acks
Syntax:

```js
io.timeout(1000).emit("some-event", (err, responses) => {
  // ...
});
```

The adapter exposes two additional methods:

- `broadcastWithAck(packets, opts, clientCountCallback, ack)`

Similar to `broadcast(packets, opts)`, but:

* `clientCountCallback()` is called with the number of clients that
  received the packet (can be called several times in a cluster)
* `ack()` is called for each client response

- `serverCount()`

It returns the number of Socket.IO servers in the cluster (1 for the
in-memory adapter).

Those two methods will be implemented in the other adapters (Redis,
Postgres, MongoDB, ...).

Related:

- https://github.com/socketio/socket.io/issues/1811
- https://github.com/socketio/socket.io/issues/4163
- https://github.com/socketio/socket.io-redis-adapter/issues/445
2022-03-31 07:49:09 +02:00
Damien Arrachequesne
0b7d70ca42 chore: bump lockfile to v2 2022-03-30 08:15:41 +02: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
Damien Arrachequesne
1633150b2b chore(release): 4.1.2
Diff: https://github.com/socketio/socket.io/compare/4.1.1...4.1.2
2021-05-17 23:17:31 +02:00
Damien Arrachequesne
0cb6ac95b4 fix(typings): ensure compatibility with TypeScript 3.x
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: https://github.com/socketio/socket.io/issues/3916
2021-05-17 23:15:22 +02:00
Damien Arrachequesne
a2cf2486c3 fix: ensure compatibility with previous versions of the adapter
Using `socket.io@4.1.0` with `socket.io-adapter@2.2.0` would lead to
the following error:

> Uncaught Error: unknown packet type NaN

Because the packet would be encoded twice, resulting in "undefined".

See also:

- 5579d40c24
- dc381b72c6

Related:

- https://github.com/socketio/socket.io/issues/3922
- https://github.com/socketio/socket.io/issues/3927
2021-05-17 23:14:36 +02:00
Damien Arrachequesne
995f38f4cc chore(release): 4.1.1
Diff: https://github.com/socketio/socket.io/compare/4.1.0...4.1.1
2021-05-12 00:04:52 +02:00
Damien Arrachequesne
891b1870e9 fix(typings): properly type the adapter attribute
Related: https://github.com/socketio/socket.io/issues/3796
2021-05-11 23:59:44 +02:00
Damien Arrachequesne
b84ed1e41c fix(typings): properly type server-side events
See also: 93cce05fb3
2021-05-11 23:59:18 +02:00
Damien Arrachequesne
fb6b0efec9 chore(release): 4.1.0
Diff: https://github.com/socketio/socket.io/compare/4.0.2...4.1.0
2021-05-11 09:27:52 +02:00
Damien Arrachequesne
95d9e4a42f test: fix randomly failing test 2021-05-11 00:06:03 +02:00
Damien Arrachequesne
499c89250d feat: notify upon namespace creation
A "new_namespace" event will be emitted when a new namespace is created:

```js
io.on("new_namespace", (namespace) => {
  // ...
});
```

This could be used for example for registering the same middleware for
each namespace.

See https://github.com/socketio/socket.io/issues/3851
2021-05-11 00:09:18 +02:00
Damien Arrachequesne
93cce05fb3 feat: add support for inter-server communication
Syntax:

```js
// server A
io.serverSideEmit("hello", "world");

// server B
io.on("hello", (arg) => {
  console.log(arg); // prints "world"
});
```

With acknowledgements:

```js
// server A
io.serverSideEmit("hello", "world", (err, responses) => {
  console.log(responses); // prints ["hi"]
});

// server B
io.on("hello", (arg, callback) => {
  callback("hi");
});
```

This feature replaces the customHook/customRequest API from the Redis
adapter: https://github.com/socketio/socket.io-redis/issues/370
2021-05-11 00:07:20 +02:00
Damien Arrachequesne
dc381b72c6 perf: add support for the "wsPreEncoded" writing option
Packets that are sent to multiple clients will now be pre-encoded for
the WebSocket transport (which means simply prepending "4" - which is
the "message" packet type in Engine.IO).

Note: buffers are not pre-encoded, since they are sent without
modification over the WebSocket connection

See also: 7706b123df

engine.io diff: https://github.com/socketio/engine.io/compare/5.0.0...5.1.0
2021-05-11 00:06:03 +02:00
84 changed files with 16616 additions and 11853 deletions

View File

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

View File

@@ -6,21 +6,31 @@ on:
schedule:
- cron: '0 0 * * 0'
permissions:
contents: read
jobs:
test-node:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [10.x, 12.x, 14.x, 15.x]
node-version: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Checkout repository
uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
env:
CI: true

View File

@@ -1,3 +1,271 @@
# History
## 2022
- [4.5.4](#454-2022-11-22) (Nov 2022)
- [4.5.3](#453-2022-10-15) (Oct 2022)
- [4.5.2](#452-2022-09-02) (Sep 2022)
- [2.5.0](#250-2022-06-26) (Jun 2022) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [4.5.1](#451-2022-05-17) (May 2022)
- [4.5.0](#450-2022-04-23) (Apr 2022)
- [4.4.1](#441-2022-01-06) (Jan 2022)
## 2021
- [4.4.0](#440-2021-11-18) (Nov 2021)
- [4.3.2](#432-2021-11-08) (Nov 2021)
- [4.3.1](#431-2021-10-16) (Oct 2021)
- [4.3.0](#430-2021-10-14) (Oct 2021)
- [4.2.0](#420-2021-08-30) (Aug 2021)
- [4.1.3](#413-2021-07-10) (Jul 2021)
- [4.1.2](#412-2021-05-17) (May 2021)
- [4.1.1](#411-2021-05-11) (May 2021)
- [4.1.0](#410-2021-05-11) (May 2021)
- [4.0.2](#402-2021-05-06) (May 2021)
- [4.0.1](#401-2021-03-31) (Mar 2021)
- [**4.0.0**](#400-2021-03-10) (Mar 2021)
- [3.1.2](#312-2021-02-26) (Feb 2021)
- [3.1.1](#311-2021-02-03) (Feb 2021)
- [3.1.0](#310-2021-01-15) (Jan 2021)
- [2.4.1](#241-2021-01-07) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
- [3.0.5](#305-2021-01-05) (Jan 2021)
- [2.4.0](#240-2021-01-04) (Jan 2021) (from the [2.x](https://github.com/socketio/socket.io/tree/2.x) branch)
## 2020
- [3.0.4](#304-2020-12-07) (Dec 2020)
- [3.0.3](#303-2020-11-19) (Nov 2020)
- [3.0.2](#302-2020-11-17) (Nov 2020)
- [3.0.1](#301-2020-11-09) (Nov 2020)
- [**3.0.0**](#300-2020-11-05) (Nov 2020)
## 2019
- [2.3.0](#230-2019-09-20) (Sep 2019)
## 2018
- [2.2.0](#220-2018-11-29) (Nov 2018)
- [2.1.1](#211-2018-05-17) (May 2018)
- [2.1.0](#210-2018-03-29) (Mar 2018)
# Release notes
## [4.5.4](https://github.com/socketio/socket.io/compare/4.5.3...4.5.4) (2022-11-22)
This release contains a bump of:
- `engine.io` in order to fix [CVE-2022-41940](https://github.com/socketio/engine.io/security/advisories/GHSA-r7qp-cfhv-p84w)
- `socket.io-parser` in order to fix [CVE-2022-2421](https://github.com/advisories/GHSA-qm95-pgcg-qqfq).
### Dependencies
- [`engine.io@~6.2.1`](https://github.com/socketio/engine.io-client/tree/6.2.1) ([diff](https://github.com/socketio/engine.io/compare/6.2.0...6.2.1))
- [`ws@~8.2.3`](https://github.com/websockets/ws/releases/tag/8.2.3)
## [4.5.3](https://github.com/socketio/socket.io/compare/4.5.2...4.5.3) (2022-10-15)
### Bug Fixes
* **typings:** accept an HTTP2 server in the constructor ([d3d0a2d](https://github.com/socketio/socket.io/commit/d3d0a2d5beaff51fd145f810bcaf6914213f8a06))
* **typings:** apply types to "io.timeout(...).emit()" calls ([e357daf](https://github.com/socketio/socket.io/commit/e357daf5858560bc84e7e50cd36f0278d6721ea1))
## [4.5.2](https://github.com/socketio/socket.io/compare/4.5.1...4.5.2) (2022-09-02)
### Bug Fixes
* prevent the socket from joining a room after disconnection ([18f3fda](https://github.com/socketio/socket.io/commit/18f3fdab12947a9fee3e9c37cfc1da97027d1473))
* **uws:** prevent the server from crashing after upgrade ([ba497ee](https://github.com/socketio/socket.io/commit/ba497ee3eb52c4abf1464380d015d8c788714364))
# [2.5.0](https://github.com/socketio/socket.io/compare/2.4.1...2.5.0) (2022-06-26)
### Bug Fixes
* fix race condition in dynamic namespaces ([05e1278](https://github.com/socketio/socket.io/commit/05e1278cfa99f3ecf3f8f0531ffe57d850e9a05b))
* ignore packet received after disconnection ([22d4bdf](https://github.com/socketio/socket.io/commit/22d4bdf00d1a03885dc0171125faddfaef730066))
* only set 'connected' to true after middleware execution ([226cc16](https://github.com/socketio/socket.io/commit/226cc16165f9fe60f16ff4d295fb91c8971cde35))
* prevent the socket from joining a room after disconnection ([f223178](https://github.com/socketio/socket.io/commit/f223178eb655a7713303b21a78f9ef9e161d6458))
## [4.5.1](https://github.com/socketio/socket.io/compare/4.5.0...4.5.1) (2022-05-17)
### Bug Fixes
* forward the local flag to the adapter when using fetchSockets() ([30430f0](https://github.com/socketio/socket.io/commit/30430f0985f8e7c49394543d4c84913b6a15df60))
* **typings:** add HTTPS server to accepted types ([#4351](https://github.com/socketio/socket.io/issues/4351)) ([9b43c91](https://github.com/socketio/socket.io/commit/9b43c9167cff817c60fa29dbda2ef7cd938aff51))
# [4.5.0](https://github.com/socketio/socket.io/compare/4.4.1...4.5.0) (2022-04-23)
### Bug Fixes
* **typings:** ensure compatibility with TypeScript 3.x ([#4259](https://github.com/socketio/socket.io/issues/4259)) ([02c87a8](https://github.com/socketio/socket.io/commit/02c87a85614e217b8e7b93753f315790ae9d99f6))
### Features
* add support for catch-all listeners for outgoing packets ([531104d](https://github.com/socketio/socket.io/commit/531104d332690138b7aab84d5583d6204132c8b4))
This is similar to `onAny()`, but for outgoing packets.
Syntax:
```js
socket.onAnyOutgoing((event, ...args) => {
console.log(event);
});
```
* broadcast and expect multiple acks ([8b20457](https://github.com/socketio/socket.io/commit/8b204570a94979bbec307f23ca078f30f5cf07b0))
Syntax:
```js
io.timeout(1000).emit("some-event", (err, responses) => {
// ...
});
```
* add the "maxPayload" field in the handshake details ([088dcb4](https://github.com/socketio/engine.io/commit/088dcb4dff60df39785df13d0a33d3ceaa1dff38))
So that clients in HTTP long-polling can decide how many packets they have to send to stay under the maxHttpBufferSize
value.
This is a backward compatible change which should not mandate a new major revision of the protocol (we stay in v4), as
we only add a field in the JSON-encoded handshake data:
```
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
```
## [4.4.1](https://github.com/socketio/socket.io/compare/4.4.0...4.4.1) (2022-01-06)
### Bug Fixes
* **types:** make `RemoteSocket.data` type safe ([#4234](https://github.com/socketio/socket.io/issues/4234)) ([770ee59](https://github.com/socketio/socket.io/commit/770ee5949fb47c2556876c622f06c862573657d6))
* **types:** pass `SocketData` type to custom namespaces ([#4233](https://github.com/socketio/socket.io/issues/4233)) ([f2b8de7](https://github.com/socketio/socket.io/commit/f2b8de71919e1b4d3e57f15a459972c1d1064787))
# [4.4.0](https://github.com/socketio/socket.io/compare/4.3.2...4.4.0) (2021-11-18)
### Bug Fixes
* only set 'connected' to true after middleware execution ([02b0f73](https://github.com/socketio/socket.io/commit/02b0f73e2c64b09c72c5fbf7dc5f059557bdbe50))
### Features
* add an implementation based on uWebSockets.js ([c0d8c5a](https://github.com/socketio/socket.io/commit/c0d8c5ab234d0d2bef0d0dec472973cc9662f647))
* add timeout feature ([f0ed42f](https://github.com/socketio/socket.io/commit/f0ed42f18cabef20ad976aeec37077b6bf3837a5))
* add type information to `socket.data` ([#4159](https://github.com/socketio/socket.io/issues/4159)) ([fe8730c](https://github.com/socketio/socket.io/commit/fe8730ca0f15bc92d5de81cf934c89c76d6af329))
## [4.3.2](https://github.com/socketio/socket.io/compare/4.3.1...4.3.2) (2021-11-08)
### Bug Fixes
* fix race condition in dynamic namespaces ([#4137](https://github.com/socketio/socket.io/issues/4137)) ([9d86397](https://github.com/socketio/socket.io/commit/9d86397243bcbb5775a29d96e5ef03e17148a8e7))
## [4.3.1](https://github.com/socketio/socket.io/compare/4.3.0...4.3.1) (2021-10-16)
### Bug Fixes
* fix server attachment ([#4127](https://github.com/socketio/socket.io/issues/4127)) ([0ef2a4d](https://github.com/socketio/socket.io/commit/0ef2a4d02c9350aff163df9cb61aece89c4dac0f))
# [4.3.0](https://github.com/socketio/socket.io/compare/4.2.0...4.3.0) (2021-10-14)
### Bug Fixes
* **typings:** add name field to cookie option ([#4099](https://github.com/socketio/socket.io/issues/4099)) ([033c5d3](https://github.com/socketio/socket.io/commit/033c5d399a2b985afad32c1e4b0c16d764e248cd))
* send volatile packets with binary attachments ([dc81fcf](https://github.com/socketio/socket.io/commit/dc81fcf461cfdbb5b34b1a5a96b84373754047d5))
### Features
* serve ESM bundle ([60edecb](https://github.com/socketio/socket.io/commit/60edecb3bd33801803cdcba0aefbafa381a2abb3))
# [4.2.0](https://github.com/socketio/socket.io/compare/4.1.3...4.2.0) (2021-08-30)
### Bug Fixes
* **typings:** allow async listener in typed events ([ccfd8ca](https://github.com/socketio/socket.io/commit/ccfd8caba6d38b7ba6c5114bd8179346ed07671c))
### Features
* ignore the query string when serving client JavaScript ([#4024](https://github.com/socketio/socket.io/issues/4024)) ([24fee27](https://github.com/socketio/socket.io/commit/24fee27ba36485308f8e995879c10931532c814e))
## [4.1.3](https://github.com/socketio/socket.io/compare/4.1.2...4.1.3) (2021-07-10)
### Bug Fixes
* fix io.except() method ([94e27cd](https://github.com/socketio/socket.io/commit/94e27cd072c8a4eeb9636f6ffbb7a21d382f36b0))
* remove x-sourcemap header ([a4dffc6](https://github.com/socketio/socket.io/commit/a4dffc6527f412d51a786ae5bf2e9080fe1ca63c))
## [4.1.2](https://github.com/socketio/socket.io/compare/4.1.1...4.1.2) (2021-05-17)
### Bug Fixes
* **typings:** ensure compatibility with TypeScript 3.x ([0cb6ac9](https://github.com/socketio/socket.io/commit/0cb6ac95b49a27483b6f1b6402fa54b35f82e36f))
* ensure compatibility with previous versions of the adapter ([a2cf248](https://github.com/socketio/socket.io/commit/a2cf2486c366cb62293101c10520c57f6984a3fc))
## [4.1.1](https://github.com/socketio/socket.io/compare/4.1.0...4.1.1) (2021-05-11)
### Bug Fixes
* **typings:** properly type server-side events ([b84ed1e](https://github.com/socketio/socket.io/commit/b84ed1e41c9053792caf58974c5de9395bfd509f))
* **typings:** properly type the adapter attribute ([891b187](https://github.com/socketio/socket.io/commit/891b1870e92d1ec38910f03bb839817e2d6be65a))
# [4.1.0](https://github.com/socketio/socket.io/compare/4.0.2...4.1.0) (2021-05-11)
### Features
* add support for inter-server communication ([93cce05](https://github.com/socketio/socket.io/commit/93cce05fb3faf91f21fa71212275c776aa161107))
* notify upon namespace creation ([499c892](https://github.com/socketio/socket.io/commit/499c89250d2db1ab7725ab2b74840e188c267c46))
* add a "connection_error" event ([7096e98](https://github.com/socketio/engine.io/commit/7096e98a02295a62c8ea2aa56461d4875887092d), from `engine.io`)
* add the "initial_headers" and "headers" events ([2527543](https://github.com/socketio/engine.io/commit/252754353a0e88eb036ebb3082e9d6a9a5f497db), from `engine.io`)
### Performance Improvements
* add support for the "wsPreEncoded" writing option ([dc381b7](https://github.com/socketio/socket.io/commit/dc381b72c6b2f8172001dedd84116122e4cc95b3))
## [4.0.2](https://github.com/socketio/socket.io/compare/4.0.1...4.0.2) (2021-05-06)
@@ -66,6 +334,16 @@
* allow integers as event names ([1c220dd](https://github.com/socketio/socket.io-parser/commit/1c220ddbf45ea4b44bc8dbf6f9ae245f672ba1b9))
## [2.4.1](https://github.com/socketio/socket.io/compare/2.4.0...2.4.1) (2021-01-07)
### Reverts
* fix(security): do not allow all origins by default ([a169050](https://github.com/socketio/socket.io/commit/a1690509470e9dd5559cec4e60908ca6c23e9ba0))
## [3.0.5](https://github.com/socketio/socket.io/compare/3.0.4...3.0.5) (2021-01-05)
@@ -79,6 +357,17 @@
* restore the socket middleware functionality ([bf54327](https://github.com/socketio/socket.io/commit/bf5432742158e4d5ba2722cff4a614967dffa5b9))
# [2.4.0](https://github.com/socketio/socket.io/compare/2.3.0...2.4.0) (2021-01-04)
### Bug Fixes
* **security:** do not allow all origins by default ([f78a575](https://github.com/socketio/socket.io/commit/f78a575f66ab693c3ea96ea88429ddb1a44c86c7))
* properly overwrite the query sent in the handshake ([d33a619](https://github.com/socketio/socket.io/commit/d33a619905a4905c153d4fec337c74da5b533a9e))
## [3.0.4](https://github.com/socketio/socket.io/compare/3.0.3...3.0.4) (2020-12-07)
@@ -364,3 +653,78 @@ io.of("/admin").use((socket, next) => {
This method was kept for backward-compatibility with pre-1.0 versions.
# [2.3.0](https://github.com/socketio/socket.io/compare/2.2.0...2.3.0) (2019-09-20)
This release mainly contains a bump of the `engine.io` and `ws` packages, but no additional features.
# [2.2.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-11-29)
### Features
- add cache-control header when serving the client source ([#2907](https://github.com/socketio/socket.io/pull/2907)) ([b00ae50](https://github.com/socketio/socket.io/commit/b00ae50be65d1bc88fa95145f1c486a6886a6b76))
### Bug fixes
- throw an error when trying to access the clients of a dynamic namespace ([#3355](https://github.com/socketio/socket.io/pull/3355)) ([a7fbd1a](https://github.com/socketio/socket.io/commit/a7fbd1ac4a47cafd832fc62e371754df924c5903))
# [2.1.1](https://github.com/socketio/socket.io/compare/2.1.0...2.1.1) (2018-05-17)
### Features
- add local flag to the socket object ([#3129](https://github.com/socketio/socket.io/pull/3219)) ([1decae3](https://github.com/socketio/socket.io/commit/1decae341c80c0417b32d3124ca30c005240b48a))
```js
socket.local.to('room101').emit(/* */);
```
# [2.1.0](https://github.com/socketio/socket.io/compare/2.1.1...2.2.0) (2018-03-29)
### Features
- add a 'binary' flag ([#3185](https://github.com/socketio/socket.io/pull/3185)) ([f48a06c](https://github.com/socketio/socket.io/commit/f48a06c040280b44f90fd225c888910544fd63b5))
```js
// by default, the object is recursively scanned to check whether it contains some binary data
// in the following example, the check is skipped in order to improve performance
socket.binary(false).emit('plain-object', object);
// it also works at the namespace level
io.binary(false).emit('plain-object', object);
```
- add support for dynamic namespaces ([#3195](https://github.com/socketio/socket.io/pull/3195)) ([c0c79f0](https://github.com/socketio/socket.io/commit/c0c79f019e7138194e438339f8192705957c8ec3))
```js
io.of(/^\/dynamic-\d+$/).on('connect', (socket) => {
// socket.nsp.name = '/dynamic-101'
});
// client-side
const client = require('socket.io-client')('/dynamic-101');
```
### Bug fixes
- properly emit 'connect' when using a custom namespace ([#3197](https://github.com/socketio/socket.io/pull/3197)) ([f4fc517](https://github.com/socketio/socket.io/commit/f4fc517e0fe25866c95b584291487b8cbdff889d))
- include the protocol in the origins check ([#3198](https://github.com/socketio/socket.io/pull/3198)) ([1f1d64b](https://github.com/socketio/socket.io/commit/1f1d64bab61a273712a199591a3f76210d8c0959))
### Important note :warning: from Engine.IO [3.2.0 release](https://github.com/socketio/engine.io/releases/tag/3.2.0)
There are two non-breaking changes that are somehow quite important:
- `ws` was reverted as the default wsEngine (https://github.com/socketio/engine.io/pull/550), as there was several blocking issues with `uws`. You can still use `uws` by running `npm install uws --save` in your project and using the `wsEngine` option:
```js
var engine = require('engine.io');
var server = engine.listen(3000, {
wsEngine: 'uws'
});
```
- `pingTimeout` now defaults to 5 seconds (instead of 60 seconds): https://github.com/socketio/engine.io/pull/551

View File

@@ -1,9 +1,7 @@
# 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)
[![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)
![Downloads](https://img.shields.io/npm/dm/socket.io.svg?style=flat)
[![](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)
- [Dart](https://github.com/rikulo/socket.io-client-dart)
- [Python](https://github.com/miguelgrinberg/python-socketio)
- [.Net](https://github.com/Quobject/SocketIoClientDotNet)
- [.NET](https://github.com/doghappy/socket.io-client-csharp)
Its main features are:
@@ -115,6 +113,14 @@ io.on('connection', client => { ... });
io.listen(3000);
```
### Module syntax
```js
import { Server } from "socket.io";
const io = new Server(server);
io.listen(3000);
```
### In conjunction with Express
Starting with **3.0**, express applications have become request handler

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

@@ -2,6 +2,13 @@
Please read the related [guide](https://socket.io/get-started/basic-crud-application/).
This repository contains several implementations of the server:
| Directory | Language | Database | Cluster? |
|----------------------------|------------|------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------|
| `server/` | TypeScript | in-memory | No |
| `server-postgres-cluster/` | JavaScript | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Running the frontend
```

View File

@@ -37,7 +37,7 @@
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-jasmine-html-reporter": "~1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",

View File

@@ -0,0 +1,16 @@
A basic TODO project.
| Characteristic | |
|----------------|-------------------------------------------------------------------------------------------|
| Language | plain JavaScript |
| Database | Postgres, with the [Postgres adapter](https://socket.io/docs/v4/postgres-adapter/) |
| Cluster? | Yes, with the [`@socket.io/sticky`](https://github.com/socketio/socket.io-sticky) module) |
## Usage
```
$ docker-compose up -d
$ npm install
$ npm start
```

View File

@@ -0,0 +1,9 @@
version: "3"
services:
postgres:
image: postgres:12
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: "changeit"

View File

@@ -0,0 +1,26 @@
import { Server } from "socket.io";
import createTodoHandlers from "./todo-management/todo.handlers.js";
import { setupWorker } from "@socket.io/sticky";
import { createAdapter } from "@socket.io/postgres-adapter";
export function createApplication(httpServer, components, serverOptions = {}) {
const io = new Server(httpServer, serverOptions);
const { createTodo, readTodo, updateTodo, deleteTodo, listTodo } =
createTodoHandlers(components);
io.on("connection", (socket) => {
socket.on("todo:create", createTodo);
socket.on("todo:read", readTodo);
socket.on("todo:update", updateTodo);
socket.on("todo:delete", deleteTodo);
socket.on("todo:list", listTodo);
});
// enable sticky session in the cluster (to remove in standalone mode)
setupWorker(io);
io.adapter(createAdapter(components.connectionPool));
return io;
}

View File

@@ -0,0 +1,28 @@
import cluster from "cluster";
import { createServer } from "http";
import { setupMaster } from "@socket.io/sticky";
import { cpus } from "os";
if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);
const httpServer = createServer();
setupMaster(httpServer, {
loadBalancingMethod: "least-connection",
});
httpServer.listen(3000);
for (let i = 0; i < cpus().length; i++) {
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork();
});
} else {
console.log(`Worker ${process.pid} started`);
import("./index.js");
}

View File

@@ -0,0 +1,51 @@
import { createServer } from "http";
import { createApplication } from "./app.js";
import { Sequelize } from "sequelize";
import pg from "pg";
import { PostgresTodoRepository } from "./todo-management/todo.repository.js";
const httpServer = createServer();
const sequelize = new Sequelize("postgres", "postgres", "changeit", {
dialect: "postgres",
});
const connectionPool = new pg.Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: "changeit",
port: 5432,
});
createApplication(
httpServer,
{
connectionPool,
todoRepository: new PostgresTodoRepository(sequelize),
},
{
cors: {
origin: ["http://localhost:4200"],
},
}
);
const main = async () => {
// create the tables if they do not exist already
await sequelize.sync();
// create the table needed by the postgres adapter
await connectionPool.query(`
CREATE TABLE IF NOT EXISTS socket_io_attachments (
id bigserial UNIQUE,
created_at timestamptz DEFAULT NOW(),
payload bytea
);
`);
// uncomment when running in standalone mode
// httpServer.listen(3000);
};
main();

View File

@@ -0,0 +1,140 @@
import { Errors, mapErrorDetails, sanitizeErrorMessage } from "../util.js";
import { v4 as uuid } from "uuid";
import Joi from "joi";
const idSchema = Joi.string().guid({
version: "uuidv4",
});
const todoSchema = Joi.object({
id: idSchema.alter({
create: (schema) => schema.forbidden(),
update: (schema) => schema.required(),
}),
title: Joi.string().max(256).required(),
completed: Joi.boolean().required(),
});
export default function (components) {
const { todoRepository } = components;
return {
createTodo: async function (payload, callback) {
const socket = this;
// validate the payload
const { error, value } = todoSchema.tailor("create").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
value.id = uuid();
// persist the entity
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
// acknowledge the creation
callback({
data: value.id,
});
// notify the other users
socket.broadcast.emit("todo:created", value);
},
readTodo: async function (id, callback) {
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
const todo = await todoRepository.findById(id);
callback({
data: todo,
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
updateTodo: async function (payload, callback) {
const socket = this;
const { error, value } = todoSchema.tailor("update").validate(payload, {
abortEarly: false,
stripUnknown: true,
});
if (error) {
return callback({
error: Errors.INVALID_PAYLOAD,
errorDetails: mapErrorDetails(error.details),
});
}
try {
await todoRepository.save(value);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:updated", value);
},
deleteTodo: async function (id, callback) {
const socket = this;
const { error } = idSchema.validate(id);
if (error) {
return callback({
error: Errors.ENTITY_NOT_FOUND,
});
}
try {
await todoRepository.deleteById(id);
} catch (e) {
return callback({
error: sanitizeErrorMessage(e),
});
}
callback();
socket.broadcast.emit("todo:deleted", id);
},
listTodo: async function (callback) {
try {
callback({
data: await todoRepository.findAll(),
});
} catch (e) {
callback({
error: sanitizeErrorMessage(e),
});
}
},
};
}

View File

@@ -0,0 +1,74 @@
import { Errors } from "../util.js";
import { Model, DataTypes } from "sequelize";
class CrudRepository {
findAll() {}
findById(id) {}
save(entity) {}
deleteById(id) {}
}
export class TodoRepository extends CrudRepository {}
class Todo extends Model {}
export class PostgresTodoRepository extends TodoRepository {
constructor(sequelize) {
super();
this.sequelize = sequelize;
Todo.init(
{
id: {
type: DataTypes.STRING,
primaryKey: true,
allowNull: false,
},
title: {
type: DataTypes.STRING,
},
completed: {
type: DataTypes.BOOLEAN,
},
},
{
sequelize,
tableName: "todos",
}
);
}
findAll() {
return this.sequelize.transaction((transaction) => {
return Todo.findAll({ transaction });
});
}
async findById(id) {
return this.sequelize.transaction(async (transaction) => {
const todo = await Todo.findByPk(id, { transaction });
if (!todo) {
throw Errors.ENTITY_NOT_FOUND;
}
return todo;
});
}
save(entity) {
return this.sequelize.transaction((transaction) => {
return Todo.upsert(entity, { transaction });
});
}
async deleteById(id) {
return this.sequelize.transaction(async (transaction) => {
const count = await Todo.destroy({ where: { id }, transaction });
if (count === 0) {
throw Errors.ENTITY_NOT_FOUND;
}
});
}
}

View File

@@ -0,0 +1,22 @@
export const Errors = {
ENTITY_NOT_FOUND: "entity not found",
INVALID_PAYLOAD: "invalid payload",
};
const errorValues = Object.values(Errors);
export function sanitizeErrorMessage(message) {
if (typeof message === "string" && errorValues.includes(message)) {
return message;
} else {
return "an unknown error has occurred";
}
}
export function mapErrorDetails(details) {
return details.map((item) => ({
message: item.message,
path: item.path,
type: item.type,
}));
}

View File

@@ -0,0 +1,30 @@
{
"name": "basic-crud-server",
"version": "0.0.1",
"description": "Server for the Basic CRUD Socket.IO example (with Postgres and multiple Socket.IO servers)",
"main": "lib/cluster.js",
"type": "module",
"scripts": {
"start": "node lib/cluster.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/socketio/socket.io.git"
},
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/socketio/socket.io/issues"
},
"homepage": "https://github.com/socketio/socket.io#readme",
"dependencies": {
"@socket.io/postgres-adapter": "^0.2.0",
"@socket.io/sticky": "^1.0.1",
"joi": "^17.4.0",
"pg": "^8.7.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.18.0",
"socket.io": "^4.0.1",
"uuid": "^8.3.2"
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
nginx:
build: ./nginx
image: nginx:alpine
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
links:
- server-john
- server-paul

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

View File

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

View File

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import logo from './logo.svg';
import './App.css';
import io from 'socket.io-client';
@@ -23,7 +24,7 @@ function App() {
socket.off('disconnect');
socket.off('message');
};
});
}, []);
const sendMessage = () => {
socket.emit('hello!');
@@ -32,9 +33,21 @@ function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Connected: { '' + isConnected }</p>
<p>Last message: { lastMessage || '-' }</p>
<button onClick={ sendMessage }>Say hello!</button>
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);

View File

@@ -0,0 +1,16 @@
# Example with [express-session](https://www.npmjs.com/package/express-session)
This example shows how to share a session context between [Express](http://expressjs.com/) and [Socket.IO](https://socket.io/docs/v4/):
![Video of the example](assets/demo.gif)
Please read the related guide: https://socket.io/how-to/use-with-express-session
## How to use
```
$ npm install
$ npm start
```
And point your browser to `http://localhost:3000`. Optionally, specify a port by supplying the `PORT` env variable.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Example with express-session</title>
</head>
<body>
<button onclick="incrementWithFetch()">Increment with fetch()</button>
<button onclick="logout()">Logout</button>
<p>Count: <span id="httpCount">0</span></p>
<button onclick="incrementWithEmit()">
Increment with Socket.IO emit()
</button>
<p>Status: <span id="ioStatus">disconnected</span></p>
<p>Count: <span id="ioCount">0</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const httpCount = document.getElementById("httpCount");
const ioStatus = document.getElementById("ioStatus");
const ioCount = document.getElementById("ioCount");
const socket = io({
// with WebSocket only
// transports: ["websocket"],
});
async function incrementWithFetch() {
const response = await fetch("/incr", {
method: "post",
});
httpCount.innerText = await response.text();
}
function logout() {
fetch("/logout", {
method: "post",
});
}
async function incrementWithEmit() {
socket.emit("incr", (count) => {
ioCount.innerText = count;
});
}
socket.on("connect", () => {
ioStatus.innerText = "connected";
});
socket.on("disconnect", () => {
ioStatus.innerText = "disconnected";
});
</script>
</body>
</html>

View File

@@ -0,0 +1,91 @@
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import session from "express-session";
const port = process.env.PORT || 3000;
const app = express();
const httpServer = createServer(app);
const sessionMiddleware = session({
secret: "changeit",
resave: true,
saveUninitialized: true,
});
app.use(sessionMiddleware);
app.get("/", (req, res) => {
res.sendFile("./index.html", { root: process.cwd() });
});
app.post("/incr", (req, res) => {
const session = req.session;
session.count = (session.count || 0) + 1;
res.status(200).end("" + session.count);
});
app.post("/logout", (req, res) => {
const sessionId = req.session.id;
req.session.destroy(() => {
// disconnect all Socket.IO connections linked to this session ID
io.to(sessionId).disconnectSockets();
res.status(204).end();
});
});
const io = new Server(httpServer, {
allowRequest: (req, callback) => {
// with HTTP long-polling, we have access to the HTTP response here, but this is not
// the case with WebSocket, so we provide a dummy response object
const fakeRes = {
getHeader() {
return [];
},
setHeader(key, values) {
req.cookieHolder = values[0];
},
writeHead() {},
};
sessionMiddleware(req, fakeRes, () => {
if (req.session) {
// trigger the setHeader() above
fakeRes.writeHead();
// manually save the session (normally triggered by res.end())
req.session.save();
}
callback(null, true);
});
},
});
io.engine.on("initial_headers", (headers, req) => {
if (req.cookieHolder) {
headers["set-cookie"] = req.cookieHolder;
delete req.cookieHolder;
}
});
io.on("connect", (socket) => {
const req = socket.request;
socket.join(req.session.id);
socket.on("incr", (cb) => {
req.session.reload((err) => {
if (err) {
// session has expired
return socket.disconnect();
}
req.session.count = (req.session.count || 0) + 1;
req.session.save(() => {
cb(req.session.count);
});
});
});
});
httpServer.listen(port, () => {
console.log(`application is running at: http://localhost:${port}`);
});

View File

@@ -0,0 +1,15 @@
{
"name": "express-session-example",
"version": "0.0.1",
"private": true,
"type": "module",
"description": "Example with express-session (https://github.com/expressjs/session)",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "~4.17.3",
"express-session": "~1.17.2",
"socket.io": "~4.4.1"
}
}

View File

@@ -0,0 +1 @@
bundle.js

View File

@@ -0,0 +1,3 @@
import { Server } from "socket.io";
new Server(0);

View File

@@ -0,0 +1,19 @@
{
"name": "rollup-server-bundle",
"version": "0.0.1",
"description": "",
"main": "index.js",
"type": "module",
"author": "Damien Arrachequesne <damien.arrachequesne@gmail.com>",
"license": "ISC",
"scripts": {
"build": "rollup --config rollup.config.js"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.3",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.1.3",
"rollup": "^2.70.1",
"socket.io": "^4.4.1"
}
}

View File

@@ -0,0 +1,12 @@
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import json from "@rollup/plugin-json";
export default {
input: "index.js",
output: {
file: "bundle.js",
format: "esm",
},
plugins: [resolve(), commonjs(), json({ compact: true })],
};

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)",
"scripts": {
"start": "node dist/server.js",
"build": "webpack --config ./support/webpack.config.js"
"build": "webpack"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"devDependencies": {
"bufferutil": "^4.0.3",
"socket.io": "^4.0.0",
"webpack": "~4.43.0",
"webpack-cli": "~3.3.11"
"utf-8-validate": "^5.0.5",
"webpack": "^5.39.0",
"webpack-cli": "^4.7.2"
}
}

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

@@ -7,14 +7,5 @@ A sample Webpack build for the browser.
```
$ npm i
$ npm run build-all
$ npm run build
```
There are two WebPack configuration:
- the minimal configuration, just bundling the application and its dependencies. The `app.js` file in the `dist` folder is the result of that build.
- a slimmer one, where:
- the JSON polyfill needed for IE6/IE7 support has been removed.
- the `debug` calls and import have been removed (the [debug](https://github.com/visionmedia/debug) library is included in the build by default).
- the source has been uglified (dropping IE8 support), and an associated SourceMap has been generated.

View File

@@ -6,8 +6,7 @@
</head>
<body>
<!-- <script src="dist/app.js"></script> -->
<script src="dist/app.slim.js"></script>
<script src="dist/bundle.js"></script>
</body>
</html>
</html>

View File

@@ -0,0 +1,15 @@
import { io } from "socket.io-client";
const socket = io("http://localhost:3000");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
socket.on("connect_error", (err) => {
console.log(`connect_error due to ${err.message}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnect due to ${reason}`);
});

View File

@@ -1,12 +0,0 @@
import io from 'socket.io-client';
const socket = io('http://localhost:3000');
console.log('init');
socket.on('connect', onConnect);
function onConnect(){
console.log('connect ' + socket.id);
}

View File

@@ -2,20 +2,15 @@
"name": "webpack-build",
"version": "1.0.0",
"description": "A sample Webpack build",
"type": "module",
"scripts": {
"build": "webpack --config ./support/webpack.config.js",
"build-slim": "webpack --config ./support/webpack.config.slim.js",
"build-json-parser": "webpack --config ./support/webpack.config.json-parser.js",
"build-all": "npm run build && npm run build-slim && npm run build-json-parser"
"build": "webpack"
},
"author": "Damien Arrachequesne",
"license": "MIT",
"dependencies": {
"socket.io-client": "^2.0.2",
"socket.io-json-parser": "^2.1.0"
},
"devDependencies": {
"strip-loader": "^0.1.2",
"webpack": "^2.6.1"
"socket.io-client": "^4.4.1",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
}
}

View File

@@ -1,2 +0,0 @@
module.exports = function () { return function () {}; };

View File

@@ -1,8 +0,0 @@
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.js'
}
};

View File

@@ -1,33 +0,0 @@
var webpack = require('webpack');
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.json-parser.js'
},
// generate sourcemap
devtool: 'source-map',
plugins: [
// replace require('debug')() with an noop function
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
// replace socket.io-parser with socket.io-json-parser
new webpack.NormalModuleReplacementPlugin(/socket\.io-parser/, 'socket.io-json-parser'),
// use uglifyJS (IE9+ support)
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
],
module: {
loaders: [
{
// strip `debug()` calls
test: /\.js$/,
loader: 'strip-loader?strip[]=debug'
}
]
}
};

View File

@@ -1,31 +0,0 @@
var webpack = require('webpack');
module.exports = {
entry: './lib/index.js',
output: {
path: require('path').join(__dirname, '../dist'),
filename: 'app.slim.js'
},
// generate sourcemap
devtool: 'source-map',
plugins: [
// replace require('debug')() with an noop function
new webpack.NormalModuleReplacementPlugin(/debug/, process.cwd() + '/support/noop.js'),
// use uglifyJS (IE9+ support)
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
})
],
module: {
loaders: [
{
// strip `debug()` calls
test: /\.js$/,
loader: 'strip-loader?strip[]=debug'
}
]
}
};

View File

@@ -0,0 +1,7 @@
export default {
entry: "./index.js",
mode: "production",
output: {
filename: "bundle.js",
},
};

View File

@@ -9,8 +9,9 @@ import type {
TypedEventBroadcaster,
} from "./typed-events";
export class BroadcastOperator<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents> {
export class BroadcastOperator<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents>
{
constructor(
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
@@ -21,18 +22,27 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public to(room: Room | Room[]) {
const rooms = new Set(this.rooms);
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r));
} else {
rooms.add(room);
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
rooms,
this.exceptRooms,
@@ -41,31 +51,43 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // disconnect all clients in the "room-101" room
* io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public in(room: Room | Room[]) {
return this.to(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public except(room: Room | Room[]) {
const exceptRooms = new Set(this.exceptRooms);
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r));
} else {
exceptRooms.add(room);
}
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
exceptRooms,
@@ -76,13 +98,15 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
public compress(compress: boolean) {
const flags = Object.assign({}, this.flags, { compress });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -95,12 +119,14 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
public get volatile() {
const flags = Object.assign({}, this.flags, { volatile: true });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -111,12 +137,39 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return a new BroadcastOperator instance
* @public
* @example
* // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents> {
public get local() {
const flags = Object.assign({}, this.flags, { local: true });
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
flags
);
}
/**
* Adds a timeout in milliseconds for the next operation
*
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/
public timeout(timeout: number) {
const flags = Object.assign({}, this.flags, { timeout });
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
this.rooms,
this.exceptRooms,
@@ -127,15 +180,30 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Emits to all clients.
*
* @example
* // the “foo” event will be broadcast to all connected clients
* io.emit("foo", "bar");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an acknowledgement expected from all connected clients
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
throw new Error(`"${String(ev)}" is a reserved event name`);
}
// set up packet object
const data = [ev, ...args];
@@ -144,14 +212,65 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
data: data,
};
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting");
const withAck = typeof data[data.length - 1] === "function";
if (!withAck) {
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
});
return true;
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
const ack = data.pop() as (...args: any[]) => void;
let timedOut = false;
let responses: any[] = [];
const timer = setTimeout(() => {
timedOut = true;
ack.apply(this, [new Error("operation has timed out"), responses]);
}, this.flags.timeout);
let expectedServerCount = -1;
let actualServerCount = 0;
let expectedClientCount = 0;
const checkCompleteness = () => {
if (
!timedOut &&
expectedServerCount === actualServerCount &&
responses.length === expectedClientCount
) {
clearTimeout(timer);
ack.apply(this, [null, responses]);
}
};
this.adapter.broadcastWithAck(
packet,
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
(clientCount) => {
// each Socket.IO server in the cluster sends the number of clients that were notified
expectedClientCount += clientCount;
actualServerCount++;
checkCompleteness();
},
(clientResponse) => {
// each client sends an acknowledgement
responses.push(clientResponse);
checkCompleteness();
}
);
this.adapter.serverCount().then((serverCount) => {
expectedServerCount = serverCount;
checkCompleteness();
});
return true;
@@ -160,7 +279,8 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Gets a list of clients.
*
* @public
* @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
@@ -172,71 +292,122 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
}
/**
* Returns the matching socket instances
* Returns the matching socket instances. This method works across a cluster of several Socket.IO servers.
*
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return (socket as unknown) as RemoteSocket<EmitEvents>;
return socket as unknown as RemoteSocket<EmitEvents, SocketData>;
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails);
return new RemoteSocket(
this.adapter,
socket as SocketDetails<SocketData>
);
}
});
});
}
/**
* Makes the matching socket instances join the specified rooms
* Makes the matching socket instances join the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
}
/**
* Makes the matching socket instances leave the specified rooms
* Makes the matching socket instances leave the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
Array.isArray(room) ? room : [room]
);
}
/**
* Makes the matching socket instances disconnect
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
},
close
);
@@ -246,31 +417,35 @@ export class BroadcastOperator<EmitEvents extends EventsMap>
/**
* Format of the data when the Socket instance exists on another Socket.IO server
*/
interface SocketDetails {
interface SocketDetails<SocketData> {
id: SocketId;
handshake: Handshake;
rooms: Room[];
data: any;
data: SocketData;
}
/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents> {
export class RemoteSocket<EmitEvents extends EventsMap, SocketData>
implements TypedEventBroadcaster<EmitEvents>
{
public readonly id: SocketId;
public readonly handshake: Handshake;
public readonly rooms: Set<Room>;
public readonly data: any;
public readonly data: SocketData;
private readonly operator: BroadcastOperator<EmitEvents>;
private readonly operator: BroadcastOperator<EmitEvents, SocketData>;
constructor(adapter: Adapter, details: SocketDetails) {
constructor(adapter: Adapter, details: SocketDetails<SocketData>) {
this.id = details.id;
this.handshake = details.handshake;
this.rooms = new Set(details.rooms);
this.data = details.data;
this.operator = new BroadcastOperator(adapter, new Set([this.id]));
this.operator = new BroadcastOperator<EmitEvents, SocketData>(
adapter,
new Set([this.id])
);
}
public emit<Ev extends EventNames<EmitEvents>>(
@@ -284,7 +459,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room);
@@ -294,7 +468,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room);
@@ -305,8 +478,6 @@ export class RemoteSocket<EmitEvents extends EventsMap>
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close);

View File

@@ -7,21 +7,49 @@ import type { Namespace } from "./namespace";
import type { EventsMap } from "./typed-events";
import type { Socket } from "./socket";
import type { SocketId } from "socket.io-adapter";
import type { Socket as RawSocket } from "engine.io";
const debug = debugModule("socket.io:client");
interface WriteOptions {
compress?: boolean;
volatile?: boolean;
preEncoded?: boolean;
wsPreEncoded?: string;
}
type CloseReason =
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error";
export class Client<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
SocketData = any
> {
public readonly conn;
public readonly conn: RawSocket;
private readonly id: string;
private readonly server: Server<ListenEvents, EmitEvents>;
private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
private readonly encoder: Encoder;
private readonly decoder: Decoder;
private sockets: Map<SocketId, Socket<ListenEvents, EmitEvents>> = new Map();
private nsps: Map<string, Socket<ListenEvents, EmitEvents>> = new Map();
private sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private nsps: Map<
string,
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private connectTimeout?: NodeJS.Timeout;
/**
@@ -31,7 +59,10 @@ export class Client<
* @param conn
* @package
*/
constructor(server: Server<ListenEvents, EmitEvents>, conn: any) {
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
conn: any
) {
this.server = server;
this.conn = conn;
this.encoder = server.encoder;
@@ -92,9 +123,12 @@ export class Client<
this.server._checkNamespace(
name,
auth,
(dynamicNspName: Namespace<ListenEvents, EmitEvents> | false) => {
(
dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false
) => {
if (dynamicNspName) {
debug("dynamic namespace %s was created", dynamicNspName);
this.doConnect(name, auth);
} else {
debug("creation of namespace %s was denied", name);
@@ -150,7 +184,9 @@ export class Client<
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
_remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name;
this.sockets.delete(socket.id);
@@ -180,30 +216,32 @@ export class Client<
* @param {Object} opts
* @private
*/
_packet(packet: Packet, opts?: any): void {
opts = opts || {};
const self = this;
// this writes to the actual connection
function writeToEngine(encodedPackets: any) {
// TODO clarify this.
if (opts.volatile && !self.conn.transport.writable) return;
for (let i = 0; i < encodedPackets.length; i++) {
self.conn.write(encodedPackets[i], { compress: opts.compress });
}
}
if ("open" === this.conn.readyState) {
debug("writing packet %j", packet);
if (!opts.preEncoded) {
// not broadcasting, need to encode
writeToEngine(this.encoder.encode(packet)); // encode, then write results to engine
} else {
// a broadcast pre-encodes a packet
writeToEngine(packet);
}
} else {
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") {
debug("ignoring packet write %j", packet);
return;
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet);
this.writeToEngine(encodedPackets, opts);
}
private writeToEngine(
encodedPackets: Array<String | Buffer>,
opts: WriteOptions
): void {
if (opts.volatile && !this.conn.transport.writable) {
debug(
"volatile packet is discarded since the transport is not currently writable"
);
return;
}
const packets = Array.isArray(encodedPackets)
? encodedPackets
: [encodedPackets];
for (const encodedPacket of packets) {
this.conn.write(encodedPacket, opts);
}
}
@@ -217,6 +255,7 @@ export class Client<
try {
this.decoder.add(data);
} catch (e) {
debug("invalid packet format");
this.onerror(e);
}
}
@@ -227,22 +266,31 @@ export class Client<
* @private
*/
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
this.connect(parsed.pathname!, parsed.query);
} else {
this.connect(packet.nsp, packet.data);
}
let namespace: string;
let authPayload;
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true);
namespace = parsed.pathname!;
authPayload = parsed.query;
} else {
const socket = this.nsps.get(packet.nsp);
if (socket) {
process.nextTick(function () {
socket._onpacket(packet);
});
} else {
debug("no socket for namespace %s", packet.nsp);
}
namespace = packet.nsp;
authPayload = packet.data;
}
const socket = this.nsps.get(namespace);
if (!socket && packet.type === PacketType.CONNECT) {
this.connect(namespace, authPayload);
} else if (
socket &&
packet.type !== PacketType.CONNECT &&
packet.type !== PacketType.CONNECT_ERROR
) {
process.nextTick(function () {
socket._onpacket(packet);
});
} else {
debug("invalid state (packet type: %s)", packet.type);
this.close();
}
}
@@ -265,7 +313,7 @@ export class Client<
* @param reason
* @private
*/
private onclose(reason: string): void {
private onclose(reason: CloseReason | "forced server close"): void {
debug("client close with reason %s", reason);
// ignore a potential subsequent `close` event

View File

@@ -1,146 +1,51 @@
import http = require("http");
import type { Server as HTTPSServer } from "https";
import type { Http2SecureServer } from "http2";
import { createReadStream } from "fs";
import { createDeflate, createGzip, createBrotliCompress } from "zlib";
import accepts = require("accepts");
import { pipeline } from "stream";
import path = require("path");
import engine = require("engine.io");
import {
attach,
Server as Engine,
ServerOptions as EngineOptions,
AttachOptions,
uServer,
} from "engine.io";
import { Client } from "./client";
import { EventEmitter } from "events";
import {
ExtendedError,
Namespace,
NamespaceReservedEventsMap,
} from "./namespace";
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace";
import { ParentNamespace } from "./parent-namespace";
import { Adapter, Room, SocketId } from "socket.io-adapter";
import * as parser from "socket.io-parser";
import type { Encoder } from "socket.io-parser";
import debugModule from "debug";
import { Socket } from "./socket";
import type { CookieSerializeOptions } from "cookie";
import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator";
import {
EventsMap,
DefaultEventsMap,
EventParams,
StrictEventEmitter,
EventNames,
} from "./typed-events";
import { patchAdapter, restoreAdapter, serveFile } from "./uws";
const debug = debugModule("socket.io:server");
const clientVersion = require("../package.json").version;
const dotMapRegex = /\.map/;
type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
auth: { [key: string]: any },
fn: (err: Error | null, success: boolean) => void
) => void;
interface EngineOptions {
/**
* 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;
}
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter);
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 {
interface ServerOptions extends EngineOptions, AttachOptions {
/**
* name of the path to capture
* @default "/socket.io"
@@ -155,7 +60,7 @@ interface ServerOptions extends EngineAttachOptions {
* the adapter to use
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
*/
adapter: any;
adapter: AdapterConstructor;
/**
* the parser to use
* @default the default parser (https://github.com/socketio/socket.io-parser)
@@ -168,23 +73,58 @@ interface ServerOptions extends EngineAttachOptions {
connectTimeout: number;
}
/**
* Represents a Socket.IO server.
*
* @example
* import { Server } from "socket.io";
*
* const io = new Server();
*
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*
* io.listen(3000);
*/
export class Server<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
{},
ServerSideEvents,
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
ServerReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly sockets: Namespace<ListenEvents, EmitEvents>;
public readonly sockets: Namespace<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
/**
* A reference to the underlying Engine.IO server.
*
* Example:
*
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
* @example
* const clientsCount = io.engine.clientsCount;
*
*/
public engine: any;
@@ -197,15 +137,18 @@ export class Server<
/**
* @private
*/
_nsps: Map<string, Namespace<ListenEvents, EmitEvents>> = new Map();
_nsps: Map<
string,
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents>
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
private _adapter?: typeof Adapter;
private _adapter?: AdapterConstructor;
private _serveClient: boolean;
private opts: Partial<EngineOptions>;
private eio;
private eio: Engine;
private _path: string;
private clientPathRegex: RegExp;
@@ -213,23 +156,37 @@ export class Server<
* @private
*/
_connectTimeout: number;
private httpServer: http.Server;
private httpServer: http.Server | HTTPSServer | Http2SecureServer;
/**
* Server constructor.
*
* @param srv http server, port, or options
* @param [opts]
* @public
*/
constructor(opts?: Partial<ServerOptions>);
constructor(srv?: http.Server | number, opts?: Partial<ServerOptions>);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
srv?: http.Server | HTTPSServer | Http2SecureServer | number,
opts?: Partial<ServerOptions>
);
constructor(
srv: undefined | Partial<ServerOptions> | http.Server | number,
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
opts?: Partial<ServerOptions>
);
constructor(
srv:
| undefined
| Partial<ServerOptions>
| http.Server
| HTTPSServer
| Http2SecureServer
| number,
opts: Partial<ServerOptions> = {}
) {
super();
@@ -249,7 +206,10 @@ export class Server<
this.adapter(opts.adapter || Adapter);
this.sockets = this.of("/");
this.opts = opts;
if (srv) this.attach(srv as http.Server);
if (srv || typeof srv == "number")
this.attach(
srv as http.Server | HTTPSServer | Http2SecureServer | number
);
}
/**
@@ -257,7 +217,6 @@ export class Server<
*
* @param v - whether to serve client code
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): this;
public serveClient(): boolean;
@@ -280,7 +239,11 @@ export class Server<
_checkNamespace(
name: string,
auth: { [key: string]: any },
fn: (nsp: Namespace<ListenEvents, EmitEvents> | false) => void
fn: (
nsp:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
| false
) => void
): void {
if (this.parentNsps.size === 0) return fn(false);
@@ -293,10 +256,18 @@ export class Server<
}
nextFn.value(name, auth, (err, allow) => {
if (err || !allow) {
run();
} else {
fn(this.parentNsps.get(nextFn.value)!.createChild(name));
return run();
}
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);
});
};
@@ -308,7 +279,6 @@ export class Server<
*
* @param {String} v pathname
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): this;
public path(): string;
@@ -322,7 +292,7 @@ export class Server<
this.clientPathRegex = new RegExp(
"^" +
escapedPath +
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
"/socket\\.io(\\.msgpack|\\.esm)?(\\.min)?\\.js(\\.map)?(?:\\?|$)"
);
return this;
}
@@ -330,7 +300,6 @@ export class Server<
/**
* Set the delay after which a client without namespace is closed
* @param v
* @public
*/
public connectTimeout(v: number): this;
public connectTimeout(): number;
@@ -346,12 +315,12 @@ export class Server<
*
* @param v pathname
* @return self when setting or value when getting
* @public
*/
public adapter(): typeof Adapter | undefined;
public adapter(v: typeof Adapter): this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this;
public adapter(v?: typeof Adapter): typeof Adapter | undefined | this {
public adapter(): AdapterConstructor | undefined;
public adapter(v: AdapterConstructor): this;
public adapter(
v?: AdapterConstructor
): AdapterConstructor | undefined | this {
if (!arguments.length) return this._adapter;
this._adapter = v;
for (const nsp of this._nsps.values()) {
@@ -366,10 +335,9 @@ export class Server<
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public listen(
srv: http.Server | number,
srv: http.Server | HTTPSServer | Http2SecureServer | number,
opts: Partial<ServerOptions> = {}
): this {
return this.attach(srv, opts);
@@ -381,10 +349,9 @@ export class Server<
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public attach(
srv: http.Server | number,
srv: http.Server | HTTPSServer | Http2SecureServer | number,
opts: Partial<ServerOptions> = {}
): this {
if ("function" == typeof srv) {
@@ -419,6 +386,69 @@ export class Server<
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
*
@@ -427,12 +457,12 @@ export class Server<
* @private
*/
private initEngine(
srv: http.Server,
opts: Partial<EngineAttachOptions>
srv: http.Server | HTTPSServer | Http2SecureServer,
opts: EngineOptions & AttachOptions
): void {
// initialize engine
debug("creating engine.io instance with opts %j", opts);
this.eio = engine.attach(srv, opts);
this.eio = attach(srv, opts);
// attach static file serving
if (this._serveClient) this.attachServe(srv);
@@ -450,13 +480,15 @@ export class Server<
* @param srv http server
* @private
*/
private attachServe(srv: http.Server): void {
private attachServe(
srv: http.Server | HTTPSServer | Http2SecureServer
): void {
debug("attaching client serving req handler");
const evs = srv.listeners("request").slice(0);
srv.removeAllListeners("request");
srv.on("request", (req, res) => {
if (this.clientPathRegex.test(req.url)) {
if (this.clientPathRegex.test(req.url!)) {
this.serve(req, res);
} else {
for (let i = 0; i < evs.length; i++) {
@@ -474,7 +506,7 @@ export class Server<
* @private
*/
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 type = isMap ? "map" : "source";
@@ -502,9 +534,6 @@ export class Server<
);
res.setHeader("ETag", expectedEtag);
if (!isMap) {
res.setHeader("X-SourceMap", filename.substring(1) + ".map");
}
Server.sendFile(filename, req, res);
}
@@ -555,7 +584,6 @@ export class Server<
*
* @param {engine.Server} engine engine.io (or compatible) server
* @return self
* @public
*/
public bind(engine): this {
this.engine = engine;
@@ -583,14 +611,27 @@ export class Server<
/**
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @example
* // with a simple string
* const myNamespace = io.of("/my-namespace");
*
* // with a regex
* const dynamicNsp = io.of(/^\/dynamic-\d+$/).on("connection", (socket) => {
* const namespace = socket.nsp; // newNamespace.name === "/dynamic-101"
*
* // broadcast to all clients in the given sub-namespace
* namespace.emit("hello");
* });
*
* @param name - nsp name
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents>) => void
): Namespace<ListenEvents, EmitEvents> {
fn?: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
if (typeof name === "function" || name instanceof RegExp) {
const parentNsp = new ParentNamespace(this);
debug("initializing parent namespace %s", parentNsp.name);
@@ -616,6 +657,10 @@ export class Server<
debug("initializing namespace %s", name);
nsp = new Namespace(this, name);
this._nsps.set(name, nsp);
if (name !== "/") {
// @ts-ignore
this.sockets.emitReserved("new_namespace", nsp);
}
}
if (fn) nsp.on("connect", fn);
return nsp;
@@ -625,7 +670,6 @@ export class Server<
* Closes server connection
*
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
@@ -634,6 +678,9 @@ export class Server<
this.engine.close();
// restore the Adapter prototype
restoreAdapter();
if (this.httpServer) {
this.httpServer.close(fn);
} else {
@@ -642,14 +689,19 @@ export class Server<
}
/**
* Sets up namespace middleware.
* Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
*
* @return self
* @public
* @example
* io.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
): this {
@@ -660,42 +712,71 @@ export class Server<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* io.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* io.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public to(room: Room | Room[]) {
return this.sockets.to(room);
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* // disconnect all clients in the "room-101" room
* io.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public in(room: Room | Room[]) {
return this.sockets.in(room);
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
* @example
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* io.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* io.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* io.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(name: Room | Room[]): Server<ListenEvents, EmitEvents> {
this.sockets.except(name);
return this;
public except(room: Room | Room[]) {
return this.sockets.except(room);
}
/**
* Sends a `message` event to all clients.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.send("hello");
*
* // this is equivalent to
* io.emit("message", "hello");
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
@@ -703,20 +784,53 @@ export class Server<
}
/**
* Sends a `message` event to all clients.
* Sends a `message` event to all clients. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args);
return this;
}
/**
* Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* io.serverSideEmit("hello", "world");
*
* io.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* io.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* io.on("ping", (cb) => {
* cb("pong");
* });
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
return this.sockets.serverSideEmit(ev, ...args);
}
/**
* Gets a list of socket ids.
*
* @public
* @deprecated this method will be removed in the next major release, please use {@link Server#serverSideEmit} or
* {@link Server#fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
return this.sockets.allSockets();
@@ -725,11 +839,13 @@ export class Server<
/**
* Sets the compress flag.
*
* @example
* io.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
* @return a new {@link BroadcastOperator} instance for chaining
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
public compress(compress: boolean) {
return this.sockets.compress(compress);
}
@@ -738,59 +854,126 @@ export class Server<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
* @example
* io.volatile.emit("hello"); // the clients may or may not receive it
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get volatile(): BroadcastOperator<EmitEvents> {
public get volatile() {
return this.sockets.volatile;
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
* @example
* // the “foo” event will be broadcast to all connected clients on this node
* io.local.emit("foo", "bar");
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents> {
public get local() {
return this.sockets.local;
}
/**
* Returns the matching socket instances
* Adds a timeout in milliseconds for the next operation.
*
* @public
* @example
* io.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
public timeout(timeout: number) {
return this.sockets.timeout(timeout);
}
/**
* Returns the matching socket instances.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // return all Socket instances
* const sockets = await io.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await io.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
return this.sockets.fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
* Makes the matching socket instances join the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
*
* // make all socket instances join the "room1" room
* io.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* io.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]): void {
public socketsJoin(room: Room | Room[]) {
return this.sockets.socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms
* Makes the matching socket instances leave the specified rooms.
*
* @param room
* @public
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances leave the "room1" room
* io.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* io.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]): void {
public socketsLeave(room: Room | Room[]) {
return this.sockets.socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* io.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* io.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
public disconnectSockets(close: boolean = false) {
return this.sockets.disconnectSockets(close);
}
}
@@ -817,3 +1000,4 @@ module.exports.Namespace = Namespace;
module.exports.Socket = Socket;
export { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket };
export { Event } from "./socket";

View File

@@ -20,35 +20,126 @@ export interface ExtendedError extends Error {
export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
SocketData
> {
connect: (socket: Socket<ListenEvents, EmitEvents>) => void;
connection: (socket: Socket<ListenEvents, EmitEvents>) => void;
connect: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void;
connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void;
}
export interface ServerReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap,
SocketData
> extends NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
> {
new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) => void;
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never, never>
>(<const>["connect", "connection", "new_namespace"]);
/**
* A Namespace is a communication channel that allows you to split the logic of your application over a single shared
* connection.
*
* Each namespace has its own:
*
* - event handlers
*
* ```
* io.of("/orders").on("connection", (socket) => {
* socket.on("order:list", () => {});
* socket.on("order:create", () => {});
* });
*
* io.of("/users").on("connection", (socket) => {
* socket.on("user:list", () => {});
* });
* ```
*
* - rooms
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.on("connection", (socket) => {
* socket.join("room1");
* orderNamespace.to("room1").emit("hello");
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.on("connection", (socket) => {
* socket.join("room1"); // distinct from the room in the "orders" namespace
* userNamespace.to("room1").emit("holà");
* });
* ```
*
* - middlewares
*
* ```
* const orderNamespace = io.of("/orders");
*
* orderNamespace.use((socket, next) => {
* // ensure the socket has access to the "orders" namespace
* });
*
* const userNamespace = io.of("/users");
*
* userNamespace.use((socket, next) => {
* // ensure the socket has access to the "users" namespace
* });
* ```
*/
export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
{},
ServerSideEvents,
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents>
NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>
> {
public readonly name: string;
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents>
Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Map();
public adapter: Adapter;
/** @private */
readonly server: Server<ListenEvents, EmitEvents>;
readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
/** @private */
_fns: Array<
(
socket: Socket<ListenEvents, EmitEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
> = [];
@@ -62,7 +153,10 @@ export class Namespace<
* @param server instance
* @param name
*/
constructor(server: Server<ListenEvents, EmitEvents>, name: string) {
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
name: string
) {
super();
this.server = server;
this.name = name;
@@ -77,18 +171,26 @@ export class Namespace<
* @private
*/
_initAdapter(): void {
// @ts-ignore
this.adapter = new (this.server.adapter()!)(this);
}
/**
* Sets up namespace middleware.
* Registers a middleware, which is a function that gets executed for every incoming {@link Socket}.
*
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.use((socket, next) => {
* // ...
* next();
* });
*
* @param fn - the middleware function
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
next: (err?: ExtendedError) => void
) => void
): this {
@@ -104,7 +206,7 @@ export class Namespace<
* @private
*/
private run(
socket: Socket<ListenEvents, EmitEvents>,
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0);
@@ -129,34 +231,63 @@ export class Namespace<
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // the “foo” event will be broadcast to all connected clients in the “room-101” room
* myNamespace.to("room-101").emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* myNamespace.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.to("room-101").to("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).to(room);
public to(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).to(room);
}
/**
* Targets a room when emitting.
* Targets a room when emitting. Similar to `to()`, but might feel clearer in some cases:
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // disconnect all clients in the "room-101" room
* myNamespace.in("room-101").disconnectSockets();
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).in(room);
public in(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).in(room);
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* myNamespace.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* myNamespace.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* myNamespace.except("room-101").except("room-102").emit("foo", "bar");
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).except(room);
public except(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).except(
room
);
}
/**
@@ -166,42 +297,46 @@ export class Namespace<
* @private
*/
_add(
client: Client<ListenEvents, EmitEvents>,
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: () => void
): Socket<ListenEvents, EmitEvents> {
): Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
debug("adding socket to nsp %s", this.name);
const socket = new Socket(this, client, query);
this.run(socket, (err) => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err) {
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message);
} else {
return socket._error({
message: err.message,
data: err.data,
});
}
}
// track socket
this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect();
if (fn) fn();
// fire user-set events
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
} else {
if ("open" !== client.conn.readyState) {
debug("next called after client was closed - ignoring socket");
socket._cleanup();
return;
}
if (err) {
debug("middleware error, sending CONNECT_ERROR packet to the client");
socket._cleanup();
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message);
} else {
return socket._error({
message: err.message,
data: err.data,
});
}
}
// track socket
this.sockets.set(socket.id, socket);
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect();
if (fn) fn();
// fire user-set events
this.emitReserved("connect", socket);
this.emitReserved("connection", socket);
});
});
return socket;
@@ -212,7 +347,9 @@ export class Namespace<
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents>): void {
_remove(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
): void {
if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id);
} else {
@@ -221,23 +358,53 @@ export class Namespace<
}
/**
* Emits to all clients.
* Emits to all connected clients.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* myNamespace.emit("hello", 1, "2", { 3: ["4"], 5: Uint8Array.from([6]) });
*
* // with an acknowledgement from the clients
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args);
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).emit(
ev,
...args
);
}
/**
* Sends a `message` event to all clients.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.send("hello");
*
* // this is equivalent to
* myNamespace.emit("message", "hello");
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -245,35 +412,93 @@ export class Namespace<
}
/**
* Sends a `message` event to all clients.
* Sends a `message` event to all clients. Sends a `message` event. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
return this;
}
/**
* Sends a message to the other Socket.IO servers of the cluster.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.serverSideEmit("hello", "world");
*
* myNamespace.on("hello", (arg1) => {
* console.log(arg1); // prints "world"
* });
*
* // acknowledgements (without binary content) are supported too:
* myNamespace.serverSideEmit("ping", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* myNamespace.on("ping", (cb) => {
* cb("pong");
* });
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${String(ev)}" is a reserved event name`);
}
args.unshift(ev);
this.adapter.serverSideEmit(args);
return true;
}
/**
* Called when a packet is received from another Socket.IO server
*
* @param args - an array of arguments, which may include an acknowledgement callback at the end
*
* @private
*/
_onServerSideEmit(args: [string, ...any[]]) {
super.emitUntyped.apply(this, args);
}
/**
* Gets a list of clients.
*
* @return self
* @public
* @deprecated this method will be removed in the next major release, please use {@link Namespace#serverSideEmit} or
* {@link Namespace#fetchSockets} instead.
*/
public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets();
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).allSockets();
}
/**
* Sets the compress flag.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.compress(false).emit("hello");
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).compress(compress);
public compress(compress: boolean) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).compress(
compress
);
}
/**
@@ -281,59 +506,149 @@ export class Namespace<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.volatile.emit("hello"); // the clients may or may not receive it
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).volatile;
public get volatile() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).volatile;
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local;
}
/**
* Returns the matching socket instances
* @example
* const myNamespace = io.of("/my-namespace");
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms
* // the “foo” event will be broadcast to all connected clients on this node
* myNamespace.local.emit("foo", "bar");
*
* @param room
* @public
* @return a new {@link BroadcastOperator} instance for chaining
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room);
public get local() {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).local;
}
/**
* Makes the matching socket instances leave the specified rooms
* Adds a timeout in milliseconds for the next operation.
*
* @param room
* @public
* @example
* const myNamespace = io.of("/my-namespace");
*
* myNamespace.timeout(1000).emit("some-event", (err, responses) => {
* if (err) {
* // some clients did not acknowledge the event in the given delay
* } else {
* console.log(responses); // one response per client
* }
* });
*
* @param timeout
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room);
public timeout(timeout: number) {
return new BroadcastOperator<EmitEvents, SocketData>(this.adapter).timeout(
timeout
);
}
/**
* Makes the matching socket instances disconnect
* Returns the matching socket instances.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // return all Socket instances
* const sockets = await myNamespace.fetchSockets();
*
* // return all Socket instances in the "room1" room
* const sockets = await myNamespace.in("room1").fetchSockets();
*
* for (const socket of sockets) {
* console.log(socket.id);
* console.log(socket.handshake);
* console.log(socket.rooms);
* console.log(socket.data);
*
* socket.emit("hello");
* socket.join("room1");
* socket.leave("room2");
* socket.disconnect();
* }
*/
public fetchSockets() {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).fetchSockets();
}
/**
* Makes the matching socket instances join the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances join the "room1" room
* myNamespace.socketsJoin("room1");
*
* // make all socket instances in the "room1" room join the "room2" and "room3" rooms
* myNamespace.in("room1").socketsJoin(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsJoin(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsJoin(room);
}
/**
* Makes the matching socket instances leave the specified rooms.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances leave the "room1" room
* myNamespace.socketsLeave("room1");
*
* // make all socket instances in the "room1" room leave the "room2" and "room3" rooms
* myNamespace.in("room1").socketsLeave(["room2", "room3"]);
*
* @param room - a room, or an array of rooms
*/
public socketsLeave(room: Room | Room[]) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).socketsLeave(room);
}
/**
* Makes the matching socket instances disconnect.
*
* Note: this method also works within a cluster of multiple Socket.IO servers, with a compatible {@link Adapter}.
*
* @example
* const myNamespace = io.of("/my-namespace");
*
* // make all socket instances disconnect (the connections might be kept alive for other namespaces)
* myNamespace.disconnectSockets();
*
* // make all socket instances in the "room1" room disconnect and close the underlying connections
* myNamespace.in("room1").disconnectSockets(true);
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close);
public disconnectSockets(close: boolean = false) {
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter
).disconnectSockets(close);
}
}

View File

@@ -1,5 +1,5 @@
import { Namespace } from "./namespace";
import type { Server } from "./index";
import type { Server, RemoteSocket } from "./index";
import type {
EventParams,
EventNames,
@@ -10,12 +10,18 @@ import type { BroadcastOptions } from "socket.io-adapter";
export class ParentNamespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
> extends Namespace<ListenEvents, EmitEvents> {
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
private static count: number = 0;
private children: Set<Namespace<ListenEvents, EmitEvents>> = new Set();
private children: Set<
Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
> = new Set();
constructor(server: Server<ListenEvents, EmitEvents>) {
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents, SocketData>
) {
super(server, "/_" + ParentNamespace.count++);
}
@@ -43,7 +49,9 @@ export class ParentNamespace<
return true;
}
createChild(name: string): Namespace<ListenEvents, EmitEvents> {
createChild(
name: string
): Namespace<ListenEvents, EmitEvents, ServerSideEvents, SocketData> {
const namespace = new Namespace(this.server, name);
namespace._fns = this._fns.slice(0);
this.listeners("connect").forEach((listener) =>
@@ -56,4 +64,13 @@ export class ParentNamespace<
this.server._nsps.set(name, namespace);
return namespace;
}
fetchSockets(): Promise<RemoteSocket<EmitEvents, SocketData>[]> {
// note: we could make the fetchSockets() method work for dynamic namespaces created with a regex (by sending the
// regex to the other Socket.IO servers, and returning the sockets of each matching namespace for example), but
// the behavior for namespaces created with a function is less clear
// note²: we cannot loop over each children namespace, because with multiple Socket.IO servers, a given namespace
// may exist on one node but not exist on another (since it is created upon client connection)
throw new Error("fetchSockets() is not supported on parent namespaces");
}
}

View File

@@ -1,5 +1,4 @@
import { Packet, PacketType } from "socket.io-parser";
import url = require("url");
import debugModule from "debug";
import type { Server } from "./index";
import {
@@ -26,9 +25,23 @@ const debug = debugModule("socket.io:socket");
type ClientReservedEvents = "connect_error";
// TODO for next major release: cleanup disconnect reasons
export type DisconnectReason =
// Engine.IO close reasons
| "transport error"
| "transport close"
| "forced close"
| "ping timeout"
| "parse error"
// Socket.IO disconnect reasons
| "server shutting down"
| "forced server close"
| "client namespace disconnect"
| "server namespace disconnect";
export interface SocketReservedEventsMap {
disconnect: (reason: string) => void;
disconnecting: (reason: string) => void;
disconnect: (reason: DisconnectReason) => void;
disconnecting: (reason: DisconnectReason) => void;
error: (err: Error) => void;
}
@@ -46,7 +59,7 @@ export interface EventEmitterReservedEventsMap {
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
| ClientReservedEvents
| keyof NamespaceReservedEventsMap<never, never>
| keyof NamespaceReservedEventsMap<never, never, never, never>
| keyof SocketReservedEventsMap
| keyof EventEmitterReservedEventsMap
>(<const>[
@@ -108,32 +121,94 @@ export interface Handshake {
auth: { [key: string]: any };
}
/**
* `[eventName, ...args]`
*/
export type Event = [string, ...any[]];
function noop() {}
/**
* This is the main object for interacting with a client.
*
* A Socket belongs to a given {@link Namespace} and uses an underlying {@link Client} to communicate.
*
* Within each {@link Namespace}, you can also define arbitrary channels (called "rooms") that the {@link Socket} can
* join and leave. That provides a convenient way to broadcast to a group of socket instances.
*
* @example
* io.on("connection", (socket) => {
* console.log(`socket ${socket.id} connected`);
*
* // send an event to the client
* socket.emit("foo", "bar");
*
* socket.on("foobar", () => {
* // an event was received from the client
* });
*
* // join the room named "room1"
* socket.join("room1");
*
* // broadcast to everyone in the room named "room1"
* io.to("room1").emit("hello");
*
* // upon disconnection
* socket.on("disconnect", (reason) => {
* console.log(`socket ${socket.id} disconnected due to ${reason}`);
* });
* });
*/
export class Socket<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap,
SocketData = any
> extends StrictEventEmitter<
ListenEvents,
EmitEvents,
SocketReservedEventsMap
> {
/**
* An unique identifier for the session.
*/
public readonly id: SocketId;
/**
* The handshake details.
*/
public readonly handshake: Handshake;
/**
* Additional information that can be attached to the Socket instance and which will be used in the fetchSockets method
* Additional information that can be attached to the Socket instance and which will be used in the
* {@link Server.fetchSockets()} method.
*/
public data: any = {};
public data: Partial<SocketData> = {};
/**
* Whether the socket is currently connected or not.
*
* @example
* io.use((socket, next) => {
* console.log(socket.connected); // false
* next();
* });
*
* io.on("connection", (socket) => {
* console.log(socket.connected); // true
* });
*/
public connected: boolean = false;
public connected: boolean;
public disconnected: boolean;
private readonly server: Server<ListenEvents, EmitEvents>;
private readonly server: Server<
ListenEvents,
EmitEvents,
ServerSideEvents,
SocketData
>;
private readonly adapter: Adapter;
private acks: Map<number, () => void> = new Map();
private fns: Array<
(event: Array<any>, next: (err?: Error) => void) => void
> = [];
private fns: Array<(event: Event, next: (err?: Error) => void) => void> = [];
private flags: BroadcastFlags = {};
private _anyListeners?: Array<(...args: any[]) => void>;
private _anyOutgoingListeners?: Array<(...args: any[]) => void>;
/**
* Interface to a `Client` for a given `Namespace`.
@@ -144,8 +219,8 @@ export class Socket<
* @package
*/
constructor(
readonly nsp: Namespace<ListenEvents, EmitEvents>,
readonly client: Client<ListenEvents, EmitEvents>,
readonly nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents>,
readonly client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
auth: object
) {
super();
@@ -157,8 +232,6 @@ export class Socket<
} else {
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);
}
@@ -177,7 +250,8 @@ export class Socket<
secure: !!this.request.connection.encrypted,
issued: +new Date(),
url: this.request.url!,
query: url.parse(this.request.url!, true).query,
// @ts-ignore
query: this.request._query,
auth,
};
}
@@ -185,15 +259,27 @@ export class Socket<
/**
* Emits to this client.
*
* @example
* io.on("connection", (socket) => {
* socket.emit("hello", "world");
*
* // all serializable datastructures are supported (no need to call JSON.stringify)
* socket.emit("hello", 1, "2", { 3: ["4"], 5: Buffer.from([6]) });
*
* // with an acknowledgement from the client
* socket.emit("hello", "world", (val) => {
* // ...
* });
* });
*
* @return Always returns `true`.
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`);
throw new Error(`"${String(ev)}" is a reserved event name`);
}
const data: any[] = [ev, ...args];
const packet: any = {
@@ -203,57 +289,124 @@ export class Socket<
// access last argument to see if it's an ACK callback
if (typeof data[data.length - 1] === "function") {
debug("emitting packet with ack id %d", this.nsp._ids);
this.acks.set(this.nsp._ids, data.pop());
packet.id = this.nsp._ids++;
const id = this.nsp._ids++;
debug("emitting packet with ack id %d", id);
this.registerAckCallback(id, data.pop());
packet.id = id;
}
const flags = Object.assign({}, this.flags);
this.flags = {};
this.notifyOutgoingListeners(packet);
this.packet(packet, flags);
return true;
}
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
* @private
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.newBroadcastOperator().to(room);
private registerAckCallback(id: number, ack: (...args: any[]) => void): void {
const timeout = this.flags.timeout;
if (timeout === undefined) {
this.acks.set(id, ack);
return;
}
const timer = setTimeout(() => {
debug("event with ack id %d has timed out after %d ms", id, timeout);
this.acks.delete(id);
ack.call(this, new Error("operation has timed out"));
}, timeout);
this.acks.set(id, (...args) => {
clearTimeout(timer);
ack.apply(this, [null, ...args]);
});
}
/**
* Targets a room when broadcasting.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients in the “room-101” room, except this socket
* socket.to("room-101").emit("foo", "bar");
*
* // the code above is equivalent to:
* io.to("room-101").except(socket.id).emit("foo", "bar");
*
* // with an array of rooms (a client will be notified at most once)
* socket.to(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.to("room-101").to("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public to(room: Room | Room[]) {
return this.newBroadcastOperator().to(room);
}
/**
* Targets a room when broadcasting. Similar to `to()`, but might feel clearer in some cases:
*
* @example
* io.on("connection", (socket) => {
* // disconnect all clients in the "room-101" room, except this socket
* socket.in("room-101").disconnectSockets();
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public in(room: Room | Room[]) {
return this.newBroadcastOperator().in(room);
}
/**
* Excludes a room when broadcasting.
*
* @param room
* @return self
* @public
* @example
* io.on("connection", (socket) => {
* // the "foo" event will be broadcast to all connected clients, except the ones that are in the "room-101" room
* // and this socket
* socket.except("room-101").emit("foo", "bar");
*
* // with an array of rooms
* socket.except(["room-101", "room-102"]).emit("foo", "bar");
*
* // with multiple chained calls
* socket.except("room-101").except("room-102").emit("foo", "bar");
* });
*
* @param room - a room, or an array of rooms
* @return a new {@link BroadcastOperator} instance for chaining
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
public except(room: Room | Room[]) {
return this.newBroadcastOperator().except(room);
}
/**
* Sends a `message` event.
*
* This method mimics the WebSocket.send() method.
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send
*
* @example
* io.on("connection", (socket) => {
* socket.send("hello");
*
* // this is equivalent to
* socket.emit("message", "hello");
* });
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -261,10 +414,9 @@ export class Socket<
}
/**
* Sends a `message` event.
* Sends a `message` event. Alias of {@link send}.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args);
@@ -290,9 +442,17 @@ export class Socket<
/**
* Joins a room.
*
* @example
* io.on("connection", (socket) => {
* // join a single room
* socket.join("room1");
*
* // join multiple rooms
* socket.join(["room1", "room2"]);
* });
*
* @param {String|Array} rooms - room or array of rooms
* @return a Promise or nothing, depending on the adapter
* @public
*/
public join(rooms: Room | Array<Room>): Promise<void> | void {
debug("join room %s", rooms);
@@ -306,9 +466,17 @@ export class Socket<
/**
* Leaves a room.
*
* @example
* io.on("connection", (socket) => {
* // leave a single room
* socket.leave("room1");
*
* // leave multiple rooms
* socket.leave("room1").leave("room2");
* });
*
* @param {String} room
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
debug("leave room %s", room);
@@ -335,6 +503,7 @@ export class Socket<
*/
_onconnect(): void {
debug("socket connected - writing packet");
this.connected = true;
this.join(this.id);
if (this.conn.protocol === 3) {
this.packet({ type: PacketType.CONNECT });
@@ -371,9 +540,6 @@ export class Socket<
case PacketType.DISCONNECT:
this.ondisconnect();
break;
case PacketType.CONNECT_ERROR:
this._onerror(new Error(packet.data));
}
}
@@ -474,19 +640,28 @@ export class Socket<
*
* @private
*/
_onclose(reason: string): this | undefined {
_onclose(reason: DisconnectReason): this | undefined {
if (!this.connected) return this;
debug("closing socket - reason %s", reason);
this.emitReserved("disconnecting", reason);
this.leaveAll();
this._cleanup();
this.nsp._remove(this);
this.client._remove(this);
this.connected = false;
this.disconnected = true;
this.emitReserved("disconnect", reason);
return;
}
/**
* Makes the socket leave all the rooms it was part of and prevents it from joining any other room
*
* @private
*/
_cleanup() {
this.leaveAll();
this.join = noop;
}
/**
* Produces an `error` packet.
*
@@ -501,10 +676,17 @@ export class Socket<
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
* @example
* io.on("connection", (socket) => {
* // disconnect this socket (the connection might be kept alive for other namespaces)
* socket.disconnect();
*
* @public
* // disconnect this socket and close the underlying connection
* socket.disconnect(true);
* })
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return self
*/
public disconnect(close = false): this {
if (!this.connected) return this;
@@ -520,9 +702,13 @@ export class Socket<
/**
* Sets the compress flag.
*
* @example
* io.on("connection", (socket) => {
* socket.compress(false).emit("hello");
* });
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Socket} self
* @public
*/
public compress(compress: boolean): this {
this.flags.compress = compress;
@@ -534,8 +720,12 @@ export class Socket<
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @example
* io.on("connection", (socket) => {
* socket.volatile.emit("hello"); // the client may or may not receive it
* });
*
* @return {Socket} self
* @public
*/
public get volatile(): this {
this.flags.volatile = true;
@@ -546,30 +736,60 @@ export class Socket<
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender.
*
* @return {Socket} self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients, except this socket
* socket.broadcast.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get broadcast(): BroadcastOperator<EmitEvents> {
public get broadcast() {
return this.newBroadcastOperator();
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
* @example
* io.on("connection", (socket) => {
* // the “foo” event will be broadcast to all connected clients on this node, except this socket
* socket.local.emit("foo", "bar");
* });
*
* @return a new {@link BroadcastOperator} instance for chaining
*/
public get local(): BroadcastOperator<EmitEvents> {
public get local() {
return this.newBroadcastOperator().local;
}
/**
* Sets a modifier for a subsequent event emission that the callback will be called with an error when the
* given number of milliseconds have elapsed without an acknowledgement from the client:
*
* @example
* io.on("connection", (socket) => {
* socket.timeout(5000).emit("my-event", (err) => {
* if (err) {
* // the client did not acknowledge the event in the given delay
* }
* });
* });
*
* @returns self
*/
public timeout(timeout: number): this {
this.flags.timeout = timeout;
return this;
}
/**
* Dispatch incoming event to socket listeners.
*
* @param {Array} event - event that will get emitted
* @private
*/
private dispatch(event: [eventName: string, ...args: any[]]): void {
private dispatch(event: Event): void {
debug("dispatching an event %j", event);
this.run(event, (err) => {
process.nextTick(() => {
@@ -588,13 +808,27 @@ export class Socket<
/**
* Sets up socket middleware.
*
* @example
* io.on("connection", (socket) => {
* socket.use(([event, ...args], next) => {
* if (isUnauthorized(event)) {
* return next(new Error("unauthorized event"));
* }
* // do not forget to call next
* next();
* });
*
* socket.on("error", (err) => {
* if (err && err.message === "unauthorized event") {
* socket.disconnect();
* }
* });
* });
*
* @param {Function} fn - middleware function (event, next)
* @return {Socket} self
* @public
*/
public use(
fn: (event: Array<any>, next: (err?: Error) => void) => void
): this {
public use(fn: (event: Event, next: (err?: Error) => void) => void): this {
this.fns.push(fn);
return this;
}
@@ -606,10 +840,7 @@ export class Socket<
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(
event: [eventName: string, ...args: any[]],
fn: (err: Error | null) => void
): void {
private run(event: Event, fn: (err: Error | null) => void): void {
const fns = this.fns.slice(0);
if (!fns.length) return fn(null);
@@ -629,10 +860,15 @@ export class Socket<
run(0);
}
/**
* Whether the socket is currently disconnected
*/
public get disconnected() {
return !this.connected;
}
/**
* A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/
public get request(): IncomingMessage {
return this.client.request;
@@ -641,25 +877,47 @@ export class Socket<
/**
* A reference to the underlying Client transport connection (Engine.IO Socket object).
*
* @public
* @example
* io.on("connection", (socket) => {
* console.log(socket.conn.transport.name); // prints "polling" or "websocket"
*
* socket.conn.once("upgrade", () => {
* console.log(socket.conn.transport.name); // prints "websocket"
* });
* });
*/
public get conn() {
return this.client.conn;
}
/**
* @public
* Returns the rooms the socket is currently in.
*
* @example
* io.on("connection", (socket) => {
* console.log(socket.rooms); // Set { <socket.id> }
*
* socket.join("room1");
*
* console.log(socket.rooms); // Set { <socket.id>, "room1" }
* });
*/
public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set();
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback.
*
* @example
* io.on("connection", (socket) => {
* socket.onAny((event, ...args) => {
* console.log(`got event ${event}`);
* });
* });
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
@@ -668,11 +926,10 @@ export class Socket<
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
* Adds a listener that will be fired when any event is received. The event name is passed as the first argument to
* the callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): this {
this._anyListeners = this._anyListeners || [];
@@ -681,10 +938,24 @@ export class Socket<
}
/**
* Removes the listener that will be fired when any event is emitted.
* Removes the listener that will be fired when any event is received.
*
* @example
* io.on("connection", (socket) => {
* const catchAllListener = (event, ...args) => {
* console.log(`got event ${event}`);
* }
*
* socket.onAny(catchAllListener);
*
* // remove a specific listener
* socket.offAny(catchAllListener);
*
* // or remove all listeners
* socket.offAny();
* });
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): this {
if (!this._anyListeners) {
@@ -707,17 +978,117 @@ export class Socket<
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || [];
}
private newBroadcastOperator(): BroadcastOperator<EmitEvents> {
/**
* Adds a listener that will be fired when any event is sent. The event name is passed as the first argument to
* the callback.
*
* Note: acknowledgements sent to the client are not included.
*
* @example
* io.on("connection", (socket) => {
* socket.onAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
* });
*
* @param listener
*/
public onAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
this._anyOutgoingListeners.push(listener);
return this;
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @example
* io.on("connection", (socket) => {
* socket.prependAnyOutgoing((event, ...args) => {
* console.log(`sent event ${event}`);
* });
* });
*
* @param listener
*/
public prependAnyOutgoing(listener: (...args: any[]) => void): this {
this._anyOutgoingListeners = this._anyOutgoingListeners || [];
this._anyOutgoingListeners.unshift(listener);
return this;
}
/**
* Removes the listener that will be fired when any event is sent.
*
* @example
* io.on("connection", (socket) => {
* const catchAllListener = (event, ...args) => {
* console.log(`sent event ${event}`);
* }
*
* socket.onAnyOutgoing(catchAllListener);
*
* // remove a specific listener
* socket.offAnyOutgoing(catchAllListener);
*
* // or remove all listeners
* socket.offAnyOutgoing();
* });
*
* @param listener - the catch-all listener
*/
public offAnyOutgoing(listener?: (...args: any[]) => void): this {
if (!this._anyOutgoingListeners) {
return this;
}
if (listener) {
const listeners = this._anyOutgoingListeners;
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1);
return this;
}
}
} else {
this._anyOutgoingListeners = [];
}
return this;
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*/
public listenersAnyOutgoing() {
return this._anyOutgoingListeners || [];
}
/**
* Notify the listeners for each packet sent (emit or broadcast)
*
* @param packet
*
* @private
*/
private notifyOutgoingListeners(packet: Packet) {
if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
const listeners = this._anyOutgoingListeners.slice();
for (const listener of listeners) {
listener.apply(this, packet.data);
}
}
}
private newBroadcastOperator() {
const flags = Object.assign({}, this.flags);
this.flags = {};
return new BroadcastOperator(
return new BroadcastOperator<EmitEvents, SocketData>(
this.adapter,
new Set<Room>(),
new Set<Room>([this.id]),

View File

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

164
lib/uws.ts Normal file
View File

@@ -0,0 +1,164 @@
import { Adapter, Room } from "socket.io-adapter";
import type { WebSocket } from "uWebSockets.js";
import type { Socket } from "./socket.js";
import { createReadStream, statSync } from "fs";
import debugModule from "debug";
const debug = debugModule("socket.io:adapter-uws");
const SEPARATOR = "\x1f"; // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const { addAll, del, broadcast } = Adapter.prototype;
export function patchAdapter(app /* : TemplatedApp */) {
Adapter.prototype.addAll = function (id, rooms) {
const isNew = !this.sids.has(id);
addAll.call(this, id, rooms);
const socket: Socket = this.nsp.sockets.get(id);
if (!socket) {
return;
}
if (socket.conn.transport.name === "websocket") {
subscribe(this.nsp.name, socket, isNew, rooms);
return;
}
if (isNew) {
socket.conn.on("upgrade", () => {
const rooms = this.sids.get(id);
if (rooms) {
subscribe(this.nsp.name, socket, isNew, rooms);
}
});
}
};
Adapter.prototype.del = function (id, room) {
del.call(this, id, room);
const socket: Socket = this.nsp.sockets.get(id);
if (socket && socket.conn.transport.name === "websocket") {
// @ts-ignore
const sessionId = socket.conn.id;
// @ts-ignore
const websocket: WebSocket = socket.conn.transport.socket;
const topic = `${this.nsp.name}${SEPARATOR}${room}`;
debug("unsubscribe connection %s from topic %s", sessionId, topic);
websocket.unsubscribe(topic);
}
};
Adapter.prototype.broadcast = function (packet, opts) {
const useFastPublish = opts.rooms.size <= 1 && opts.except!.size === 0;
if (!useFastPublish) {
broadcast.call(this, packet, opts);
return;
}
const flags = opts.flags || {};
const basePacketOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress,
};
packet.nsp = this.nsp.name;
const encodedPackets = this.encoder.encode(packet);
const topic =
opts.rooms.size === 0
? this.nsp.name
: `${this.nsp.name}${SEPARATOR}${opts.rooms.keys().next().value}`;
debug("fast publish to %s", topic);
// fast publish for clients connected with WebSocket
encodedPackets.forEach((encodedPacket) => {
const isBinary = typeof encodedPacket !== "string";
// "4" being the message type in the Engine.IO protocol, see https://github.com/socketio/engine.io-protocol
app.publish(
topic,
isBinary ? encodedPacket : "4" + encodedPacket,
isBinary
);
});
this.apply(opts, (socket) => {
if (socket.conn.transport.name !== "websocket") {
// classic publish for clients connected with HTTP long-polling
socket.client.writeToEngine(encodedPackets, basePacketOpts);
}
});
};
}
function subscribe(
namespaceName: string,
socket: Socket,
isNew: boolean,
rooms: Set<Room>
) {
// @ts-ignore
const sessionId = socket.conn.id;
// @ts-ignore
const websocket: WebSocket = socket.conn.transport.socket;
if (isNew) {
debug("subscribe connection %s to topic %s", sessionId, namespaceName);
websocket.subscribe(namespaceName);
}
rooms.forEach((room) => {
const topic = `${namespaceName}${SEPARATOR}${room}`; // '#' can be used as wildcard
debug("subscribe connection %s to topic %s", sessionId, topic);
websocket.subscribe(topic);
});
}
export function restoreAdapter() {
Adapter.prototype.addAll = addAll;
Adapter.prototype.del = del;
Adapter.prototype.broadcast = broadcast;
}
const toArrayBuffer = (buffer: Buffer) => {
const { buffer: arrayBuffer, byteOffset, byteLength } = buffer;
return arrayBuffer.slice(byteOffset, byteOffset + byteLength);
};
// imported from https://github.com/kolodziejczak-sz/uwebsocket-serve
export function serveFile(res /* : HttpResponse */, filepath: string) {
const { size } = statSync(filepath);
const readStream = createReadStream(filepath);
const destroyReadStream = () => !readStream.destroyed && readStream.destroy();
const onError = (error: Error) => {
destroyReadStream();
throw error;
};
const onDataChunk = (chunk: Buffer) => {
const arrayBufferChunk = toArrayBuffer(chunk);
const lastOffset = res.getWriteOffset();
const [ok, done] = res.tryEnd(arrayBufferChunk, size);
if (!done && !ok) {
readStream.pause();
res.onWritable((offset) => {
const [ok, done] = res.tryEnd(
arrayBufferChunk.slice(offset - lastOffset),
size
);
if (!done && ok) {
readStream.resume();
}
return ok;
});
}
};
res.onAborted(destroyReadStream);
readStream
.on("data", onDataChunk)
.on("error", onError)
.on("end", destroyReadStream);
}

8227
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "socket.io",
"version": "4.0.2",
"version": "4.5.4",
"description": "node.js realtime framework server",
"keywords": [
"realtime",
@@ -27,7 +27,8 @@
"main": "./dist/index.js",
"exports": {
"import": "./wrapper.mjs",
"require": "./dist/index.js"
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"types": "./dist/index.d.ts",
"license": "MIT",
@@ -39,39 +40,34 @@
"compile": "rimraf ./dist && tsc",
"test": "npm run format:check && npm run compile && npm run test:types && npm run test:unit",
"test:types": "tsd",
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/socket.io.ts",
"test:unit": "nyc mocha --require ts-node/register --reporter spec --slow 200 --bail --timeout 10000 test/index.ts",
"format:check": "prettier --check \"lib/**/*.ts\" \"test/**/*.ts\"",
"format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\"",
"prepack": "npm run compile"
},
"dependencies": {
"@types/cookie": "^0.4.0",
"@types/cors": "^2.8.8",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"debug": "~4.3.1",
"engine.io": "~5.0.0",
"socket.io-adapter": "~2.2.0",
"socket.io-parser": "~4.0.3"
"debug": "~4.3.2",
"engine.io": "~6.2.1",
"socket.io-adapter": "~2.4.0",
"socket.io-parser": "~4.2.1"
},
"devDependencies": {
"@types/mocha": "^8.0.4",
"babel-eslint": "^10.1.0",
"eslint": "^7.14.0",
"eslint-config-prettier": "^6.11.0",
"@types/mocha": "^9.0.0",
"expect.js": "0.3.1",
"mocha": "^3.5.3",
"mocha": "^10.0.0",
"nyc": "^15.1.0",
"prettier": "^2.2.0",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
"socket.io-client": "4.0.2",
"socket.io-client": "4.5.4",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"superagent": "^6.1.0",
"supertest": "^6.0.1",
"ts-node": "^9.0.0",
"tsd": "^0.14.0",
"typescript": "^4.1.2"
"superagent": "^8.0.0",
"supertest": "^6.1.6",
"ts-node": "^10.2.1",
"tsd": "^0.21.0",
"typescript": "^4.4.2",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.0.0"
},
"contributors": [
{

181
test/close.ts Normal file
View File

@@ -0,0 +1,181 @@
import { createServer } from "http";
import { io as ioc } from "socket.io-client";
import { join } from "path";
import { exec } from "child_process";
import { Server } from "..";
import expect from "expect.js";
import { createClient, getPort } from "./support/util";
import request from "supertest";
// TODO: update superagent as latest release now supports promises
const eioHandshake = (httpServer): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
const sid = JSON.parse(res.text.substring(1)).sid;
resolve(sid);
});
});
};
const eioPush = (httpServer, sid: string, body: string): Promise<void> => {
return new Promise((resolve) => {
request(httpServer)
.post("/socket.io/")
.send(body)
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end(() => {
resolve();
});
});
};
const eioPoll = (httpServer, sid): Promise<string> => {
return new Promise((resolve) => {
request(httpServer)
.get("/socket.io/")
.query({ transport: "polling", EIO: 4, sid })
.expect(200)
.end((err, res) => {
resolve(res.text);
});
});
};
describe("close", () => {
it("should be able to close sio sending a srv", (done) => {
const httpServer = createServer().listen(0);
const io = new Server(httpServer);
const port = getPort(io);
const net = require("net");
const server = net.createServer();
const clientSocket = createClient(io, "/", { reconnection: false });
clientSocket.on("disconnect", () => {
expect(io.sockets.sockets.size).to.equal(0);
server.listen(port);
});
clientSocket.on("connect", () => {
expect(io.sockets.sockets.size).to.equal(1);
io.close();
});
server.once("listening", () => {
// PORT should be free
server.close((error) => {
expect(error).to.be(undefined);
done();
});
});
});
it("should be able to close sio sending a srv", (done) => {
const io = new Server(0);
const port = getPort(io);
const net = require("net");
const server = net.createServer();
const clientSocket = ioc("ws://0.0.0.0:" + port, {
reconnection: false,
});
clientSocket.on("disconnect", () => {
expect(io.sockets.sockets.size).to.equal(0);
server.listen(port);
});
clientSocket.on("connect", () => {
expect(io.sockets.sockets.size).to.equal(1);
io.close();
});
server.once("listening", () => {
// PORT should be free
server.close((error) => {
expect(error).to.be(undefined);
done();
});
});
});
describe("graceful close", () => {
function fixture(filename) {
return (
'"' +
process.execPath +
'" "' +
join(__dirname, "fixtures", filename) +
'"'
);
}
it("should stop socket and timers", (done) => {
exec(fixture("server-close.ts"), done);
});
});
describe("protocol violations", () => {
it("should close the connection when receiving several CONNECT packets", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send a first CONNECT packet
await eioPush(httpServer, sid, "40");
// send another CONNECT packet
await eioPush(httpServer, sid, "40");
// session is cleanly closed (not discarded, see 'client.close()')
// first, we receive the Socket.IO handshake response
await eioPoll(httpServer, sid);
// then a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
it("should close the connection when receiving an EVENT packet while not connected", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send an EVENT packet
await eioPush(httpServer, sid, '42["some event"]');
// session is cleanly closed, we receive a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
it("should close the connection when receiving an invalid packet", async () => {
const httpServer = createServer();
const io = new Server(httpServer);
httpServer.listen(0);
const sid = await eioHandshake(httpServer);
// send a CONNECT packet
await eioPush(httpServer, sid, "40");
// send an invalid packet
await eioPush(httpServer, sid, "4abc");
// session is cleanly closed (not discarded, see 'client.close()')
// first, we receive the Socket.IO handshake response
await eioPoll(httpServer, sid);
// then a close packet
const body = await eioPoll(httpServer, sid);
expect(body).to.be("6\u001e1");
io.close();
});
});
});

View File

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

87
test/handshake.ts Normal file
View File

@@ -0,0 +1,87 @@
import { Server } from "..";
import expect from "expect.js";
import { getPort, success } from "./support/util";
describe("handshake", () => {
const request = require("superagent");
it("should send the Access-Control-Allow-xxx headers on OPTIONS request", (done) => {
const io = new Server(0, {
cors: {
origin: "http://localhost:54023",
methods: ["GET", "POST"],
allowedHeaders: ["content-type"],
credentials: true,
},
});
request
.options(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54023")
.end((err, res) => {
expect(res.status).to.be(204);
expect(res.headers["access-control-allow-origin"]).to.be(
"http://localhost:54023"
);
expect(res.headers["access-control-allow-methods"]).to.be("GET,POST");
expect(res.headers["access-control-allow-headers"]).to.be(
"content-type"
);
expect(res.headers["access-control-allow-credentials"]).to.be("true");
success(done, io);
});
});
it("should send the Access-Control-Allow-xxx headers on GET request", (done) => {
const io = new Server(0, {
cors: {
origin: "http://localhost:54024",
methods: ["GET", "POST"],
allowedHeaders: ["content-type"],
credentials: true,
},
});
request
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 })
.set("Origin", "http://localhost:54024")
.end((err, res) => {
expect(res.status).to.be(200);
expect(res.headers["access-control-allow-origin"]).to.be(
"http://localhost:54024"
);
expect(res.headers["access-control-allow-credentials"]).to.be("true");
success(done, io);
});
});
it("should allow request if custom function in opts.allowRequest returns true", (done) => {
const io = new Server(0, {
allowRequest: (req, callback) => callback(null, true),
});
request
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
expect(res.status).to.be(200);
success(done, io);
});
});
it("should disallow request if custom function in opts.allowRequest returns false", (done) => {
const io = new Server(0, {
allowRequest: (req, callback) => callback(null, false),
});
request
.get(`http://localhost:${getPort(io)}/socket.io/default/`)
.set("origin", "http://foo.example")
.query({ transport: "polling", EIO: 4 })
.end((err, res) => {
expect(res.status).to.be(403);
success(done, io);
});
});
});

23
test/index.ts Normal file
View File

@@ -0,0 +1,23 @@
"use strict";
import expect from "expect.js";
describe("socket.io", () => {
it("should be the same version as client", () => {
const version = require("../package").version;
expect(version).to.be(require("socket.io-client/package.json").version);
});
require("./server-attachment");
require("./handshake");
require("./close");
require("./namespaces");
require("./socket");
require("./messaging-many");
require("./middleware");
require("./socket-middleware");
require("./v2-compatibility");
require("./socket-timeout");
require("./uws");
require("./utility-methods");
});

501
test/messaging-many.ts Normal file
View File

@@ -0,0 +1,501 @@
import { Server } from "..";
import expect from "expect.js";
import {
createClient,
createPartialDone,
success,
successFn,
waitFor,
} from "./support/util";
describe("messaging many", () => {
it("emits to a namespace", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/test");
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2, socket3)
);
socket1.on("a", (a) => {
expect(a).to.be("b");
partialDone();
});
socket2.on("a", (a) => {
expect(a).to.be("b");
partialDone();
});
socket3.on("a", () => {
done(new Error("not"));
});
let sockets = 3;
io.on("connection", () => {
--sockets || emit();
});
io.of("/test", () => {
--sockets || emit();
});
function emit() {
io.emit("a", "b");
}
});
it("emits binary data to a namespace", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/test");
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2, socket3)
);
socket1.on("bin", (a) => {
expect(Buffer.isBuffer(a)).to.be(true);
partialDone();
});
socket2.on("bin", (a) => {
expect(Buffer.isBuffer(a)).to.be(true);
partialDone();
});
socket3.on("bin", () => {
done(new Error("not"));
});
let sockets = 3;
io.on("connection", () => {
--sockets || emit();
});
io.of("/test", () => {
--sockets || emit();
});
function emit() {
io.emit("bin", Buffer.alloc(10));
}
});
it("emits to the rest", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/test");
socket1.on("a", (a) => {
expect(a).to.be("b");
socket1.emit("finish");
});
socket2.emit("broadcast");
socket2.on("a", () => {
done(new Error("done"));
});
socket3.on("a", () => {
done(new Error("not"));
});
io.on("connection", (socket) => {
socket.on("broadcast", () => {
socket.broadcast.emit("a", "b");
});
socket.on("finish", () => {
success(done, io, socket1, socket2, socket3);
});
});
});
it("emits to rooms", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
socket2.on("a", () => {
done(new Error("not"));
});
socket1.on("a", () => {
success(done, io, socket1, socket2);
});
socket1.emit("join", "woot");
socket1.emit("emit", "woot");
io.on("connection", (socket) => {
socket.on("join", (room, fn) => {
socket.join(room);
fn && fn();
});
socket.on("emit", (room) => {
io.in(room).emit("a");
});
});
});
it("emits to rooms avoiding dupes", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2)
);
socket2.on("a", () => {
done(new Error("not"));
});
socket1.on("a", partialDone);
socket2.on("b", partialDone);
socket1.emit("join", "woot");
socket1.emit("join", "test");
socket2.emit("join", "third", () => {
socket2.emit("emit");
});
io.on("connection", (socket) => {
socket.on("join", (room, fn) => {
socket.join(room);
fn && fn();
});
socket.on("emit", () => {
io.in("woot").in("test").emit("a");
io.in("third").emit("b");
});
});
});
it("broadcasts to rooms", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2, socket3)
);
socket1.emit("join", "woot");
socket2.emit("join", "test");
socket3.emit("join", "test", () => {
socket3.emit("broadcast");
});
socket1.on("a", () => {
done(new Error("not"));
});
socket2.on("a", () => {
partialDone();
});
socket3.on("a", () => {
done(new Error("not"));
});
socket3.on("b", () => {
partialDone();
});
io.on("connection", (socket) => {
socket.on("join", (room, fn) => {
socket.join(room);
fn && fn();
});
socket.on("broadcast", () => {
socket.broadcast.to("test").emit("a");
socket.emit("b");
});
});
});
it("broadcasts binary data to rooms", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2, socket3)
);
socket1.emit("join", "woot");
socket2.emit("join", "test");
socket3.emit("join", "test", () => {
socket3.emit("broadcast");
});
socket1.on("bin", (data) => {
throw new Error("got bin in socket1");
});
socket2.on("bin", (data) => {
expect(Buffer.isBuffer(data)).to.be(true);
partialDone();
});
socket2.on("bin2", (data) => {
throw new Error("socket2 got bin2");
});
socket3.on("bin", (data) => {
throw new Error("socket3 got bin");
});
socket3.on("bin2", (data) => {
expect(Buffer.isBuffer(data)).to.be(true);
partialDone();
});
io.on("connection", (socket) => {
socket.on("join", (room, fn) => {
socket.join(room);
fn && fn();
});
socket.on("broadcast", () => {
socket.broadcast.to("test").emit("bin", Buffer.alloc(5));
socket.emit("bin2", Buffer.alloc(5));
});
});
});
it("keeps track of rooms", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", (s) => {
s.join("a");
expect(s.rooms).to.contain(s.id, "a");
s.join("b");
expect(s.rooms).to.contain(s.id, "a", "b");
s.join("c");
expect(s.rooms).to.contain(s.id, "a", "b", "c");
s.leave("b");
expect(s.rooms).to.contain(s.id, "a", "c");
(s as any).leaveAll();
expect(s.rooms.size).to.eql(0);
success(done, io, socket);
});
});
it("deletes empty rooms", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", (s) => {
s.join("a");
expect(s.nsp.adapter.rooms).to.contain("a");
s.leave("a");
expect(s.nsp.adapter.rooms).to.not.contain("a");
success(done, io, socket);
});
});
it("should properly cleanup left rooms", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", (s) => {
s.join("a");
expect(s.rooms).to.contain(s.id, "a");
s.join("b");
expect(s.rooms).to.contain(s.id, "a", "b");
s.leave("unknown");
expect(s.rooms).to.contain(s.id, "a", "b");
(s as any).leaveAll();
expect(s.rooms.size).to.eql(0);
success(done, io, socket);
});
});
it("allows to join several rooms at once", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", (s) => {
s.join(["a", "b", "c"]);
expect(s.rooms).to.contain(s.id, "a", "b", "c");
success(done, io, socket);
});
});
it("should exclude specific sockets when broadcasting", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket2.on("a", () => {
done(new Error("not"));
});
socket3.on("a", () => {
done(new Error("not"));
});
socket1.on("a", successFn(done, io, socket1, socket2, socket3));
io.on("connection", (socket) => {
socket.on("exclude", (id) => {
socket.broadcast.except(id).emit("a");
});
});
socket2.on("connect", () => {
socket3.emit("exclude", socket2.id);
});
});
it("should exclude a specific room when broadcasting", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket2.on("a", () => {
done(new Error("not"));
});
socket3.on("a", () => {
done(new Error("not"));
});
socket1.on("a", successFn(done, io, socket1, socket2, socket3));
io.on("connection", (socket) => {
socket.on("join", (room, cb) => {
socket.join(room);
cb();
});
socket.on("broadcast", () => {
socket.broadcast.except("room1").emit("a");
});
});
socket2.emit("join", "room1", () => {
socket3.emit("broadcast");
});
});
it("should return an immutable broadcast operator", (done) => {
const io = new Server(0);
const clientSocket = createClient(io);
io.on("connection", (socket) => {
const operator = socket.local
.compress(false)
.to(["room1", "room2"])
.except("room3");
operator.compress(true).emit("hello");
operator.volatile.emit("hello");
operator.to("room4").emit("hello");
operator.except("room5").emit("hello");
socket.emit("hello");
socket.to("room6").emit("hello");
// @ts-ignore
expect(operator.rooms).to.contain("room1", "room2");
// @ts-ignore
expect(operator.rooms).to.not.contain("room4", "room5", "room6");
// @ts-ignore
expect(operator.exceptRooms).to.contain("room3");
// @ts-ignore
expect(operator.flags).to.eql({ local: true, compress: false });
success(done, io, clientSocket);
});
});
it("should broadcast and expect multiple acknowledgements", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", (cb) => {
cb(3);
});
Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]).then(() => {
io.timeout(2000).emit("some event", (err, responses) => {
expect(err).to.be(null);
expect(responses).to.have.length(3);
expect(responses).to.contain(1, 2, 3);
success(done, io, socket1, socket2, socket3);
});
});
});
it("should fail when a client does not acknowledge the event in the given delay", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket1.on("some event", (cb) => {
cb(1);
});
socket2.on("some event", (cb) => {
cb(2);
});
socket3.on("some event", () => {
// timeout
});
Promise.all([
waitFor(socket1, "connect"),
waitFor(socket2, "connect"),
waitFor(socket3, "connect"),
]).then(() => {
io.timeout(200).emit("some event", (err, responses) => {
expect(err).to.be.an(Error);
expect(responses).to.have.length(2);
expect(responses).to.contain(1, 2);
success(done, io, socket1, socket2, socket3);
});
});
});
it("should broadcast and return if the packet is sent to 0 client", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/", { multiplex: false });
const socket2 = createClient(io, "/", { multiplex: false });
const socket3 = createClient(io, "/", { multiplex: false });
socket1.on("some event", () => {
done(new Error("should not happen"));
});
socket2.on("some event", () => {
done(new Error("should not happen"));
});
socket3.on("some event", () => {
done(new Error("should not happen"));
});
io.to("room123")
.timeout(200)
.emit("some event", (err, responses) => {
expect(err).to.be(null);
expect(responses).to.have.length(0);
success(done, io, socket1, socket2, socket3);
});
});
});

210
test/middleware.ts Normal file
View File

@@ -0,0 +1,210 @@
import { Server, Socket } from "..";
import expect from "expect.js";
import {
success,
createClient,
successFn,
createPartialDone,
} from "./support/util";
describe("middleware", () => {
it("should call functions", (done) => {
const io = new Server(0);
let run = 0;
io.use((socket, next) => {
expect(socket).to.be.a(Socket);
run++;
next();
});
io.use((socket, next) => {
expect(socket).to.be.a(Socket);
run++;
next();
});
const socket = createClient(io);
socket.on("connect", () => {
expect(run).to.be(2);
success(done, io, socket);
});
});
it("should pass errors", (done) => {
const io = new Server(0);
io.use((socket, next) => {
next(new Error("Authentication error"));
});
io.use((socket, next) => {
done(new Error("nope"));
});
const socket = createClient(io);
socket.on("connect", () => {
done(new Error("nope"));
});
socket.on("connect_error", (err) => {
expect(err.message).to.be("Authentication error");
success(done, io, socket);
});
});
it("should pass an object", (done) => {
const io = new Server(0);
io.use((socket, next) => {
const err = new Error("Authentication error");
// @ts-ignore
err.data = { a: "b", c: 3 };
next(err);
});
const socket = createClient(io);
socket.on("connect", () => {
done(new Error("nope"));
});
socket.on("connect_error", (err) => {
expect(err).to.be.an(Error);
expect(err.message).to.eql("Authentication error");
// @ts-ignore
expect(err.data).to.eql({ a: "b", c: 3 });
success(done, io, socket);
});
});
it("should only call connection after fns", (done) => {
const io = new Server(0);
io.use((socket: any, next) => {
socket.name = "guillermo";
next();
});
const clientSocket = createClient(io);
io.on("connection", (socket) => {
expect((socket as any).name).to.be("guillermo");
success(done, io, clientSocket);
});
});
it("should only call connection after (lengthy) fns", (done) => {
const io = new Server(0);
let authenticated = false;
io.use((socket, next) => {
setTimeout(() => {
authenticated = true;
next();
}, 300);
});
const socket = createClient(io);
socket.on("connect", () => {
expect(authenticated).to.be(true);
success(done, io, socket);
});
});
it("should be ignored if socket gets closed", (done) => {
const io = new Server(0);
let socket;
io.use((s, next) => {
socket.io.engine.close();
s.client.conn.on("close", () => {
process.nextTick(next);
setTimeout(() => {
success(done, io, socket);
}, 50);
});
});
socket = createClient(io);
io.on("connection", (socket) => {
done(new Error("should not fire"));
});
});
it("should call functions in expected order", (done) => {
const io = new Server(0);
const result: number[] = [];
io.use(() => {
done(new Error("should not fire"));
});
io.of("/chat").use((socket, next) => {
result.push(1);
setTimeout(next, 50);
});
io.of("/chat").use((socket, next) => {
result.push(2);
setTimeout(next, 50);
});
io.of("/chat").use((socket, next) => {
result.push(3);
setTimeout(next, 50);
});
const chat = createClient(io, "/chat");
chat.on("connect", () => {
expect(result).to.eql([1, 2, 3]);
success(done, io, chat);
});
});
it("should disable the merge of handshake packets", (done) => {
const io = new Server(0);
io.use((socket, next) => {
next();
});
const socket = createClient(io);
socket.on("connect", successFn(done, io, socket));
});
it("should work with a custom namespace", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/");
const socket2 = createClient(io, "/chat");
const partialDone = createPartialDone(
2,
successFn(done, io, socket1, socket2)
);
io.of("/chat").use((socket, next) => {
next();
});
socket1.on("connect", partialDone);
socket2.on("connect", partialDone);
});
it("should only set `connected` to true after the middleware execution", (done) => {
const io = new Server(0);
const clientSocket = createClient(io, "/");
io.use((socket, next) => {
expect(socket.connected).to.be(false);
expect(socket.disconnected).to.be(true);
next();
});
io.on("connection", (socket) => {
expect(socket.connected).to.be(true);
expect(socket.disconnected).to.be(false);
success(done, io, clientSocket);
});
});
});

575
test/namespaces.ts Normal file
View File

@@ -0,0 +1,575 @@
import type { SocketId } from "socket.io-adapter";
import { Server, Namespace, Socket } from "..";
import expect from "expect.js";
import {
success,
createClient,
successFn,
createPartialDone,
} from "./support/util";
describe("namespaces", () => {
it("should be accessible through .sockets", () => {
const io = new Server();
expect(io.sockets).to.be.a(Namespace);
});
it("should be aliased", () => {
const io = new Server();
expect(io.use).to.be.a("function");
expect(io.to).to.be.a("function");
expect(io["in"]).to.be.a("function");
expect(io.emit).to.be.a("function");
expect(io.send).to.be.a("function");
expect(io.write).to.be.a("function");
expect(io.allSockets).to.be.a("function");
expect(io.compress).to.be.a("function");
});
it("should return an immutable broadcast operator", () => {
const io = new Server();
const operator = io.local.to(["room1", "room2"]).except("room3");
operator.compress(true).emit("hello");
operator.volatile.emit("hello");
operator.to("room4").emit("hello");
operator.except("room5").emit("hello");
io.to("room6").emit("hello");
// @ts-ignore
expect(operator.rooms).to.contain("room1", "room2");
// @ts-ignore
expect(operator.exceptRooms).to.contain("room3");
// @ts-ignore
expect(operator.flags).to.eql({ local: true });
});
it("should automatically connect", (done) => {
const io = new Server(0);
const socket = createClient(io);
socket.on("connect", successFn(done, io, socket));
});
it("should fire a `connection` event", (done) => {
const io = new Server(0);
const clientSocket = createClient(io);
io.on("connection", (socket) => {
expect(socket).to.be.a(Socket);
success(done, io, clientSocket);
});
});
it("should fire a `connect` event", (done) => {
const io = new Server(0);
const clientSocket = createClient(io);
io.on("connect", (socket) => {
expect(socket).to.be.a(Socket);
success(done, io, clientSocket);
});
});
it("should work with many sockets", (done) => {
const io = new Server(0);
io.of("/chat");
io.of("/news");
const chat = createClient(io, "/chat");
const news = createClient(io, "/news");
let total = 2;
chat.on("connect", () => {
--total || success(done, io, chat, news);
});
news.on("connect", () => {
--total || success(done, io, chat, news);
});
});
it('should be able to equivalently start with "" or "/" on server', (done) => {
const io = new Server(0);
const c1 = createClient(io, "/");
const c2 = createClient(io, "/abc");
let total = 2;
io.of("").on("connection", () => {
--total || success(done, io, c1, c2);
});
io.of("abc").on("connection", () => {
--total || success(done, io, c1, c2);
});
});
it('should be equivalent for "" and "/" on client', (done) => {
const io = new Server(0);
const c1 = createClient(io, "");
io.of("/").on("connection", successFn(done, io, c1));
});
it("should work with `of` and many sockets", (done) => {
const io = new Server(0);
const chat = createClient(io, "/chat");
const news = createClient(io, "/news");
let total = 2;
io.of("/news").on("connection", (socket) => {
expect(socket).to.be.a(Socket);
--total || success(done, io, chat, news);
});
io.of("/news").on("connection", (socket) => {
expect(socket).to.be.a(Socket);
--total || success(done, io, chat, news);
});
});
it("should work with `of` second param", (done) => {
const io = new Server(0);
const chat = createClient(io, "/chat");
const news = createClient(io, "/news");
let total = 2;
io.of("/news", (socket) => {
expect(socket).to.be.a(Socket);
--total || success(done, io, chat, news);
});
io.of("/news", (socket) => {
expect(socket).to.be.a(Socket);
--total || success(done, io, chat, news);
});
});
it("should disconnect upon transport disconnection", (done) => {
const io = new Server(0);
const chat = createClient(io, "/chat");
const news = createClient(io, "/news");
let total = 2;
let totald = 2;
let s;
io.of("/news", (socket) => {
socket.on("disconnect", (reason) => {
--totald || success(done, io, chat, news);
});
--total || close();
});
io.of("/chat", (socket) => {
s = socket;
socket.on("disconnect", (reason) => {
--totald || success(done, io, chat, news);
});
--total || close();
});
function close() {
s.disconnect(true);
}
});
it("should fire a `disconnecting` event just before leaving all rooms", (done) => {
const io = new Server(0);
const socket = createClient(io);
io.on("connection", (s) => {
s.join("a");
// FIXME not sure why process.nextTick() is needed here
process.nextTick(() => s.disconnect());
let total = 2;
s.on("disconnecting", (reason) => {
expect(s.rooms).to.contain(s.id, "a");
total--;
});
s.on("disconnect", (reason) => {
expect(s.rooms.size).to.eql(0);
--total || success(done, io, socket);
});
});
});
it("should return error connecting to non-existent namespace", (done) => {
const io = new Server(0);
const socket = createClient(io, "/doesnotexist");
socket.on("connect_error", (err) => {
expect(err.message).to.be("Invalid namespace");
success(done, io);
});
});
it("should not reuse same-namespace connections", (done) => {
const io = new Server(0);
const clientSocket1 = createClient(io);
const clientSocket2 = createClient(io);
let connections = 0;
io.on("connection", () => {
connections++;
if (connections === 2) {
success(done, io, clientSocket1, clientSocket2);
}
});
});
it("should find all clients in a namespace", (done) => {
const io = new Server(0);
const chatSids: string[] = [];
let otherSid: SocketId | null = null;
const c1 = createClient(io, "/chat");
const c2 = createClient(io, "/chat", { forceNew: true });
const c3 = createClient(io, "/other", { forceNew: true });
let total = 3;
io.of("/chat").on("connection", (socket) => {
chatSids.push(socket.id);
--total || getSockets();
});
io.of("/other").on("connection", (socket) => {
otherSid = socket.id;
--total || getSockets();
});
async function getSockets() {
const sids = await io.of("/chat").allSockets();
expect(sids).to.contain(chatSids[0], chatSids[1]);
expect(sids).to.not.contain(otherSid);
success(done, io, c1, c2, c3);
}
});
it("should find all clients in a namespace room", (done) => {
const io = new Server(0);
let chatFooSid: SocketId | null = null;
let chatBarSid: SocketId | null = null;
let otherSid: SocketId | null = null;
const c1 = createClient(io, "/chat");
const c2 = createClient(io, "/chat", { forceNew: true });
const c3 = createClient(io, "/other", { forceNew: true });
let chatIndex = 0;
let total = 3;
io.of("/chat").on("connection", (socket) => {
if (chatIndex++) {
socket.join("foo");
chatFooSid = socket.id;
--total || getSockets();
} else {
socket.join("bar");
chatBarSid = socket.id;
--total || getSockets();
}
});
io.of("/other").on("connection", (socket) => {
socket.join("foo");
otherSid = socket.id;
--total || getSockets();
});
async function getSockets() {
const sids = await io.of("/chat").in("foo").allSockets();
expect(sids).to.contain(chatFooSid);
expect(sids).to.not.contain(chatBarSid);
expect(sids).to.not.contain(otherSid);
success(done, io, c1, c2, c3);
}
});
it("should find all clients across namespace rooms", (done) => {
const io = new Server(0);
let chatFooSid: SocketId | null = null;
let chatBarSid: SocketId | null = null;
let otherSid: SocketId | null = null;
const c1 = createClient(io, "/chat");
const c2 = createClient(io, "/chat", { forceNew: true });
const c3 = createClient(io, "/other", { forceNew: true });
let chatIndex = 0;
let total = 3;
io.of("/chat").on("connection", (socket) => {
if (chatIndex++) {
socket.join("foo");
chatFooSid = socket.id;
--total || getSockets();
} else {
socket.join("bar");
chatBarSid = socket.id;
--total || getSockets();
}
});
io.of("/other").on("connection", (socket) => {
socket.join("foo");
otherSid = socket.id;
--total || getSockets();
});
async function getSockets() {
const sids = await io.of("/chat").allSockets();
expect(sids).to.contain(chatFooSid, chatBarSid);
expect(sids).to.not.contain(otherSid);
success(done, io, c1, c2, c3);
}
});
it("should not emit volatile event after regular event", (done) => {
const io = new Server(0);
let counter = 0;
io.of("/chat").on("connection", (s) => {
// Wait to make sure there are no packets being sent for opening the connection
setTimeout(() => {
io.of("/chat").emit("ev", "data");
io.of("/chat").volatile.emit("ev", "data");
}, 50);
});
const socket = createClient(io, "/chat");
socket.on("ev", () => {
counter++;
});
setTimeout(() => {
expect(counter).to.be(1);
success(done, io, socket);
}, 500);
});
it("should emit volatile event", (done) => {
const io = new Server(0);
let counter = 0;
io.of("/chat").on("connection", (s) => {
// Wait to make sure there are no packets being sent for opening the connection
setTimeout(() => {
io.of("/chat").volatile.emit("ev", "data");
}, 100);
});
const socket = createClient(io, "/chat");
socket.on("ev", () => {
counter++;
});
setTimeout(() => {
expect(counter).to.be(1);
success(done, io, socket);
}, 500);
});
it("should enable compression by default", (done) => {
const io = new Server(0);
const socket = createClient(io, "/chat");
io.of("/chat").on("connection", (s) => {
s.conn.once("packetCreate", (packet) => {
expect(packet.options.compress).to.be(true);
success(done, io, socket);
});
io.of("/chat").emit("woot", "hi");
});
});
it("should disable compression", (done) => {
const io = new Server(0);
const socket = createClient(io, "/chat");
io.of("/chat").on("connection", (s) => {
s.conn.once("packetCreate", (packet) => {
expect(packet.options.compress).to.be(false);
success(done, io, socket);
});
io.of("/chat").compress(false).emit("woot", "hi");
});
});
it("should throw on reserved event", () => {
const io = new Server();
expect(() => io.emit("connect")).to.throwException(
/"connect" is a reserved event name/
);
});
it("should close a client without namespace", (done) => {
const io = new Server(0, {
connectTimeout: 10,
});
const socket = createClient(io);
// @ts-ignore
socket.io.engine.write = () => {}; // prevent the client from sending a CONNECT packet
socket.on("disconnect", successFn(done, io, socket));
});
it("should exclude a specific socket when emitting", (done) => {
const io = new Server(0);
const socket1 = createClient(io, "/");
const socket2 = createClient(io, "/");
socket2.on("a", () => {
done(new Error("should not happen"));
});
socket1.on("a", successFn(done, io, socket1, socket2));
socket2.on("connect", () => {
io.except(socket2.id).emit("a");
});
});
it("should exclude a specific socket when emitting (in a namespace)", (done) => {
const io = new Server(0);
const nsp = io.of("/nsp");
const socket1 = createClient(io, "/nsp");
const socket2 = createClient(io, "/nsp");
socket2.on("a", () => {
done(new Error("not"));
});
socket1.on("a", successFn(done, io, socket1, socket2));
socket2.on("connect", () => {
nsp.except(socket2.id).emit("a");
});
});
it("should exclude a specific room when emitting", (done) => {
const io = new Server(0);
const nsp = io.of("/nsp");
const socket1 = createClient(io, "/nsp");
const socket2 = createClient(io, "/nsp");
socket1.on("a", successFn(done, io, socket1, socket2));
socket2.on("a", () => {
done(new Error("not"));
});
nsp.on("connection", (socket) => {
socket.on("broadcast", () => {
socket.join("room1");
nsp.except("room1").emit("a");
});
});
socket2.emit("broadcast");
});
it("should emit an 'new_namespace' event", (done) => {
const io = new Server();
io.on("new_namespace", (namespace) => {
expect(namespace.name).to.eql("/nsp");
done();
});
io.of("/nsp");
});
describe("dynamic namespaces", () => {
it("should allow connections to dynamic namespaces with a regex", (done) => {
const io = new Server(0);
const socket = createClient(io, "/dynamic-101");
const partialDone = createPartialDone(4, successFn(done, io, socket));
let dynamicNsp = io
.of(/^\/dynamic-\d+$/)
.on("connect", (socket) => {
expect(socket.nsp.name).to.be("/dynamic-101");
dynamicNsp.emit("hello", 1, "2", { 3: "4" });
partialDone();
})
.use((socket, next) => {
next();
partialDone();
});
socket.on("connect_error", (err) => {
expect().fail();
});
socket.on("connect", () => {
partialDone();
});
socket.on("hello", (a, b, c) => {
expect(a).to.eql(1);
expect(b).to.eql("2");
expect(c).to.eql({ 3: "4" });
partialDone();
});
});
it("should allow connections to dynamic namespaces with a function", (done) => {
const io = new Server(0);
const socket = createClient(io, "/dynamic-101");
io.of((name, query, next) => next(null, "/dynamic-101" === name));
socket.on("connect", successFn(done, io, socket));
});
it("should disallow connections when no dynamic namespace matches", (done) => {
const io = new Server(0);
const socket = createClient(io, "/abc");
io.of(/^\/dynamic-\d+$/);
io.of((name, query, next) => next(null, "/dynamic-101" === name));
socket.on("connect_error", (err) => {
expect(err.message).to.be("Invalid namespace");
success(done, io, socket);
});
});
it("should emit an 'new_namespace' event for a dynamic namespace", (done) => {
const io = new Server(0);
io.of(/^\/dynamic-\d+$/);
const socket = createClient(io, "/dynamic-101");
io.on("new_namespace", (namespace) => {
expect(namespace.name).to.be("/dynamic-101");
success(done, io, socket);
});
});
it("should handle race conditions with dynamic namespaces (#4136)", (done) => {
const io = new Server(0);
const counters = {
connected: 0,
created: 0,
events: 0,
};
const buffer: Function[] = [];
io.on("new_namespace", (namespace) => {
counters.created++;
});
const handler = () => {
if (++counters.events === 2) {
expect(counters.created).to.equal(1);
success(done, io, one, two);
}
};
io.of((name, query, next) => {
buffer.push(next);
if (buffer.length === 2) {
buffer.forEach((next) => next(null, true));
}
}).on("connection", (socket) => {
if (++counters.connected === 2) {
io.of("/dynamic-101").emit("message");
}
});
let one = createClient(io, "/dynamic-101");
let two = createClient(io, "/dynamic-101");
one.on("message", handler);
two.on("message", handler);
});
});
});

170
test/server-attachment.ts Normal file
View File

@@ -0,0 +1,170 @@
import { Server } from "..";
import { createServer } from "http";
import request from "supertest";
import expect from "expect.js";
import { getPort, successFn } from "./support/util";
describe("server attachment", () => {
describe("http.Server", () => {
const clientVersion = require("socket.io-client/package.json").version;
const testSource = (filename) => (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/" + filename)
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/javascript");
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.headers["x-sourcemap"]).to.be(undefined);
expect(res.text).to.match(/engine\.io/);
expect(res.status).to.be(200);
done();
});
};
const testSourceMap = (filename) => (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/" + filename)
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/json");
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.text).to.match(/engine\.io/);
expect(res.status).to.be(200);
done();
});
};
it("should serve client", testSource("socket.io.js"));
it(
"should serve client with query string",
testSource("socket.io.js?buster=" + Date.now())
);
it("should serve source map", testSourceMap("socket.io.js.map"));
it("should serve client (min)", testSource("socket.io.min.js"));
it("should serve source map (min)", testSourceMap("socket.io.min.js.map"));
it("should serve client (gzip)", (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/socket.io.js")
.set("accept-encoding", "gzip,br,deflate")
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-encoding"]).to.be("gzip");
expect(res.status).to.be(200);
done();
});
});
it(
"should serve bundle with msgpack parser",
testSource("socket.io.msgpack.min.js")
);
it(
"should serve source map for bundle with msgpack parser",
testSourceMap("socket.io.msgpack.min.js.map")
);
it("should serve the ESM bundle", testSource("socket.io.esm.min.js"));
it(
"should serve the source map for the ESM bundle",
testSourceMap("socket.io.esm.min.js.map")
);
it("should handle 304", (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/socket.io.js")
.set("If-None-Match", '"' + clientVersion + '"')
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.be(304);
done();
});
});
it("should handle 304", (done) => {
const srv = createServer();
new Server(srv);
request(srv)
.get("/socket.io/socket.io.js")
.set("If-None-Match", 'W/"' + clientVersion + '"')
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.be(304);
done();
});
});
it("should not serve static files", (done) => {
const srv = createServer();
new Server(srv, { serveClient: false });
request(srv).get("/socket.io/socket.io.js").expect(400, done);
});
it("should work with #attach", (done) => {
const srv = createServer((req, res) => {
res.writeHead(404);
res.end();
});
const sockets = new Server();
sockets.attach(srv);
request(srv)
.get("/socket.io/socket.io.js")
.end((err, res) => {
if (err) return done(err);
expect(res.status).to.be(200);
done();
});
});
it("should work with #attach (and merge options)", () => {
const srv = createServer((req, res) => {
res.writeHead(404);
res.end();
});
const server = new Server({
pingTimeout: 6000,
});
server.attach(srv, {
pingInterval: 24000,
});
// @ts-ignore
expect(server.eio.opts.pingTimeout).to.eql(6000);
// @ts-ignore
expect(server.eio.opts.pingInterval).to.eql(24000);
server.close();
});
});
describe("port", () => {
it("should be bound", (done) => {
const io = new Server(0);
request(`http://localhost:${getPort(io)}`)
.get("/socket.io/socket.io.js")
.expect(200, successFn(done, io));
});
it("with listen", (done) => {
const io = new Server().listen(0);
request(`http://localhost:${getPort(io)}`)
.get("/socket.io/socket.io.js")
.expect(200, successFn(done, io));
});
});
});

60
test/socket-middleware.ts Normal file
View File

@@ -0,0 +1,60 @@
import { Server } from "..";
import expect from "expect.js";
import { success, createClient } from "./support/util";
describe("socket middleware", () => {
it("should call functions", (done) => {
const io = new Server(0);
const clientSocket = createClient(io, "/", { multiplex: false });
clientSocket.emit("join", "woot");
let run = 0;
io.on("connection", (socket) => {
socket.use((event, next) => {
expect(event).to.eql(["join", "woot"]);
event.unshift("wrap");
run++;
next();
});
socket.use((event, next) => {
expect(event).to.eql(["wrap", "join", "woot"]);
run++;
next();
});
socket.on("wrap", (data1, data2) => {
expect(data1).to.be("join");
expect(data2).to.be("woot");
expect(run).to.be(2);
success(done, io, clientSocket);
});
});
});
it("should pass errors", (done) => {
const io = new Server(0);
const clientSocket = createClient(io, "/", { multiplex: false });
clientSocket.emit("join", "woot");
io.on("connection", (socket) => {
socket.use((event, next) => {
next(new Error("Authentication error"));
});
socket.use((event, next) => {
done(new Error("should not happen"));
});
socket.on("join", () => {
done(new Error("should not happen"));
});
socket.on("error", (err) => {
expect(err).to.be.an(Error);
expect(err.message).to.eql("Authentication error");
success(done, io, clientSocket);
});
});
});
});

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

@@ -1,8 +1,10 @@
"use strict";
import { Server, Socket } from "..";
import { Namespace, Server, Socket } from "..";
import type { DefaultEventsMap } from "../lib/typed-events";
import { createServer } from "http";
import { expectError, expectType } from "tsd";
import { Adapter } from "socket.io-adapter";
import type { DisconnectReason } from "../lib/socket";
// This file is run by tsd, not mocha.
@@ -16,10 +18,10 @@ describe("server", () => {
sio.on("connection", (s) => {
expectType<Socket<DefaultEventsMap, DefaultEventsMap>>(s);
s.on("disconnect", (reason) => {
expectType<string>(reason);
expectType<DisconnectReason>(reason);
});
s.on("disconnecting", (reason) => {
expectType<string>(reason);
expectType<DisconnectReason>(reason);
});
});
sio.on("connect", (s) => {
@@ -117,7 +119,9 @@ describe("server", () => {
it("does not accept arguments of wrong types", (done) => {
const srv = createServer();
const sio = new Server<BidirectionalEvents>(srv);
const sio = new Server<BidirectionalEvents, BidirectionalEvents, {}>(
srv
);
expectError(sio.on("random", (a, b, c) => {}));
srv.listen(() => {
expectError(sio.on("wrong name", (s) => {}));
@@ -205,6 +209,10 @@ describe("server", () => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
sio.emit("helloFromServer", "hi", 1);
sio.to("room").emit("helloFromServer", "hi", 1);
sio.timeout(1000).emit("helloFromServer", "hi", 1);
sio.on("connection", (s) => {
s.emit("helloFromServer", "hi", 10);
done();
@@ -216,6 +224,10 @@ describe("server", () => {
const srv = createServer();
const sio = new Server<ClientToServerEvents, ServerToClientEvents>(srv);
srv.listen(() => {
expectError(sio.emit("helloFromClient"));
expectError(sio.to("room").emit("helloFromClient"));
expectError(sio.timeout(1000).to("room").emit("helloFromClient"));
sio.on("connection", (s) => {
expectError(s.emit("helloFromClient", "hi"));
expectError(s.emit("helloFromServer", "hi", 10, "10"));
@@ -229,4 +241,69 @@ describe("server", () => {
});
});
});
describe("listen and emit event maps for the serverSideEmit method", () => {
interface ClientToServerEvents {
helloFromClient: (message: string) => void;
}
interface ServerToClientEvents {
helloFromServer: (message: string, x: number) => void;
}
interface InterServerEvents {
helloFromServerToServer: (message: string, x: number) => void;
}
describe("on", () => {
it("infers correct types for listener parameters", () => {
const srv = createServer();
const sio = new Server<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents
>(srv);
expectType<
Server<ClientToServerEvents, ServerToClientEvents, InterServerEvents>
>(sio);
srv.listen(() => {
sio.serverSideEmit("helloFromServerToServer", "hello", 10);
sio
.of("/test")
.serverSideEmit("helloFromServerToServer", "hello", 10);
sio.on("helloFromServerToServer", (message, x) => {
expectType<string>(message);
expectType<number>(x);
});
sio.of("/test").on("helloFromServerToServer", (message, x) => {
expectType<string>(message);
expectType<number>(x);
});
});
});
});
});
describe("adapter", () => {
it("accepts arguments of the correct types", () => {
const io = new Server({
adapter: (nsp) => new Adapter(nsp),
});
io.adapter(Adapter);
class MyCustomAdapter extends Adapter {
constructor(nsp, readonly opts) {
super(nsp);
}
}
io.adapter((nsp) => new MyCustomAdapter(nsp, { test: "123" }));
});
it("does not accept arguments of wrong types", () => {
const io = new Server();
expectError(io.adapter((nsp) => "nope"));
});
});
});

File diff suppressed because it is too large Load Diff

1043
test/socket.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,18 @@
import type { Server } from "../..";
import {
io as ioc,
ManagerOptions,
Socket as ClientSocket,
SocketOptions,
} from "socket.io-client";
const expect = require("expect.js");
const i = expect.stringify;
// add support for Set/Map
const contain = expect.Assertion.prototype.contain;
expect.Assertion.prototype.contain = function (...args) {
if (typeof this.obj === "object") {
if (this.obj instanceof Set || this.obj instanceof Map) {
args.forEach((obj) => {
this.assert(
this.obj.has(obj),
@@ -20,3 +28,53 @@ expect.Assertion.prototype.contain = function (...args) {
}
return contain.apply(this, args);
};
export function createClient(
io: Server,
nsp: string = "/",
opts?: Partial<ManagerOptions & SocketOptions>
): ClientSocket {
// @ts-ignore
const port = io.httpServer.address().port;
return ioc(`http://localhost:${port}${nsp}`, opts);
}
export function success(
done: Function,
io: Server,
...clients: ClientSocket[]
) {
io.close();
clients.forEach((client) => client.disconnect());
done();
}
export function successFn(
done: () => void,
sio: Server,
...clientSockets: ClientSocket[]
) {
return () => success(done, sio, ...clientSockets);
}
export function getPort(io: Server): number {
// @ts-ignore
return io.httpServer.address().port;
}
export function createPartialDone(count: number, done: (err?: Error) => void) {
let i = 0;
return () => {
if (++i === count) {
done();
} else if (i > count) {
done(new Error(`partialDone() called too many times: ${i} > ${count}`));
}
};
}
export function waitFor(emitter, event) {
return new Promise((resolve) => {
emitter.once(event, resolve);
});
}

View File

@@ -6,26 +6,10 @@ import expect from "expect.js";
import type { AddressInfo } from "net";
import "./support/util";
import { createPartialDone } from "./support/util";
const SOCKETS_COUNT = 3;
const createPartialDone = (
count: number,
done: () => void,
callback?: () => void
) => {
let i = 0;
return () => {
i++;
if (i === count) {
done();
if (callback) {
callback();
}
}
};
};
class DummyAdapter extends Adapter {
fetchSockets(opts: BroadcastOptions): Promise<any[]> {
return Promise.resolve([
@@ -49,7 +33,7 @@ class DummyAdapter extends Adapter {
}
}
describe("socket.io", () => {
describe("utility methods", () => {
let io: Server, clientSockets: ClientSocket[], serverSockets: Socket[];
beforeEach((done) => {
const srv = createServer();
@@ -59,7 +43,12 @@ describe("socket.io", () => {
clientSockets = [];
for (let i = 0; i < SOCKETS_COUNT; i++) {
clientSockets.push(ioc(`http://localhost:${port}`));
clientSockets.push(
ioc(`http://localhost:${port}`, {
// FIXME needed so that clients are properly closed
transports: ["websocket"],
})
);
}
serverSockets = [];
@@ -77,100 +66,99 @@ describe("socket.io", () => {
clientSockets.forEach((socket) => socket.disconnect());
});
describe("utility methods", () => {
describe("fetchSockets", () => {
it("returns all socket instances", async () => {
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(3);
});
describe("fetchSockets", () => {
it("returns all socket instances", async () => {
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(3);
});
it("returns all socket instances in the given room", async () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
const sockets = await io.in("room1").fetchSockets();
expect(sockets.length).to.eql(2);
});
it("returns all socket instances in the given room", async () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
const sockets = await io.in("room1").fetchSockets();
expect(sockets.length).to.eql(2);
});
it("works with a custom adapter", async () => {
io.adapter(DummyAdapter);
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(1);
const remoteSocket = sockets[0];
expect(remoteSocket.id).to.eql("42");
expect(remoteSocket.rooms).to.contain("42", "room1");
expect(remoteSocket.data).to.eql({ username: "john" });
it("works with a custom adapter", async () => {
io.adapter(DummyAdapter);
const sockets = await io.fetchSockets();
expect(sockets.length).to.eql(1);
const remoteSocket = sockets[0];
expect(remoteSocket.id).to.eql("42");
expect(remoteSocket.rooms).to.contain("42", "room1");
expect(remoteSocket.data).to.eql({ username: "john" });
});
});
describe("socketsJoin", () => {
it("makes all socket instances join the given room", () => {
io.socketsJoin("room1");
serverSockets.forEach((socket) => {
expect(socket.rooms).to.contain("room1");
});
});
describe("socketsJoin", () => {
it("makes all socket instances join the given room", () => {
io.socketsJoin("room1");
serverSockets.forEach((socket) => {
expect(socket.rooms).to.contain("room1");
});
});
it("makes all socket instances in a room join the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room1").socketsJoin("room3");
expect(serverSockets[0].rooms).to.contain("room3");
expect(serverSockets[1].rooms).to.contain("room3");
expect(serverSockets[2].rooms).to.not.contain("room3");
});
});
it("makes all socket instances in a room join the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room1").socketsJoin("room3");
expect(serverSockets[0].rooms).to.contain("room3");
expect(serverSockets[1].rooms).to.contain("room3");
expect(serverSockets[2].rooms).to.not.contain("room3");
});
describe("socketsLeave", () => {
it("makes all socket instances leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.not.contain("room1");
});
describe("socketsLeave", () => {
it("makes all socket instances leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.not.contain("room1");
});
it("makes all socket instances in a room leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.contain("room1");
});
});
it("makes all socket instances in a room leave the given room", () => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").socketsLeave("room1");
expect(serverSockets[0].rooms).to.contain("room2");
expect(serverSockets[0].rooms).to.not.contain("room1");
expect(serverSockets[1].rooms).to.contain("room1");
});
describe("disconnectSockets", () => {
it("makes all socket instances disconnect", (done) => {
io.disconnectSockets(true);
const partialDone = createPartialDone(3, done);
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", partialDone);
clientSockets[2].on("disconnect", partialDone);
});
describe("disconnectSockets", () => {
it("makes all socket instances disconnect", (done) => {
io.disconnectSockets(true);
it("makes all socket instances in a room disconnect", (done) => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").disconnectSockets(true);
const partialDone = createPartialDone(3, done);
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", partialDone);
clientSockets[2].on("disconnect", partialDone);
const partialDone = createPartialDone(2, () => {
clientSockets[1].off("disconnect");
done();
});
it("makes all socket instances in a room disconnect", (done) => {
serverSockets[0].join(["room1", "room2"]);
serverSockets[1].join("room1");
serverSockets[2].join("room2");
io.in("room2").disconnectSockets(true);
const partialDone = createPartialDone(2, done, () => {
clientSockets[1].off("disconnect");
});
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", () => {
done(new Error("should not happen"));
});
clientSockets[2].on("disconnect", partialDone);
clientSockets[0].on("disconnect", partialDone);
clientSockets[1].on("disconnect", () => {
done(new Error("should not happen"));
});
clientSockets[2].on("disconnect", partialDone);
});
});
});

211
test/uws.ts Normal file
View File

@@ -0,0 +1,211 @@
import {
App,
us_socket_local_port,
us_listen_socket_close,
} from "uWebSockets.js";
import { Server } from "..";
import { io as ioc, Socket as ClientSocket } from "socket.io-client";
import request from "supertest";
import expect from "expect.js";
const createPartialDone = (done: (err?: Error) => void, count: number) => {
let i = 0;
return () => {
if (++i === count) {
done();
} else if (i > count) {
done(new Error(`partialDone() called too many times: ${i} > ${count}`));
}
};
};
const shouldNotHappen = (done) => () => done(new Error("should not happen"));
describe("socket.io with uWebSocket.js-based engine", () => {
let io: Server,
uwsSocket: any,
port: number,
client: ClientSocket,
clientWSOnly: ClientSocket,
clientPollingOnly: ClientSocket,
clientCustomNamespace: ClientSocket;
beforeEach((done) => {
const app = App();
io = new Server();
io.attachApp(app);
io.of("/custom");
app.listen(0, (listenSocket) => {
uwsSocket = listenSocket;
port = us_socket_local_port(listenSocket);
client = ioc(`http://localhost:${port}`);
clientWSOnly = ioc(`http://localhost:${port}`, {
transports: ["websocket"],
});
clientPollingOnly = ioc(`http://localhost:${port}`, {
transports: ["polling"],
});
clientCustomNamespace = ioc(`http://localhost:${port}/custom`);
});
const partialDone = createPartialDone(done, 4);
client.on("connect", partialDone);
clientWSOnly.on("connect", partialDone);
clientPollingOnly.on("connect", partialDone);
clientCustomNamespace.on("connect", partialDone);
});
afterEach(() => {
io.close();
us_listen_socket_close(uwsSocket);
client.disconnect();
clientWSOnly.disconnect();
clientPollingOnly.disconnect();
clientCustomNamespace.disconnect();
});
it("should broadcast", (done) => {
const partialDone = createPartialDone(done, 3);
client.on("hello", partialDone);
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.emit("hello");
});
it("should broadcast in a namespace", (done) => {
client.on("hello", shouldNotHappen(done));
clientWSOnly.on("hello", shouldNotHappen(done));
clientPollingOnly.on("hello", shouldNotHappen(done));
clientCustomNamespace.on("hello", done);
io.of("/custom").emit("hello");
});
it("should broadcast in a dynamic namespace", (done) => {
const dynamicNamespace = io.of(/\/dynamic-\d+/);
const dynamicClient = clientWSOnly.io.socket("/dynamic-101");
dynamicClient.on("connect", () => {
dynamicNamespace.emit("hello");
});
dynamicClient.on("hello", () => {
dynamicClient.disconnect();
done();
});
});
it("should broadcast binary content", (done) => {
const partialDone = createPartialDone(done, 3);
client.on("hello", partialDone);
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.emit("hello", Buffer.from([1, 2, 3]));
});
it("should broadcast volatile packet with binary content", (done) => {
const partialDone = createPartialDone(done, 3);
client.on("hello", partialDone);
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
// wait to make sure there are no packets being sent for opening the connection
setTimeout(() => {
io.volatile.emit("hello", Buffer.from([1, 2, 3]));
}, 20);
});
it("should broadcast in a room", (done) => {
const partialDone = createPartialDone(done, 2);
client.on("hello", shouldNotHappen(done));
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
io.to("room1").emit("hello");
});
it("should broadcast in multiple rooms", (done) => {
const partialDone = createPartialDone(done, 2);
client.on("hello", shouldNotHappen(done));
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
io.to(["room1", "room2"]).emit("hello");
});
it("should broadcast in all but a given room", (done) => {
const partialDone = createPartialDone(done, 2);
client.on("hello", partialDone);
clientWSOnly.on("hello", partialDone);
clientPollingOnly.on("hello", shouldNotHappen(done));
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room2");
io.except("room2").emit("hello");
});
it("should work even after leaving room", (done) => {
const partialDone = createPartialDone(done, 2);
client.on("hello", partialDone);
clientWSOnly.on("hello", shouldNotHappen(done));
clientPollingOnly.on("hello", partialDone);
clientCustomNamespace.on("hello", shouldNotHappen(done));
io.of("/").sockets.get(client.id)!.join("room1");
io.of("/").sockets.get(clientPollingOnly.id)!.join("room1");
io.of("/").sockets.get(clientWSOnly.id)!.join("room1");
io.of("/").sockets.get(clientWSOnly.id)!.leave("room1");
io.to("room1").emit("hello");
});
it("should not crash when socket is disconnected before the upgrade", (done) => {
client.on("disconnect", () => done());
io.of("/").sockets.get(client.id)!.disconnect();
});
it("should serve static files", (done) => {
const clientVersion = require("socket.io-client/package.json").version;
request(`http://localhost:${port}`)
.get("/socket.io/socket.io.js")
.buffer(true)
.end((err, res) => {
if (err) return done(err);
expect(res.headers["content-type"]).to.be("application/javascript");
expect(res.headers.etag).to.be('"' + clientVersion + '"');
expect(res.headers["x-sourcemap"]).to.be(undefined);
expect(res.text).to.match(/engine\.io/);
expect(res.status).to.be(200);
done();
});
});
});

64
test/v2-compatibility.ts Normal file
View File

@@ -0,0 +1,64 @@
import { Server, Socket } from "..";
import expect from "expect.js";
import { success, getPort, waitFor } from "./support/util";
import * as io_v2 from "socket.io-client-v2";
describe("v2 compatibility", () => {
it("should connect if `allowEIO3` is true", (done) => {
const io = new Server(0, {
allowEIO3: true,
});
const clientSocket = io_v2.connect(`http://localhost:${getPort(io)}`, {
multiplex: false,
});
Promise.all([
waitFor(io, "connection"),
waitFor(clientSocket, "connect"),
]).then(([socket]) => {
expect((socket as Socket).id).to.eql(clientSocket.id);
success(done, io, clientSocket);
});
});
it("should be able to connect to a namespace with a query", (done) => {
const io = new Server(0, {
allowEIO3: true,
});
const clientSocket = io_v2.connect(
`http://localhost:${getPort(io)}/the-namespace`,
{
multiplex: false,
}
);
clientSocket.query = { test: "123" };
Promise.all([
waitFor(io.of("/the-namespace"), "connection"),
waitFor(clientSocket, "connect"),
]).then(([socket]) => {
expect((socket as Socket).handshake.auth).to.eql({ test: "123" });
success(done, io, clientSocket);
});
});
it("should not connect if `allowEIO3` is false (default)", (done) => {
const io = new Server(0);
const clientSocket = io_v2.connect(`http://localhost:${getPort(io)}`, {
multiplex: false,
});
clientSocket.on("connect", () => {
done(new Error("should not happen"));
});
clientSocket.on("connect_error", () => {
success(done, io, clientSocket);
});
});
});